LadyCailin
LadyCailin

Reputation: 931

How to use dash argument in Powershell?

I am porting a script from bash to PowerShell, and I would like to keep the same support for argument parsing in both. In the bash, one of the possible arguments is -- and I want to also detect that argument in PowerShell. However, nothing I've tried so far has worked. I cannot define it as an argument like param($-) as that causes a compile error. Also, if I decide to completely forego PowerShell argument processing, and just use $args everything appears good, but when I run the function, the -- argument is missing.

Function Test-Function {
    Write-Host $args
}

Test-Function -- -args go -here # Prints "-args go -here"

I know about $PSBoundParameters as well, but the value isn't there, because I can't bind a parameter named $-. Are there any other mechanisms here that I can try, or any solution?

For a bit more context, note that me using PowerShell is a side effect. This isn't expected to be used as a normal PowerShell command, I have also written a batch wrapper around this, but the logic of the wrapper is more complex than I wanted to write in batch, so the batch wrapper just calls the PowerShell function, which then does the more complex processing.

Upvotes: 5

Views: 6266

Answers (3)

oldium
oldium

Reputation: 171

I came up with the following solution, which works well also inside pipelines multi-line expressions. I am using the PowerShell Parser to parse the invocation expression string (while ignoring any incomplete tokens, which might be present at the end of $MyInfocation.Line value) and then Invoke-Expression with Write-Output to get the actual argument values:

# Parse the whole invocation line
$code = [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null)
# Find our invocation expression without redirections
$myline = $code.Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '
# Get the argument values
$command, [array] $arguments = Invoke-Expression ('Write-Output -- ' + $myline)
# Fine-tune arguments to be always an array
if ($null -eq $arguments ) { $arguments = @() }

Please be aware that the original values in the function call are reevaluated in Invoke-Expression, so any local variables might shadow values of the actual arguments. Because of that, you can also use this (almost) one-liner at the top of your function, which prevents the pollution of local variables:

# Parse arguments
$command, [array] $arguments = Invoke-Expression ('Write-Output -- ' + ([System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null).Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '))
# Fine-tune arguments to be always an array
if ($null -eq $arguments) { $arguments = @() }

Upvotes: 2

mklement0
mklement0

Reputation: 437608

As an aside: PowerShell allows a surprising range of variable names, but you have to enclose them in {...} in order for them to be recognized; that is, ${-} technically works, but it doesn't solve your problem.

The challenge is that PowerShell quietly strips -- from the list of arguments - and the only way to preserve that token is you precede it with the PSv3+ stop-parsing symbol, --%, which, however, fundamentally changes how the arguments are passed and is obviously an extra requirement, which is what you're trying to avoid.

Your best bet is to try - suboptimal - workarounds:

  • Option A: Quote the -- argument on invocation - either

    • as a whole (Test-Function '--' -args go -here)
    • or by `-escaping the first - (Test-Function `-- -args go -here).
    • While inconvenient for and likely unexpected by the caller, this approach allows you to formally declare parameters in your function, which Option C does not.
  • Option B: In your batch-file wrapper, translate -- to a special argument that PowerShell does preserve and pass it instead; the PowerShell script will then have to re-translate that special argument to --.

  • Option C: Perform custom argument parsing in PowerShell, as follows:

You can analyze $MyInvocation.Line, which contains the raw command line that invoked your script, and look for the presence of -- there.

Getting this right and making it robust is nontrivial, however, given that you need to determine where the list of arguments starts and ends, remove any redirections, and not only split the resulting argument-list string into individual arguments with support for quoted arguments, but you also need to expand those arguments, i.e. to evaluate arguments containing variable references and/or subexpressions.
Here's a reasonably robust approach:

# Don't use `param()` - instead, do your own argument parsing:

$customArgs = 
  if ($MyInvocation.Line) { # In-session invocation or CLI call with -Command (-c)
    # Extract the argument list from the invocation command line 
    # and strip out any redirections.
    $argList = (($MyInvocation.Line -replace ('^.*?' + [regex]::Escape($MyInvocation.InvocationName)) -split '[;|&]')[0] -replace '[0-9*]?>>?(&[0-9]| *\S+)').Trim()

    # Use Invoke-Expression with a Write-Output call to parse the 
    # raw argument list, performing evaluation and splitting it into
    # an array:
    if ($argList) { @(Invoke-Expression "Write-Output -- $argList") } else { @() }

  } else { # CLI call with -File (-f)
    # In this case, PowerShell does *not* strip out '--', 
    # so $args can simply be used.
    $args
    # !! However, this behavior is arguably a *bug*:
    # !! See https://github.com/PowerShell/PowerShell/issues/20208
    # To avoid relying on this, you can use [Environment]::CommandLine
    # in lieu of $MyInvocation.Line and (Split-Path -Leaf $MyInvocation.InvocationName) instead of $MyInvocation.InvocationName 
    # applied to the code in the `if` branch.
  }

# Print the resulting arguments array for verification:
$i = 0
$customArgs | % { "Arg #$((++$i)): [$_]" }

Note:

  • If you also want to support calls to your script via the -File / -f CLI parameter, you mustn't also use a param(...) block in your script, due to what is arguably a bug (see GitHub issue #20208).

    • On a related note, GitHub issue #21208 is a feature request to allow functions and scripts to be declared in a manner that makes PowerShell not strip -- from the arguments passed, in the context of then generally treating such a function and script as if it were an external program with respect to parameter passing.
      If this is ever implemented, no workarounds would be needed anymore (but note the side effects on parameter passing, such as non-support for arrays).
  • There are undoubtedly edge cases where the argument list may not be correctly extracted (e.g. if |, ;, & or > occur inside of (then of necessity quoted) arguments) or where the re-evaluation of the raw arguments causes side effect, but for the majority of cases - especially when called from outside PowerShell - this should do.

  • While useful here, Invoke-Expression should generally be avoided.

If your script is named foo.ps1 and you invoked it as ./foo.ps1 -- -args go -here, you'd see the following output:

Arg #1: [--]
Arg #2: [-args]
Arg #3: [go]
Arg #4: [-here]

Upvotes: 3

Shadowfax
Shadowfax

Reputation: 586

I found a way to do so, but instead of double-hyphen you have to pass 3 of them.

This is a simple function, you can change the code as you want:

function Test-Hyphen {
    param(
        ${-}
    )
    if (${-}) {
        write-host "You used triple-hyphen"
    } else {
        write-host "You didn't use triple-hyphen"
    }
}

Sample 1

Test-Hyphen

Output

You didn't use triple-hyphen

Sample 2

Test-Hyphen ---

Output

You used triple-hyphen

Upvotes: 4

Related Questions