src/CommandLine.ps1
|
$script:QuietLinePumpOptions=$null function quiet { $call = ConvertFrom-QuietLineCommandLine $args $MyInvocation.Statement $MyInvocation.MyCommand.Name Invoke-QuietPump @call } function pump { # Command-line form of Invoke-QuietPump: options first, then the command and its arguments, passed verbatim. A simple # function receives native options such as -c, -o or -v unbound, where an advanced function would claim them. $call = ConvertFrom-QuietLineCommandLine $args $MyInvocation.Statement $MyInvocation.MyCommand.Name Invoke-QuietPump @call } function Get-QuietLinePumpOption { # Invoke-QuietPump parameters accepted before the command, mapped to whether each one is a switch. Read once per session: # command metadata costs tens of milliseconds on every pump call. if ($null -eq $script:QuietLinePumpOptions) { $excluded = @('Command','ArgumentList') + @([Management.Automation.Cmdlet]::CommonParameters) + @([Management.Automation.Cmdlet]::OptionalCommonParameters) $options = @{} foreach ($parameter in (Get-Command Invoke-QuietPump -CommandType Function).Parameters.Values) { if ($parameter.Name -notin $excluded) { $options[$parameter.Name] = $parameter.SwitchParameter } } $script:QuietLinePumpOptions = $options } return $script:QuietLinePumpOptions } function Test-QuietLineParameterToken { # PowerShell marks the $args entries written as -name tokens; quoted strings and values are never options. param([object]$Value) return $Value -is [string] -and $null -ne $Value.PSObject.Properties['<CommandParameterName>'] } function ConvertFrom-QuietLineCommandLine { # Parameter tokens before the command are Invoke-QuietPump options, by exact name; every entry after the command is a # native argument. PowerShell splits "-name:value" into two entries, which are joined again. param([object[]]$Arguments, [string]$Statement, [string]$EntryPoint = 'quiet') $usage = "Usage: $EntryPoint [options] <command> [arguments]." $options = Get-QuietLinePumpOption $call = @{}; $index = 0 while ($index -lt $Arguments.Count -and (Test-QuietLineParameterToken $Arguments[$index])) { $token = [string]$Arguments[$index]; $index++ $name = $token.Substring(1).TrimEnd(':') if (!$options.ContainsKey($name)) { throw "Unknown $EntryPoint option '$token'. $usage" } if ($options[$name] -and !$token.EndsWith(':')) { $call[$name] = $true; continue } if ($index -eq $Arguments.Count) { throw "Missing value for $EntryPoint option '$token'. $usage" } $call[$name] = $Arguments[$index]; $index++ } if ($index -eq $Arguments.Count) { throw "Missing command. $usage" } $call.Command = [string]$Arguments[$index] $native = [Collections.Generic.List[string]]::new() for ($index++; $index -lt $Arguments.Count; $index++) { $value = $Arguments[$index] if ($value -is [scriptblock] -or $value -is [Collections.IDictionary]) { throw "$EntryPoint options precede the command; a script block or dictionary is not a native argument. $usage" } if ((Test-QuietLineParameterToken $value) -and $value.EndsWith(':') -and $index+1 -lt $Arguments.Count) { $index++; $native.Add($value + $Arguments[$index]); continue } foreach ($item in @($value)) { if ($null -ne $item) { $native.Add([string]$item) } } } $call.ArgumentList = $native.ToArray() Assert-QuietLineSeparator $Statement $options $EntryPoint return $call } function Get-QuietLineElementKind { param([object]$Element) if ($Element -is [Management.Automation.Language.CommandParameterAst]) { return $(if ($Element.ParameterName -eq '-') { 'Separator' } else { 'Option' }) } if ($Element -is [Management.Automation.Language.VariableExpressionAst] -and $Element.Splatted) { return 'Splat' } return 'Value' } function Assert-QuietLineSeparator { # PowerShell removes the first -- of a function call before pump receives its arguments. Before the command it # only ends the options; after the command it was a native argument, which cannot be restored. param([string]$Statement, [hashtable]$Options, [string]$EntryPoint = 'quiet') $ast = [Management.Automation.Language.Parser]::ParseInput($Statement, [ref]$null, [ref]$null) # A pump call inside a script block of this statement (such as a callback) is checked when it runs. $commands = $ast.FindAll({ param($node) $node -is [Management.Automation.Language.CommandAst] -and $node.GetCommandName() -match '(^|\\)(?:pump|quiet)$' }, $false) foreach ($command in $commands) { $elements = $command.CommandElements $index = 1 while ($index -lt $elements.Count -and (Get-QuietLineElementKind $elements[$index]) -in @('Option','Splat')) { $element = $elements[$index]; $index++ if ((Get-QuietLineElementKind $element) -eq 'Option' -and !$element.Argument -and !$Options[$element.ParameterName]) { $index++ } } # The options end at the separator or at the command; a separator after the command was removed from it. for ($index++; $index -lt $elements.Count; $index++) { if ((Get-QuietLineElementKind $elements[$index]) -eq 'Separator') { throw "PowerShell removed the first '--' of this command line, which followed the command. Write one before the command as well: $EntryPoint [options] -- <command> [arguments]." } } } } |