变量引用和子表达式在参数模式中未加引号的标记:为什么它们有时会被拆分成多个参数?

21
注意: 此问题的概要已经发布在PowerShell GitHub库中(点击此处),但后来被更全面的问题所取代。
在PowerShell中,传递给命令的参数是以参数模式解析的(与表达式模式相对应,请参阅Get-Help about_Parsing)。
方便地,即使这些参数涉及变量引用(例如$HOME\sub)或子表达式(例如version=$($PsVersionTable.PsVersion)),双引号括起来不包含空格或元字符的参数通常是可选的

在大多数情况下,这样的未加引号的参数会被视为双引号字符串,并且适用于通常的string-interpolation rules(除了需要转义元字符如,)。

我尝试总结了this answer中参数模式下未加引号标记的解析规则,但存在奇怪的边缘情况

具体而言(截至Windows PowerShell v5.1),为什么以下每个命令中的未加引号的参数标记没有被识别为单个可展开的字符串,并导致传递了2个参数(变量引用/子表达式保留其类型)?

  • $(...) at the start of a token:

    Write-Output $(Get-Date)/today # -> 2 arguments: [datetime] obj. and string '/today'
    
    • Note that the following work as expected:

      • Write-Output $HOME/sub - simple var. reference at the start
      • Write-Output today/$(Get-Date) - subexpression not at the start
  • .$ at the start of a token:

    Write-Output .$HOME  # -> 2 arguments: string '.' and value of $HOME
    
    • Note that the following work as expected:

      • Write-Output /$HOME - different initial char. preceding $
      • Write-Output .-$HOME - initial . not directly followed by $
      • Write-Output a.$HOME - . is not the initial char.
作为附注:在 PowerShell Core v6.0.0-alpha.15 中,一个简单的变量引用后面跟着一个等号(=)似乎会将该标记分成两个参数,而在 Windows PowerShell v5.1 中不会发生这种情况。例如,Write-Output $HOME=dir。
注意:
• 我主要寻找所描述行为的设计原理或确认其是否为错误。如果这不是错误,则需要一些东西来帮助我概念化该行为,以便我能记住它并避免其缺陷。
• 所有这些边缘情况都可以通过显式双引号避免,鉴于上述非明显的行为,这可能是通常使用的最安全选择。

可选阅读:文档状态和设计思考

截至本文撰写时,v5.1 Get-Help about_Parsing 页面

  • 不完全描述了规则

  • 使用了一些术语,这些术语既没有在该主题中定义,也没有在 PowerShell 的世界中普遍使用(“可扩展字符串”,“值表达式” - 尽管人们可以猜测它们的含义)

从链接的页面中可以看到(重点加粗):

在参数模式下,每个值都被视为可扩展的字符串,除非它以以下特殊字符之一开头:美元符号($)、at 符号(@)、单引号(')、双引号(")或括号(()。如果前面带有这些字符之一,则该值将被视为值表达式。顺便提一下,以引号(")开头的标记当然也是可扩展的字符串(插值字符串)。关于引用的概念性帮助主题Get-Help about_Quoting_Rules巧妙地避免了“扩展”和“插值”这两个术语。
请注意,该段落没有说明当(非元字符)字符直接跟随以这些特殊字符开头的令牌时会发生什么,特别是$
然而,该页面包含一个示例表明以变量引用开头的令牌也被解释为可扩展字符串
  • 使用包含4$aWrite-Output $a/H计算为(单个字符串参数)4/H
请注意,该段落暗示未引用的令牌内部的变量引用/子表达式(不以特殊字符开头)将被扩展,就像在双引号字符串内一样(“被视为可扩展字符串”)。
如果这些有效:
$a = 4
Write-Output $a/H         # -> '4/H'
Write-Output H/$a         # -> 'H/4'
Write-Output H/$(2 + 2)   # -> 'H/4'

为什么Write-Output $(2 + 2)/H不应该扩展为'4/H'(而被视为2个参数)?为什么在开头的子表达式与变量引用不同对待?
这些微妙的区别很难记住,特别是在没有理由的情况下。
一个更有意义的规则是无条件地将以$开始并在变量引用/子表达式之后具有其他字符的标记也视为可扩展字符串。(相比之下,保持当前单独的变量引用/子表达式的类型是有意义的。)
请注意,以.$开头的令牌被拆分为2个参数的情况在帮助主题中没有涉及。

更多可选阅读:在以其他特殊字符之一开头的令牌后跟随额外字符。

在其他特殊令牌起始字符中,以下内容将无条件地将后面的所有字符视为一个独立的参数(这是有意义的):
( ' "

Write-Output (2 + 2)/H   # -> 2 arguments: 4 and '/H'
Write-Output "2 + $a"/H  # -> 2 arguments: '2 + 4' and '/H', assuming $a equals 4
Write-Output '2 + 2'/H   # -> 2 arguments: '2 + 2' and '/H'

作为旁注:这表明,在PowerShell中不通常支持类似于bash的字符串连接——将任何混合引号和未引用标记放在一起——它仅在第一个子字符串/变量引用恰好是未引用的情况下才起作用。例如,Write-Output H/'2 + 2'(与上面反转子字符串的示例不同)只生成一个参数。
但是,有一个例外:@:当@后面跟着一个语法上有效的变量名(例如@parms)时,@确实具有特殊含义(请参阅Get-Help about_Splatting),但其他任何内容都会导致该标记被视为可扩展的字符串:
Write-Output @parms    # splatting (results in no arguments if $parms is undefined)

Write-Output @parms$a  # *expandable string*: '@parms4', if $a equals 4

3
我一直觉得将$var/string的实际解析为一个可扩展字符串与文档所说的相反,因为表达式以$开头。$(date)/sub作为两个不同的参数(一个值表达式和一个裸字字符串)是完全有道理的。 - Mathias R. Jessen
1
@briantist:感谢你提供的Hello'Hi'sup"whatup"示例 - 我不知道它能用,但事实证明,只有在第一个子字符串是_unquoted_(尝试Write-Host -Object 'Hello'sup"whatup")时才有效,这正是引发这个问题的那种晦涩行为。 - mklement0
1
@mklement0 我理解你的感受。在没有通过调试器逐步执行代码的情况下,跟踪 GitHub 上的代码可能会令人发疯。很高兴看到开发团队中有人关注这个问题! - briantist
3
我注意到了同样的问题,并养成了一种习惯:如果我想确保输出的所有内容都是一个完整的字符串,我会用双引号将其全部包裹起来。 Write-Output "$(Get-Date)/today" - Parrish
1
请注意可疑的差异:$x=Get-Date; Write-Output $x/today 与相似的 Write-Output $(Get-Date)/today - JosefZ
显示剩余8条评论
1个回答

1
我认为你所说的更多是类型“提示”,而不是其他任何东西。
您正在使用Write-Output,其简介中指定了以下内容:
将指定的对象发送到管道中的下一个命令。
此命令旨在接受一个数组。当它遇到第一个像today/这样的字符串时,它会将其视为字符串处理。当第一个项最终成为函数调用的结果时,它可能是字符串,也可能不是,因此它启动了一个数组。
如果您运行相同的命令以Write-Host(旨在接受要输出的字符串),则它会按您所期望的方式工作。
 Write-Host $(Get-Date)/today

输出

2018年7月25日下午1:30:43 /今天

所以我认为你遇到的边缘情况与解析无关,更多地与PowerShell使用的类型和试图隐藏的类型有关。


谢谢,但是Write-Host也会将$(Get-Date)/today解析为2个不同的参数 - 只是不太明显,因为它使用一个_空格_来分隔输出中的元素,而Write-Output则在每行输出每个元素。换句话说:解析是一致的,在任何情况下我都不希望将令牌视为_2_个参数,因为简单地_交换_所涉及的令牌被视为_1_个参数:Write-Output today/$(Get-Date) - mklement0
顺便提一下:从 $(Get-Date)/today 得到的 2 个参数并不是作为数组 传递 的;它们作为单独的位置参数传递,只有因为 Write-OutputWrite-Host 使用了特殊的 ValueFromRemainingArguments 参数属性标志来声明其 -InputObject / -Object 参数,这会导致单独传递的参数被 隐式收集 到一个数组中。由于这个标志,实际上 Write-Output 1, 2 - 传递一个 数组 - 等同于 Write-Output 1 2 - 传递单独的参数。 - mklement0

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