powershellparameter-passing

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.


Solution

  • 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:

    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 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]