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.
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
Test-Function '--' -args go -here
)`
-escaping the first -
(Test-Function `-- -args go -here
).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).
--
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.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]