PowerShell: Unable to provide .ps1 script multiple values for a single parameter (when called from a non-pwsh environment)

Prerequisites

Steps to reproduce

It’s currently impossible to supply an ‘array’ of strings as an argument. Of course pwsh should not attempt to parse any input (so ./script.ps1 -x a,b should NOT result in $x = ('a','b')), but there seems to be no way to do this (calling from within a pwsh shell notwithstanding, naturally).

For instance a cron job script cannot successfully call the below powershell without script-specific hacks like [Parameter(ValueFromRemainingArguments=$true)] (which aren’t applicable to multi-parameter applications).

$ cat test.ps1
#!/bin/pwsh
Param($x)
$x[0]
'!'
$x[1]

This might be better suited as a feature request but at the very least the error message is definitely not correct. Other than this niggle pwsh does an absolutely amazing job at argument parsing.

Expected behavior

$ ./test.ps1 -x a -x b #from bash, for example
a
!
b

Actual behavior

#gives error that's not even applicable to the situation (asks to use "array syntax", an in-pwsh structure)

Error details

test.ps1: Cannot bind parameter because parameter 'x' is specified more than once. To provide multiple values to parameters that can accept multiple values, use the array syntax. For example, "-parameter value1,value2,value3".

Environment data

Name                           Value
----                           -----
PSVersion                      7.3.6
PSEdition                      Core
GitCommitId                    7.3.6
OS                             Linux 5.10.0-19-amd64 #1 SMP Debian 5.10.149-2 (2022-10-21)
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response

About this issue

  • Original URL
  • State: open
  • Created 10 months ago
  • Comments: 28 (13 by maintainers)

Most upvoted comments

There is a way to supply array-parameters and there there is also a way that everything becomes ONE string. Running pwsh from outside powershell.

No, what you’ve done is completely side-stepped the issue, and ran with the cut-down minimal reproducible example to test it. Like with Dmitry suggesting ValueFromRemainingArguments which I mentioned in the original post as insufficient (introduce a variable $y also expecting multiple values…), your solution ignores ‘calling from a non-pwsh env/should not attempt to parse’, changing the problem from “how do I pass in my arguments as the script expects” to “now I need to write a script to call my script”, a much more convoluted issue!

Consider attempting $x = ''''$!%','arg2'; we now no longer just need to worry about escaping in the calling language (which we already had to and is the onus of the programmer) but ALSO escaping for pwsh (which the caller might not even use, remember they’re calling our script, not necessarily authoring it). The -command framework would yield…

pwsh -command ./test.ps1 -x \`\'\`\$\!\`%,arg2
#'$!%
#!
#arg2

A simpler way of doing this would be to serialise your inputs (say, via json and ConvertFrom-Json in your -command) and maybe also encode for good measure (say via base64 in the calling language, and back before the convertfrom).

As Michael realised, -x -x is the only backwards-compatible solution and has precedent in all other shells & multi-var-requiring posix commands. The only issue I can foresee is people relying on the error message as a sanity check. For which I could propose a flag for pwsh; -AcceptMultipleParameters, for which the crunchbang would become #!/bin/pwsh -AcceptMultipleParameters -File (I think), with zero change in existing behaviour (though I still strongly believe the existing behaviour to be not-usefully limiting).

He also concurred with my last thought there that this may make it a change request, but the verbiage of the error message itself is still “buggy”. With the fix to #1908 finally permitting devs to use pwsh in posix environments (and indeed allow us Windows users to drop cmd completely), this is a lesser issue nevertheless hindering users from taking our scripts into posix environments, and it would be positively fantastic to see a proper solution.

To flesh the scenario out a bit:

  • A script with a shebang line runs in a child process, implicitly via pwsh -File; this allows such scripts to be general-purpose CLIs on Unix, directly callable from other shells, such as Bash.

  • To PowerShell, such a script is an external program (as an explicit pwsh -File call would be), and the usual rules for such calls apply - which are distinct from passing arguments to in-process .ps1 script calls.

Since there is no array support, a robust emulation of one in combination with a named parameter is to allow binding this parameter multiple times, which is also not supported.

In external utilities, support for passing an option multiple times isn’t unusual, e.g. curl’s -o option (curl example.com example.net -o aa -o bb)

This syntax does not work & doubtful it ever will as it makes little sense in calling a parameter twice like this ./test.ps1 -x a -x b

this however does ./test.ps1 -x 'a','b'

@237dmitry: Simpler, yes - but limited to unnamed arguments. (Conceivably, you emulate named arguments by examining the positional arguments and treating those that look like parameter names as such, but that would be quite cumbersome).

would be to pass all arguments as a single

It simpler to deal with $args: script a b

#! shebang
$args[0]
'!'
$args[1]

Yes, I’d say this is a feature request:

  • Shebang line-based invocation is the same as a CLI invocation with pwsh -File, which doesn’t support PowerShell-style arrays - only pwsh -Command calls do.

  • PowerShell fundamentally disallows binding the same parameter multiple times (such as the two -x arguments in your example), which is what the error message indicates.

    • However, the error message is worth amending to note that it only pertains to in-session and pwsh -Command calls.

In limited scenarios, the ValueFromRemainingArguments workaround offered by @237dmitry may do, but, due to its ambiguity, it isn’t a robust solution.

The only robust - but cumbersome - workaround I see at the moment would be to pass all arguments as a single, ,-separated string argument (e.g., -x a,b, quoted as necessary), and make the target script parse it into elements with -split ','

From what I can tell, the only way to provide a proper solution that doesn’t break backward compatibility is to indeed allow binding a parameter multiple times, assuming it is either untyped (implied [object]) or array-typed - and this definitely calls for a feature request (and my sense is that changes to the parameter binder have to be justified by a pressing need).

It works in this variant (NOT printing x parameter explicitly):

$ cat ./aaa
#!/bin/env -S pwsh -nop

Param (
    [Parameter(ValueFromRemainingArguments)] $x
)

$x[0]
'!'
$x[1]

$ ./aaa a b
a
!
b

or:

#!/bin/env -S pwsh -nop

Param (

    [Parameter()] $x,
    [Parameter(ValueFromRemainingArguments)] $y
)

$x
'!'
$y[0]

# ./aaa -x a b c d
# ./aaa a b c d