PSNativeCmdDevKit.psm1

#Region './Public/Add-SudoPreferenceRule.ps1' -1

<#
    .SYNOPSIS
        Adds or changes a sudo preference rule.

    .DESCRIPTION
        Registers a command-specific sudo rule or enables or disables sudo for
        every native command. Parameter filters must be script blocks or the
        wildcard string '*'. Script blocks are retained without recompilation.

    .PARAMETER Executable
        Specifies the executable to which the rule applies. Use '*' with a
        wildcard or constant script-block filter to configure all commands.

    .PARAMETER ParameterFilterRule
        Specifies '*' to match every argument list or a script block that
        evaluates the command arguments through its automatic $args variable.

    .PARAMETER EnableSudoForAllCommands
        Enables sudo for every native command.

    .PARAMETER DisableSudoForAllCommands
        Disables the global sudo preference without deleting command rules.

    .PARAMETER SudoUser
        Specifies the user supplied to `sudo -u` when the matching rule applies.

    .EXAMPLE
        Add-SudoPreferenceRule -Executable 'dpkg' -ParameterFilterRule {
            $args -contains '--install'
        }

        Adds a rule that uses sudo for dpkg installation commands.

    .EXAMPLE
        Add-SudoPreferenceRule -EnableSudoForAllCommands -SudoUser 'root'

        Enables sudo for all native commands and selects the root user.
#>


function  Add-SudoPreferenceRule
{
    param
    (

        [Parameter(ParameterSetName = 'Sudo', Mandatory = $true)]
        [Alias('Command')]
        # The binary or command the rule will affect.
        [string]
        $Executable,

        [Parameter(ParameterSetName = 'Sudo', Mandatory = $true)]
        # The Parameter filter to be evaluated for the command.
        # if you want to use sudo for an Executable, regardless of the parameters, use:
        # `-ParameterFilterRule *` or `-ParameterFilterRule {$true}`
        # Otherwise, you can evaluate the Parameters to be used, populated the $Args variable:
        # `-ParameterFilterRule {$args -contains '-i' -or $args -contains '--install'}`
        [object]
        $ParameterFilterRule,

        [Parameter(ParameterSetName = 'SudoAll', Mandatory = $true)]
        # This will Enable sudo for any command, but won't destroy your
        # registered settings. You can set a $SudoUser to be used along.
        [switch]
        $EnableSudoForAllCommands,

        [Parameter(ParameterSetName = 'NoSudoAll', Mandatory = $true)]
        # This will ensure sudo is not automatically added to each command,
        # instead it will use the Sudo Preference rules registered with `Add-SudoPreferenceRule`.
        [switch]
        $DisableSudoForAllCommands,

        [Parameter(ParameterSetName = 'Sudo')]
        [Parameter(ParameterSetName = 'SudoAll')]
        # The executable that is invoked with sudo should be run as this user.
        # the resulting command invoked will be `sudo <sudo user> <executable> <parameters>`.
        [string]
        $SudoUser
    )

    if ($script:SudoPreferenceRules -isnot [System.Collections.ArrayList])
    {
        # There is no default rules store, let's create an array list
        $script:SudoPreferenceRules = [System.Collections.ArrayList]::new()
    }

    if ($PSCmdlet.ParameterSetName -eq 'Sudo' -and
        $ParameterFilterRule -isnot [scriptblock] -and
        $ParameterFilterRule -ne '*')
    {
        throw [System.ArgumentException]::new(
            'ParameterFilterRule must be a ScriptBlock or the wildcard string ''*''.'
        )
    }

    if ($EnableSudoForAllCommands.IsPresent -or $DisableSudoForAllCommands.IsPresent)
    {
        $Script:SudoAll = switch ($PSCmdlet.ParameterSetName)
        {
            NoSudoAll   { $false  }
            SudoAll     { $true   }
        }

        # If sudoUser is specified, set to SudoAllAs. Clean up if disabling SudoAll
        $script:SudoAllAs = $SudoUser
        return
    }
    elseif ($Executable -eq '*')
    {
        $Script:SudoAll = switch ($ParameterFilterRule)
        {
            '*'     { $true }
            default { [bool]$ParameterFilterRule.Invoke() }
        }

        $script:SudoAllAs = $SudoUser
    }

    $index = $null

    if (Get-SudoPreferenceRule -Executable $Executable -ParameterFilterRule $ParameterFilterRule)
    {
        Write-Verbose "Sudo Preference Rule found. Replacing"
        $index = [int](Remove-SudoPreferenceRule -Executable $Executable -ParameterFilterRule $ParameterFilterRule)
    }

    # copy hash with Executable, ParameterFilterRule, and SudoUser if present
    $newRule = @{
        Executable          = $Executable
        ParameterFilterRule = $ParameterFilterRule
        Sudo                = $true
        SudoAs              = $SudoUser
    }

    if ($null -ne $index)
    {
        Write-Debug "Replacing Sudo rule for '$Executable' with filter '$ParameterFilterRule' at index $index"
        $null = $script:SudoPreferenceRules.Insert($index, $newRule)
    }
    else
    {
        Write-Debug "Adding Sudo rule for '$Executable' with filter '$ParameterFilterRule'"
        $null = $script:SudoPreferenceRules.Add($newRule)
    }
}
#EndRegion './Public/Add-SudoPreferenceRule.ps1' 145
#Region './Public/Get-PropertyHashFromListOutput.ps1' -1

<#
    .SYNOPSIS
        Converts line-oriented native output into a property hashtable.

    .DESCRIPTION
        Parses lines containing named property and value groups, normalizes
        property names, appends continuation lines, and routes redirected
        standard-error records to a caller-provided handler.

    .PARAMETER Output
        Specifies one or more native-command output lines or error records.

    .PARAMETER Regex
        Specifies the regular expression used to capture named `property` and
        `val` groups.

    .PARAMETER AllowedPropertyName
        Specifies property names to retain at the top level. The default '*'
        accepts every parsed property.

    .PARAMETER DiscardExtraProperties
        Discards parsed properties that are not listed in AllowedPropertyName.

    .PARAMETER AddExtraPropertiesAsKey
        Specifies the nested hashtable key used for properties that are not in
        AllowedPropertyName.

    .PARAMETER ErrorHandling
        Specifies the script block that receives redirected standard-error
        records.

    .EXAMPLE
        'Name: PowerShell', 'Version: 7.6' |
            Get-PropertyHashFromListOutput

        Returns a hashtable containing Name and Version.
#>


function Get-PropertyHashFromListOutput
{
    [CmdletBinding(DefaultParameterSetName = 'AddExtraPropertiesUnderKey')]
    [OutputType([hashtable])]
    param
    (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [Object]
        # Output from a command, typically the result of Invoke-LinuxCommand.
        # Error records will be handled by the scriptblock in -ErrorHandling parameter.
        # The latter defaults to send the error record to Write-Error.
        $Output,

        [Parameter()]
        # Regex with 'property' & 'val' Named groups
        # of a string to extract an hashtable key/value pair from a string.
        [regex]
        $Regex = '^\s*(?<property>[\w-\s]*):\s*(?<val>.*)',

        [Parameter()]
        # List of property names allowed to be parsed.
        # Default to '*' for all properties, otherwise the parsed properties
        # not listed here will either be discarded if -DiscardExtraProperties is set
        # or will be added to a hashtable under the key named $AddExtraPropertiesAsKey.
        [string[]]
        $AllowedPropertyName = '*',

        [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DiscardExtraProperties')]
        # When only a limited number of Property named is allowed using -AllowedPropertyName
        # parameter, the extra properties will be discarded.
        [switch]
        $DiscardExtraProperties,

        [Parameter(ParameterSetName = 'AddExtraPropertiesUnderKey')]
        # When only a limited number of Property named is allowed using -AllowedPropertyName
        # parameter, the extra properties will be added under the `$property[$AddExtraPropertiesAsKey]`
        # hash. For instance, `$property['ExtraProperties']['NotAllowedPropertyName'] = $ParsedValue`
        [string]
        $AddExtraPropertiesAsKey = 'ExtraProperties',

        [Parameter()]
        # When the output of a native command has had its `STDERR` redirected
        # using `2>&1`, we'll send the ErrorRecords (output from STDERR) to
        # this scriptblock. By default: `$errorRecord | &{ Write-Error $_}`.
        [scriptblock]
        $ErrorHandling = { Write-Error $_ }
    )

    begin
    {
        $properties = @{}
        if (-not $DiscardExtraProperties.isPresent)
        {
            $properties[$AddExtraPropertiesAsKey] = @{}
        }
    }

    process
    {
        foreach ($line in $Output)
        {
            Write-Debug "Output Line: $line"
            if ($line -is [System.Management.Automation.ErrorRecord])
            {
                $line | &$ErrorHandling
            }
            elseif ($line -match $Regex)
            {
                $propertyName = $Matches.property.replace('-','').replace(' ','')
                if ($AllowedPropertyName -contains '*' -or $AllowedPropertyName -contains $propertyName)
                {
                    $properties.Add($propertyName, $Matches.val)
                }
                else
                {
                    if (-not $DiscardExtraProperties.isPresent)
                    {
                        Write-Debug " Adding Property '$propertyName' to $AddExtraPropertiesAsKey"
                        $properties[$AddExtraPropertiesAsKey].Add($propertyName, $Matches.val)
                    }
                }

                $lastProperty = $propertyName
            }
            else
            {
                if (-not $lastProperty)
                {
                    Write-Verbose $line
                }
                elseif ($AllowedPropertyName -contains '*' -or $AllowedPropertyName -contains $lastProperty)
                {
                    Write-Debug " Adding second line to property $lastProperty"
                    $properties[$lastProperty] += "`n" + $line.TrimEnd()
                }
                elseif (-not $DiscardExtraProperties.IsPresent)
                {
                    $properties[$AddExtraPropertiesAsKey][$lastProperty] += $line.Trim()
                }
            }
        }
    }

    end
    {
        if ($properties[$AddExtraPropertiesAsKey].Count -eq 0)
        {
            Write-Debug "No Extra properties where found, removing unnecessary key '$AddExtraPropertiesAsKey'"
            $properties.Remove($AddExtraPropertiesAsKey)
        }

        $properties
    }
}
#EndRegion './Public/Get-PropertyHashFromListOutput.ps1' 153
#Region './Public/Get-SudoPreference.ps1' -1

<#
    .SYNOPSIS
        Resolves the sudo preference for a native command.

    .DESCRIPTION
        Returns the first global or command-specific sudo preference whose
        executable and argument filter match the supplied invocation.

    .PARAMETER Executable
        Specifies the executable for which to resolve a sudo preference.

    .PARAMETER Parameters
        Specifies the native argument values evaluated by script-block filters.

    .EXAMPLE
        Get-SudoPreference -Executable 'dpkg' -Parameters '--install', 'package.deb'

        Returns the matching sudo preference, when one is registered.
#>


function Get-SudoPreference
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [Alias('Command')]
        # The binary or command to be executed.
        [string]
        $Executable,

        [Parameter()]
        # List of parameters to pass to the invocation that will be
        # evaluated against the registered Sudo Preference Rules.
        [String[]]
        $Parameters
    )

    if ($script:SudoAll)
    {
        @{
            Sudo   = $true
            SudoAs = $script:SudoAllAs
        }
    }
    elseif ($script:SudoPreferenceRules)
    {
        $RuleMatchFound = $script:SudoPreferenceRules | Where-Object -FilterScript {
            $Executable -eq $_.Executable -and
            ($_.ParameterFilterRule -eq '*' -or $_.ParameterFilterRule.Invoke($Parameters))
        } | Select-Object -First 1

        if ($RuleMatchFound)
        {
            return [hashtable]$RuleMatchFound
        }
        else
        {
            Write-Debug "No matching rules for '$Executable' with params '$Parameters'"
        }
    }
}
#EndRegion './Public/Get-SudoPreference.ps1' 64
#Region './Public/Get-SudoPreferenceRule.ps1' -1

<#
    .SYNOPSIS
        Gets registered sudo preference rules.

    .DESCRIPTION
        Returns all registered rules or filters rules by executable and,
        optionally, by the exact parameter-filter object.

    .PARAMETER Executable
        Specifies the executable whose registered rules should be returned.

    .PARAMETER ParameterFilterRule
        Specifies the exact script block or wildcard filter to match.

    .PARAMETER All
        Returns every registered sudo preference rule.

    .EXAMPLE
        Get-SudoPreferenceRule -Executable 'dpkg'

        Returns all rules registered for dpkg.
#>


function  Get-SudoPreferenceRule
{
    [CmdletBinding(DefaultParameterSetName = 'all')]
    [OutputType([System.Object[]])]
    param
    (

        [Parameter(ParameterSetName = 'byCommand', Mandatory = $true)]
        [Alias('Command')]
        # The binary or command to be executed.
        [string]
        $Executable,

        [Parameter(ParameterSetName = 'byCommand')]
        [object]
        $ParameterFilterRule,

        [Parameter(ParameterSetName = 'all')]
        [switch]
        $All

    )

    if ($script:SudoPreferenceRules -isnot [System.Collections.ArrayList])
    {
        # There is no default rules store, let's create an array list and return it
        $script:SudoPreferenceRules = [System.Collections.ArrayList]::new()
    }

    if ($PSCmdlet.ParameterSetName -eq 'All')
    {
        $script:SudoPreferenceRules
    }
    else
    {
        $script:SudoPreferenceRules.Where{
            $_.Executable -eq $Executable -and
            $(
                if ($ParameterFilterRule -and $ParameterFilterRule -ne '*')
                {
                    $_.ParameterFilterRule -eq $ParameterFilterRule
                }
                else
                {
                    $true
                }
            )
        }
    }
}
#EndRegion './Public/Get-SudoPreferenceRule.ps1' 74
#Region './Public/Invoke-NativeCommand.ps1' -1

<#
    .SYNOPSIS
        Invokes a native command with an argument array.

    .DESCRIPTION
        Invokes an executable without constructing PowerShell source code,
        optionally applies sudo preferences on Linux or macOS, and redirects
        standard error into the success stream for downstream parsing.

    .PARAMETER Executable
        Specifies the native executable or command to invoke.

    .PARAMETER Sudo
        Invokes the command through sudo on Linux or macOS.

    .PARAMETER SudoAs
        Invokes the command through `sudo -u` for the specified user on Linux
        or macOS.

    .PARAMETER Parameters
        Specifies native argument values in their required order.

    .EXAMPLE
        Invoke-NativeCommand -Executable 'git' -Parameters '--version'

        Invokes git and returns its combined standard output and standard error.
#>


function Invoke-NativeCommand
{
    [cmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [Alias('Command')]
        # The binary or command you would like to execute.
        [string]
        $Executable,

        [Parameter()]
        # Whether you want to sudo the command invocation, on non-windows OSes.
        # If you want to sudo as a different user, use the parameter `-SudoAs`.
        [switch]
        $Sudo,

        [Parameter()]
        # Specify a user to sudo he command as. i.e.: `sudo otheruser ls -alh`
        [String]
        $SudoAs,

        [Parameter()]
        # list of Parameters to pass to the invocation.
        # For binaries and commands requiring a specific order
        # make sure it is respected as no further check is done.
        [String[]]
        $Parameters
    )

    # If Sudo or SudoAs is not specified, lookup in the Module variable DefaultCommandToSudo
    if ( -not ($PSBoundParameters.ContainsKey('Sudo') -or $PSBoundParameters.ContainsKey('SudoAs')) )
    {
        if ($DefaultSudo = Get-SudoPreference @PSBoundParameters)
        {
            $Sudo   = $DefaultSudo.Sudo
            $SudoAs = $DefaultSudo.SudoAs
        }
    }

    [string] $Command = $Executable
    [string[]] $CommandParameters = @()

    if ($SudoAs -and ($IsLinux -or $IsMacOS))
    {
        $Command = 'sudo'
        $CommandParameters += '-u'
        $CommandParameters += $SudoAs
        $CommandParameters += $Executable
    }
    elseif ($Sudo -and ($IsLinux -or $IsMacOS))
    {
        $Command = 'sudo'
        $CommandParameters += $Executable
    }

    $CommandParameters += $Parameters

    Write-Verbose -Message "Running #> $Command $CommandParameters"

    # Stream output through the pipeline and mix STDERR with STDOUT.
    & $Command @CommandParameters 2>&1
}
#EndRegion './Public/Invoke-NativeCommand.ps1' 92
#Region './Public/Remove-SudoPreferenceRule.ps1' -1

<#
    .SYNOPSIS
        Removes registered sudo preference rules.

    .DESCRIPTION
        Removes rules by executable and filter, by internal index, or clears
        every registered rule.

    .PARAMETER Executable
        Specifies the executable whose matching rules should be removed.

    .PARAMETER ParameterFilterRule
        Specifies the exact script block or wildcard filter to remove.

    .PARAMETER Index
        Specifies the internal zero-based rule index to remove.

    .PARAMETER All
        Removes every registered sudo preference rule.

    .EXAMPLE
        Remove-SudoPreferenceRule -Executable 'dpkg' -ParameterFilterRule '*'

        Removes wildcard sudo rules registered for dpkg.
#>


function Remove-SudoPreferenceRule
{
    [cmdletBinding()]
    param
    (
        [Parameter(ParameterSetName = 'ByValue', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Command')]
        # The executable that has the rule applied to.
        [string]
        $Executable,

        [Parameter(ParameterSetName = 'ByValue', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        # The parameter filter rule to match with the executable to remve.
        [object]
        $ParameterFilterRule,

        [Parameter(Dontshow = $true, ParameterSetName = 'ByIndex', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        # Remove the Rule stored in the module's $script:SudoPreferenceRules by its index (advanced user only)
        [int]
        $index,

        [Parameter(ParameterSetName = 'All', Mandatory = $true)]
        # Remove all previously registered rules.
        [switch]
        $All
    )

    begin
    {
        if ($script:SudoPreferenceRules -isnot [System.Collections.ArrayList])
        {
            # There is no default rules store
            return
        }
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'ByIndex')
        {
            $script:SudoPreferenceRules.RemoveAt($Index)
            return
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'All')
        {
            $script:SudoPreferenceRules.Clear()
            return
        }

        $CurrentIndex = 0
        $indexesToRemove = $script:SudoPreferenceRules.Foreach{
            if ($_.Executable -eq $Executable -and
                ($_.ParameterFilterRule -eq '*' -or $_.ParameterFilterRule -eq $ParameterFilterRule)
            )
            {
                $CurrentIndex
            }

            $CurrentIndex++
        }

        $indexesToRemove |
            Sort-Object -Descending |
            ForEach-Object {
            $script:SudoPreferenceRules.RemoveAt($_)
            # return the Indexes where the rule has been removed
            $_
        }
    }
}
#EndRegion './Public/Remove-SudoPreferenceRule.ps1' 97