在 PowerShell 中使用变量将多个参数传递给外部程序

mie*_*ooy 5 powershell junit parameter-passing parameter-splatting

我下载了用于合并 junit 报告的 npm 包 - https://www.npmjs.com/package/junit-merge

问题是我有多个文件要合并,并且我正在尝试使用字符串变量来保存要合并的文件名。

当我自己编写脚本时,如下所示:

junit-merge a.xml b.xml c.xml 
Run Code Online (Sandbox Code Playgroud)

这有效,正在创建合并文件,但是当我这样做时

$command = "a.xml b.xml c.xml"
junit-merge $command
Run Code Online (Sandbox Code Playgroud)

这是行不通的。错误是

错误:找不到文件

有人遇到过类似的问题吗?

mkl*_*nt0 7

# WRONG
Run Code Online (Sandbox Code Playgroud)

$command = "a.xml b.xml c.xml"; junit-merge $command

results in command line junit-merge "a.xml b.xml c.xml"[1], i.e. it passes a string with verbatim value a.xml b.xml c.xml as a single argument to junit-merge, which is not the intent.

PowerShell does not act like POSIX-like shells such as bash do in this regard: In bash, the value of variable $command - due to being referenced unquoted - would be subject to word splitting (one of the so-called shell expansions) and would indeed result in 3 distinct arguments (though even there an array-based invocation would be preferable).
PowerShell supports no bash-like shell expansions[2]; it has different, generally more flexible constructs, such as the splatting technique discussed below.

Instead, define your arguments as individual elements of an array, as justnotme advises:

# Define the *array* of *individual* arguments.
$command = "a.xml", "b.xml", "c.xml"

# Pass the array to junit-merge, which causes PowerShell
# to pass its elements as *individual arguments*; it is the equivalent of:
#     junit-merge  a.xml  b.xml  c.xml
junit-merge $command
Run Code Online (Sandbox Code Playgroud)

This is an application of a PowerShell technique called splatting, where you specify arguments to pass to a command via a variable:

  • Either (typically only used for external programs, as in your case):

    • As an array of arguments to pass individually as positional arguments, as shown above. In this case, even an array literal would work.
  • Or (more typically when calling PowerShell commands):

    • As a hashtable to pass named parameter values, in which case you must replace the $ sigil in the variable reference with @; e.g., in your case @command; e.g., the following is the equivalent of calling Get-ChildItem C:\ -Directory:

    • $paramVals = @{ LiteralPath = 'C:\'; Directory = $true }; Get-ChildItem @paramVals


Caveat re array-based splatting:

Due to a bug detailed in GitHub issue #6280, PowerShell doesn't pass empty arguments through to external programs (applies to all Windows PowerShell versions / up to PowerShell (Core) 7.2.x. This has been fixed in 7.3, with selective exceptions on Windows, in conjunction with fixing how arguments with embedded " are passed - see the $PSNativeCommandArgumentPassing preference variable.

E.g., up to PowerShell v7.2.x, foo.exe "" unexpectedly results in just foo.exe being called.

This problem equally affects array-based splatting, so that
$cmdArgs = "", "other"; foo.exe $cmdArgs results in foo.exe other rather than the expected foo.exe "" other.

The workaround (which also applies in v7.3+ if $PSNativeCommandArgumentPassing = 'Legacy' is set) is to use '""' (sic).


Optional use of @ in array-based splatting with external programs:

As noted, with external programs use of @ in lieu of $ isn't necessary, because passing an array implicitly results in splatting. (By contrast, when calling a PowerShell command to which you want to pass the elements of an array as individual, positional arguments, you must use @)

However, you may choose to use @ with external programs too, and arguably it conveys the splatting intent more clearly:

junit-merge @command
Run Code Online (Sandbox Code Playgroud)

There is a subtle behavioral distinction, however - though it will probably rarely if ever surface in practice:

The safer choice is to use $, because it guards against (the however hypothetical) accidental misinterpretation of a array element containing --% that you intend to be passed on as-is.

Only the @ syntax recognizes an array element with verbatim value --% as the special stop-parsing token, --%

Said token tells PowerShell not to parse the remaining arguments as it normally would and instead pass them through as-is - unexpanded, except for expanding cmd.exe-style variable references such as %USERNAME%.

This is normally only useful when not using splatting, typically in the context of being able to use command lines that were written for cmd.exe from PowerShell as-is, without having to account for PowerShell's syntactical differences.

In the context of splatting, however, the behavior resulting from --% is non-obvious and best avoided:

  • As in direct argument passing, the --% is removed from the resulting command line.

  • Argument boundaries are lost, so that a single array element foo bar, which normally gets placed as "foo bar" on the command line, is placed as foo bar, i.e. effectively as 2 arguments.


[1] Your call implies the intent to pass the value of variable $command as a single argument, so when PowerShell builds the command line behind the scenes, it double-quotes the verbatim a.xml b.xml c.xml string contained in $command to ensure that. Note that these double quotes are unrelated to how you originally assigned a value to $command. Unfortunately, this automatic quoting is broken for values with embedded " chars. - see this answer, for instance.

[2] 作为对类 POSIX shell 的认可,PowerShell确实执行一种 shell 扩展,但 (a) 仅在类 Unix 平台(macOS、Linux)上,并且 (b) 仅在调用外部程序时:不带引号的通配符模式,例如*.txt当您调用外部程序(例如)时, as确实会扩展为其匹配的文件名/bin/echo *.txt,这是 PowerShell 称为本机 globbing的功能。