PowerShell 7.3.0 破坏了命令调用。

12

我在PowerShell脚本中使用WinSCP,但突然间它停止了工作。经过一段时间的排查,我发现问题出现在更近版本的PowerShell中:

简化的代码:

& winscp `
    /log `
    /command `
        'echo Connecting...' `
        "open sftp://kjhgk:jkgh@example.com/ -hostkey=`"`"ssh-ed25519 includes spaces`"`"" 

v7.2.7的错误信息

主机“example.com”不存在。

v7.3.0的错误信息

命令“open”的参数过多。

正如您所看到的,使用v7.3.0时,WinSCP接收到不同的输入取决于PS的版本。我发现这种差异与主机密钥中的空格有关。如果省略它们,v7.3.0会输出相同的错误。

是什么导致了PowerShell的这种变化,我该如何解决? (我该如何调试此类问题?我尝试了一些转义操作,但无论版本如何,字符串看起来都是相同的,没有明显的破坏性变化可能负责此问题)


1
针对外部程序参数传递的故障排除:如果您使用 Chocolatey,您可以在提升的会话中使用 choco install echoargs -y 安装 echoargs.exe,该工具显示了 PowerShell 在幕后构建的原始命令行以及(大多数)外部程序如何将其解析为参数。或者,您可以即席编译一个实用程序:请参见此答案。在 PowerShell 7.3+ 中,只有在您首先显式(临时)设置 $PSNativeCommandArgumentPassing = 'Legacy' 时才会看到破损的旧行为。 - mklement0
1个回答

22

PowerShell (Core)的7.3.0版本引入了一个与外部程序(如winscp)有关的重大变更,涉及到如何传递带有嵌入的"字符(以及空字符串参数)[1][2]

虽然这个变更在很大程度上是有益的,因为它修复了自v1以来一直存在的基本错误行为(本答案讨论了旧的错误行为),但它也不可避免地破坏了建立在错误行为基础上的现有解决方法除了对于调用批处理文件和WSH CLIs(wscript.execscript.exe)以及它们关联的脚本文件(文件名扩展名为.vbs.js)的解决方法。

为了使现有的解决方法继续工作,请将$PSNativeCommandArgumentPassing preference variable(暂时)设置为'Legacy'
# Note: Enclosing the call in & { ... } makes it execute in a *child scope*
#       limiting the change to $PSNativeCommandArgumentPassing to that scope.
& {
  $PSNativeCommandArgumentPassing = 'Legacy'
  & winscp `
    /log `
    /command `
        'echo Connecting...' `
        "open sftp://kjhgk:jkgh@example.com/ -hostkey=`"`"ssh-ed25519 includes spaces`"`"" 
}

很不幸的是,因为只接受这种形式的进程命令行(即嵌入的<">被转义为<"">),而不接受更常用的形式(嵌入的<">被转义为<\">),这是现在修复后的行为,对于,特别是,仍然需要一个解决方法。
如果你不想依赖于修改< $PSNativeCommandArgumentPassing>来解决问题,这里有一些在v7.2-和v7.3+中都有效的解决方法:
使用--%停止解析标记,但是这种方法有缺陷和严格限制,特别是不能在其后的参数中(直接)使用PowerShell 变量或子表达式 - 有关详细信息,请参阅此答案;但是,如果您将--%作为您构建并分配给变量的数组的一部分,然后通过splatting传递,您可以绕过这些限制:
# 注意:必须是单行;注意--%和后面参数中未转义的""的使用。
# --% 后面只能使用 "..." 引用
# 只能使用 cmd 样式的 *环境变量*,如 %OS%。
winscp /log /command 'echo Connecting...' --% "open sftp://kjhgk:jkgh@example.com/ -hostkey=""ssh-ed25519 includes spaces""" 

# 更好的替代方案,使用 splatting:
$argList = '/log', '/command', 'echo Connecting...', 
           '--%', "open sftp://kjhgk:jkgh@example.com/ -hostkey=""ssh-ed25519 includes spaces"""
winscp @argList

或者,通过cmd /c调用
# 注意:传递的命令必须是单行,
# 只支持 "..." 引用,
# 嵌入的命令必须遵守 cmd.exe 的语法规则。
cmd /c @"
  winscp /log /command "echo Connecting..." "open sftp://kjhgk:jkgh@example.com/ -hostkey=""ssh-ed25519 includes spaces"""
"@

注意:您不一定需要使用here-string@"<newline>...<newline>"@@'<newline>...<newline>'@),但它有助于提高可读性并简化嵌入引用的使用。
无论是哪种解决方法,都允许您直接传递参数,但不幸的是,这也要求您将整个(传递)命令制定为一行 - 除非将--%与扩展操作符结合使用。

背景信息:

在Windows上,v7.3+的默认$PSNativeCommandArgumentPassing值为'Windows'

遗憾的是,对于调用批处理文件和WSH CLIs(wscript.exe和cscript.exe)以及它们关联的脚本文件(文件名扩展名为.vbs和.js等),PowerShell仍然保留了旧的、有问题的行为。
对于这些程序而言,这样做可以使现有的解决方法继续正常工作,但是只需要在v7.3+中运行的未来代码仍然需要依赖这些晦涩的解决方法,这些解决方法是基于有问题的行为构建的。
另一种选择是将这些程序以及一些与程序无关的适应措施纳入PowerShell中,这样在绝大多数情况下,未来将不再需要解决方法。但是这种选择并没有被实施,具体信息请参见GitHub问题#15143。
此外,还有一些令人头疼的迹象表明,这个例外列表还将逐步扩充,这几乎可以保证在给定的PowerShell版本中,对于哪些程序需要解决方法以及哪些不需要会产生混淆。
值得赞扬的是,对于所有其他程序,PowerShell在必要时重新构建命令行时对参数进行了编码,具体规则如下:
对于遵循C++命令行解析规则(如C/C++/.NET应用程序所使用的规则)/CommandLineToArgv WinAPI函数解析规则的程序,PowerShell对参数进行编码,这是解析进程命令行的最常见约定。
简而言之,这意味着将嵌入在参数中的“字符”作为目标程序的“原样部分”进行转义,转义为\",如果\本身需要转义(作为\\)只有在它之前是一个“字符”,但是要被原样解释。
请注意,如果将$PSNativeCommandArgumentPassing值设置为“Standard”(这是类Unix平台上的默认值,在这种模式下,解决了所有问题,使v7.3+代码不再需要解决方法),则此行为适用于所有外部程序,即上述例外不再适用。

关于 v7.3 变更的影响摘要,请参阅 GitHub 上的此评论

如果您需要编写跨版本、跨平台的 PowerShell 代码:Native 模块(由我编写,使用 Install-Module Native 安装),提供了一个名为 ie 的函数(即 Invoke Executable 的缩写),它是一个填充函数,可以在绝大多数情况下实现无需解决问题的跨版本(v3+)、跨平台和跨版本行为 - 只需在外部程序调用前加上 ie
注意:在当前特定情况下,它将无法工作,因为它不知道 winscp.exe 需要进行 "" 转义。


[1] 有关详细信息和解决方法,请参阅this answer
[2] 在后续版本中撤销该更改并将新行为设置为“选择加入”曾经被考虑过,但最终决定不这样做 - 请参阅GitHub issue #18694

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接