【问题标题】:PowerShell stripping double quotes from command line argumentsPowerShell 从命令行参数中去除双引号
【发布时间】:2011-10-06 13:10:49
【问题描述】:

最近,每当涉及双引号时,我在 PowerShell 中使用 GnuWin32 时遇到了一些问题。

经过进一步调查,PowerShell 似乎正在从命令行参数中删除双引号,即使正确转义也是如此。

PS C:\Documents and Settings\Nick> echo '"hello"'
"hello"
PS C:\Documents and Settings\Nick> echo.exe '"hello"'
hello
PS C:\Documents and Settings\Nick> echo.exe '\"hello\"'
"hello"

请注意,双引号在传递给 PowerShell 的 echo cmdlet 时存在,但当作为参数传递给 echo.exe 时,双引号会被去除,除非使用反斜杠(即使 PowerShell 的转义字符是反斜杠,而不是反斜杠)。

这对我来说似乎是一个错误。如果我将正确的转义字符串传递给 PowerShell,那么 PowerShell 应该处理可能需要的任何转义,但它会调用命令。

这是怎么回事?

目前,修复方法是根据这些规则转义命令行参数(这似乎被 PowerShell 用来调用 .exe 文件的 CreateProcess API 调用(间接)使用):

  • 要传递双引号,请使用反斜杠转义:\" -> "
  • 要传递一个或多个反斜杠后跟双引号,请使用另一个反斜杠转义每个反斜杠并转义引号:\\\\\" -> \\"
  • 如果后面没有双引号,则反斜杠不需要转义:\\ -> \\

请注意,为了将 Windows API 转义字符串中的双引号转义到 PowerShell,可能需要进一步转义双引号。

这里有一些例子,来自 GnuWin32 的 echo.exe

PS C:\Documents and Settings\Nick> echo.exe "\`""
"
PS C:\Documents and Settings\Nick> echo.exe "\\\\\`""
\\"
PS C:\Documents and Settings\Nick> echo.exe "\\"
\\

我想如果你需要传递一个复杂的命令行参数,这很快就会变成地狱。当然,这些都没有记录在 CreateProcess() 或 PowerShell 文档中。

另请注意,将带双引号的参数传递给 .NET 函数或 PowerShell cmdlet 不是必需的。为此,您只需将双引号转义到 PowerShell。

编辑:正如 Martin 在他出色的回答中指出的那样,这在 CommandLineToArgv() 函数(CRT 用于解析命令行参数)文档中有记录。

【问题讨论】:

  • 你用的是什么版本的powershell? v1 的引号要糟糕得多。 v2 好多了,但仍然受到影响。 v2 随 win7+2008r2 提供,但也可安装在 2003/xp 上。
  • 我认为这是一个错误,但 PS 并没有去掉引号。正如您在上一个示例中发现的那样,PS 在执行应用程序时根本无法转义引号。
  • 我发现我的大部分问题都来自于过于努力。大多数时候 PowerShell 引用按预期工作。一个方便的工具是showargs.exe,它可以显示 PowerShell 实际运行的命令行。
  • 请注意echoWrite-Output 的别名。
  • 8 年后的今天,我目前的解决方法是当我必须将双引号传递给某些程序时不使用 powershell。

标签: windows command-line powershell


【解决方案1】:

在撰写本文时,这似乎已在 PowerShell 的最新版本中得到修复,因此不再需要担心。

如果您仍然认为您看到此问题,请记住它可能与其他问题有关,例如调用 PowerShell 的程序,因此如果您在直接从命令提示符或 ISE 调用 PowerShell 时无法重现它,您应该在别处调试。

例如,我在调查使用 Process.Start 从 C# 代码运行 PowerShell 脚本时引号消失的问题时发现了这个问题。问题实际上是C# Process Start needs Arguments with double quotes - they disappear

【讨论】:

  • 我知道我的回答并不是特别有用。我写它是因为我花了几个小时寻找 PowerShell 处理引号的方式,而实际上它在其他地方,而其他人可能处于相同的情况。我希望我的回答已经或将帮助人们,让他们在别处寻找问题并通过运行不同的命令等来调试他们的脚本。
  • 我认为这个答案实际上是不正确的,因为没有证据表明这个问题已经得到解决。在最新的 powershell 中尝试 OP 中的所有内容,您将获得完全相同的结果。
  • pwsh 6.1.2 并且引号仍然消失 调用EchoArgs.exe "quote" '""twoquotes""' """""4quotes"""""时不显示引号
  • 我不确定“没有有限的完整列表”与此有什么关系。我再重复一遍:尝试 OP 中的所有内容,这是非常有限且不是很长的列表。
【解决方案2】:

哦,亲爱的。显然试图转义双引号以将它们从命令行导入 PowerShell,或者更糟糕的是,您使用其他一些语言来生成这样的命令行,或者可能链接 PowerShell 脚本的执行环境,可以是浪费时间。

作为一种实际解决方案的尝试,我们能做些什么呢?看起来很傻的变通办法有时也很有效:

powershell Write-Host "'say ___hi___'.Replace('___', [String][Char]34)"

但这在很大程度上取决于执行方式。请注意,如果您希望该命令在粘贴到 PowerShell 中而不是从命令提示符运行时具有相同的结果,则需要那些外部双引号!因为托管 Powershell 将表达式转换为一个字符串对象,它只是 'powershell.exe' 的一个参数

PS> powershell Write-Host 'say ___hi___'.Replace('___', [String][Char]34)

然后,我猜,它会将其参数解析为 Write-Host say "hi"

所以你努力用 string.Replace() 重新引入的引号将消失!

【讨论】:

【解决方案3】:

TL;DR

如果您只想要 Powershell 5 的解决方案,请参阅:

ConvertTo-ArgvQuoteForPoSh.ps: Powershell V5 (and C# Code) to allow escaping native command arguments

我将尝试回答的问题

...,看来 PowerShell 正在从命令中删除双引号 行参数,即使正确转义。

PS C:\Documents and Settings\Nick> echo.exe '"hello"'
hello 
PS C:\Documents and Settings\Nick> echo.exe '\"hello\"' 
"hello"

请注意,双引号在传递给 PowerShell 时 echo cmdlet,但是当作为参数传递给 echo.exe 时,double 除非用反斜杠转义,否则引号将被删除(即使 PowerShell 的转义字符是反引号,而不是反斜杠)。

这对我来说似乎是一个错误。如果我通过正确的转义 字符串到 PowerShell,然后 PowerShell 应该处理任何事情 转义可能是必要的,因为它会调用命令。

这是怎么回事?

非 Powershell 背景

您需要使用反斜杠 \ 转义引号这一事实与 powershell 相比 没有,但使用所有 msvcrt 和 C# 程序用来构建argv 来自 Windows 进程传递的单字符串命令行的数组。

详细信息在Everyone quotes command line arguments the wrong way 进行了解释,它基本上归结为这个函数在历史上具有非常不直观的转义规则:

  • 2n 个反斜杠后跟一个引号产生 n 个反斜杠,后跟开始/结束引号。这不会成为解析的一部分 参数,但切换“引号”模式。
  • (2n) + 1 个反斜杠后跟一个引号再次产生 n 个反斜杠后跟一个引号文字 (")。这不会 切换“引号”模式。
  • n 个不带引号的反斜杠只会产生 n 个反斜杠。

导致所描述的通用转义函数(这里的逻辑短引号):

CommandLine.push_back (L'"');

for (auto It = Argument.begin () ; ; ++It) {
      unsigned NumberBackslashes = 0;

      while (It != Argument.end () && *It == L'\\') {
              ++It;
              ++NumberBackslashes;
      }

      if (It == Argument.end ()) {
              // Escape all backslashes, but let the terminating
              // double quotation mark we add below be interpreted
              // as a metacharacter.
              CommandLine.append (NumberBackslashes * 2, L'\\');
              break;
      } else if (*It == L'"') {
              // Escape all backslashes and the following
              // double quotation mark.
              CommandLine.append (NumberBackslashes * 2 + 1, L'\\');
              CommandLine.push_back (*It);
      } else {
              // Backslashes aren't special here.
              CommandLine.append (NumberBackslashes, L'\\');
              CommandLine.push_back (*It);
      }
}

CommandLine.push_back (L'"');

Powershell 细节

现在,直到 Powershell 5(包括 Win10/1909 上的 PoSh 5.1.18362.145)PoSh 基本上对这些规则一无所知,也不应该争论,因为这些规则并不是真正通用的,因为任何理论上,您调用的可执行文件可以使用其他方式来解释传递的命令行。

这导致我们 -

Powershell 引用规则

然而,PoSh 所做 所做的是尝试确定您将其作为参数传递给本机命令的字符串s 是否需要被引用,因为它们包含空格。

PoSh - in contrast to cmd.exe - 对您提交的命令进行更多解析,因为它必须解析变量并了解多个参数。

所以,给定一个类似的命令

$firs  = 'whaddyaknow'
$secnd = 'it may have spaces'
$third = 'it may also have "quotes" and other \" weird \\ stuff'
EchoArgs.exe $firs $secnd $third

Powershell 必须就如何为 Win32 CreateProcess(或者更确切地说 C# Process.Start)调用创建 single 字符串命令行采取立场,它最终将不得不这样做。

Powershell 采用的方法是 weird 并得到了 more complicated in PoSh V7 ,据我所知,它必须做 powershell 如何处理不带引号的字符串中的不平衡引号。长话短说是这样的:

Powershell 将自动引用(包含在 "> 中)单个参数 字符串,如果它包含空格 and 空格不与 奇数个(未转义的)双引号。

PoSh V5 的特定引用规则使得不可能将某个类别的字符串作为单个参数传递给子进程。

PoSh V7 修复了这个问题,因此只要所有引号都被\" 转义——无论如何它们都需要通过CommandLineToArgvW——我们可以将任何来自 PoSh 的任意字符串传递给一个子可执行文件使用CommandLineToArgvW

以下是从 PoSh github 存储库中提取的 C# 代码规则,用于我们的工具类:

PoSh 引用规则 V5

    public static bool NeedQuotesPoshV5(string arg)
    {
        // bool needQuotes = false;
        int quoteCount = 0;
        for (int i = 0; i < arg.Length; i++)
        {
            if (arg[i] == '"')
            {
                quoteCount += 1;
            }
            else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0))
            {
                // needQuotes = true;
                return true;
            }
        }
        return false;
    }

PoSh 引用规则 V7

    internal static bool NeedQuotesPoshV7(string arg)
    {
        bool followingBackslash = false;
        // bool needQuotes = false;
        int quoteCount = 0;
        for (int i = 0; i < arg.Length; i++)
        {
            if (arg[i] == '"' && !followingBackslash)
            {
                quoteCount += 1;
            }
            else if (char.IsWhiteSpace(arg[i]) && (quoteCount % 2 == 0))
            {
                // needQuotes = true;
                return true;
            }

            followingBackslash = arg[i] == '\\';
        }
        // return needQuotes;
        return false;
    }

哦,是的,they also added in 在 V7 中正确转义和引用字符串的半生不熟的尝试:

if (NeedQuotes(arg))
{
      _arguments.Append('"');
      // need to escape all trailing backslashes so the native command receives it correctly
      // according to http://www.daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULESDOC
      _arguments.Append(arg);
      for (int i = arg.Length - 1; i >= 0 && arg[i] == '\\'; i--)
      {
              _arguments.Append('\\');
      }

      _arguments.Append('"');

Powershell 情况

Input to EchoArgs             | Output V5 (powershell.exe)  | Output V7 (pwsh.exe)
===================================================================================
EchoArgs.exe 'abc def'        | Arg 0 is <abc def>          | Arg 0 is <abc def>
------------------------------|-----------------------------|---------------------------
EchoArgs.exe '\"nospace\"'    | Arg 0 is <"nospace">        | Arg 0 is <"nospace">
------------------------------|-----------------------------|---------------------------
EchoArgs.exe '"\"nospace\""'  | Arg 0 is <"nospace">        | Arg 0 is <"nospace">
------------------------------|-----------------------------|---------------------------
EchoArgs.exe 'a\"bc def'      | Arg 0 is <a"bc>             | Arg 0 is <a"bc def>
                              | Arg 1 is <def>              |
------------------------------|-----------------------------|---------------------------
   ...

由于时间原因,我在这里截取更多示例。无论如何,他们不应该在答案中添加太多内容。

Powershell 解决方案

要使用 CommandLineToArgvW 将任意字符串从 Powershell 传递到本机命令,我们必须:

  • 正确转义源参数中的所有引号和反斜杠
    • 这意味着识别 V7 对反斜杠的特殊字符串结束处理。 (这部分在下面的代码中没有实现。)
  • 确定powershell是否会自动引用我们的转义字符串,如果它不会自动引用它,请自己引用它。
    • 并且确保我们自己引用的字符串不会被 powershell 自动引用:这是破坏 V5 的原因。

Powershell V5 源代码,用于正确地将所有参数转义到任何本机命令

I've put the full code on Gist,因为太长了,无法在此处包含:ConvertTo-ArgvQuoteForPoSh.ps: Powershell V5 (and C# Code) to allow escaping native command arguments

  • 请注意,此代码尽力而为,但对于有效负载和 V5 中带有引号的某些字符串,您只需在传递的参数中添加前导空格即可。 (有关逻辑详细信息,请参见代码)。

【讨论】:

  • 感谢您的全面解释。这太可怕了。
  • 那么,换句话说,当用gcc 编译一个c 源代码并试图用-D 定义一个带引号的宏时,这是不可能的?
  • @RenéNyffenegger - 你到底为什么要从 PoSh 调用 gcc? ;-) 坦率地说,我不知道......我想试试看?
  • @MartinBa 因为我想将 C 源代码编译成可执行文件或 DLL。我非常努力地定义了一个带引号的宏,但没有成功,这就是我最终偶然发现这个问题的原因。
【解决方案4】:

依靠 CMD 来解决the accepted answer 中指出的问题对我来说不起作用,因为在调用 CMD 可执行文件时双引号仍然被去掉。

对我来说,一个好的解决方案是将我的命令行构造为一个字符串数组,而不是一个包含所有参数的完整字符串。然后只需将该数组作为二进制调用的参数传递:

$args = New-Object System.Collections.ArrayList
$args.Add("-U") | Out-Null
$args.Add($cred.UserName) | Out-Null
$args.Add("-P") | Out-Null
$args.Add("""$($cred.Password)""")
$args.Add("-i") | Out-Null
$args.Add("""$SqlScriptPath""") | Out-Null
& SQLCMD $args

在这种情况下,参数周围的双引号会正确传递给调用的命令。

如果需要,您可以使用来自PowerShell Community Extensions 的 EchoArgs 对其进行测试和调试。

【讨论】:

  • 这不完全是 PowerShell 的错。 sqlcmd 也有一些非常奇怪的行为。在命令提示符中试试这个命令:sqlcmd Q "SELECT '$(x)'" -v x= """hello world"""。我在里面得到""hello world"",而我应该得到"hello world"。这就像 sqlcmd 也以某种方式绕过了正常的转义规则。也就是说,您绝对可以通过传递一个普通的 PowerShell 字符串来传递包含空格的参数:sqlcmd -Q "SELECT '`$(x)'" -v x= 'hello world' 给了我hello world
【解决方案5】:

我个人避免在 PowerShell 中使用 '\' 来转义内容,因为它在技术上不是 shell 转义字符。我已经得到了不可预知的结果。在双引号字符串中,您可以使用 "" 来获取嵌入的双引号,或者使用反引号将其转义:

PS C:\Users\Droj> "string ""with`" quotes"
string "with" quotes

单引号也是如此:

PS C:\Users\Droj> 'string ''with'' quotes'
string 'with' quotes

将参数发送到外部程序的奇怪之处在于,还有额外的报价评估级别。我不知道这是否是一个错误,但我猜它不会改变,因为当您使用 Start-Process 并传入参数时行为是相同的。 Start-Process 使用一个数组作为参数,这使得事情变得更清晰,就实际发送了多少参数而言,但这些参数似乎需要额外的时间来评估。

所以,如果我有一个数组,我可以将参数值设置为嵌入引号:

PS C:\cygwin\home\Droj> $aa = 'arg="foo"', 'arg=""""bar""""'
PS C:\cygwin\home\Droj> echo $aa
arg="foo"
arg=""""bar""""

'bar' 参数足以覆盖额外的隐藏评估。就好像我用双引号将该值发送到 cmdlet,然后用双引号再次发送该结果:

PS C:\cygwin\home\Droj> echo "arg=""""bar""""" # level one
arg=""bar""
PS C:\cygwin\home\Droj> echo "arg=""bar""" # hidden level
arg="bar"

人们会期望这些参数按原样传递给外部命令,就像传递给 'echo'/'write-output' 之类的 cmdlet 一样,但由于隐藏级别,它们不是:

PS C:\cygwin\home\Droj> $aa = 'arg="foo"', 'arg=""""bar""""'
PS C:\cygwin\home\Droj> start c:\cygwin\bin\echo $aa -nonew -wait
arg=foo arg="bar"

我不知道它的确切原因,但这种行为就像是在重新解析字符串的幕后采取了另一个未记录的步骤。例如,如果我将数组发送到 cmdlet,我会得到相同的结果,但通过 invoke-expression 添加解析级别:

PS C:\cygwin\home\Droj> $aa = 'arg="foo"', 'arg=""""bar""""'
PS C:\cygwin\home\Droj> iex "echo $aa"
arg=foo
arg="bar"

...这正是我将这些参数发送到外部 Cygwin 实例的“echo.exe”时得到的结果:

PS C:\cygwin\home\Droj> c:\cygwin\bin\echo 'arg="foo"' 'arg=""""bar""""'
arg=foo arg="bar"

【讨论】:

  • 这种方法很好,因为它还允许您在字符串中展开 PowerShell 变量,例如:"name=`"$value`"" 将导致 name=""
【解决方案6】:

这是一个known thing

将参数传递给需要带引号的字符串的应用程序太难了。我在 IRC 中与“满屋”的 PowerShell 专家一起问了这个问题,有人花了一个小时才想出办法(我最初开始在这里发帖说这根本不可能)。这完全破坏了 PowerShell 作为通用 shell 的能力,因为我们不能做简单的事情,比如执行 sqlcmd。命令外壳的首要任务应该是运行命令行应用程序...例如,尝试使用 SQL Server 2008 中的 SqlCmd,有一个 -v 参数,它采用一系列名称:值参数。如果值中有空格,则必须引用它...

...没有一种方法可以编写命令行来正确调用此应用程序,因此即使您掌握了所有 4 或 5 种不同的引用和转义方法,您仍然在猜测哪种方法会在什么时候起作用... 或者,您可以直接使用 cmd,然后完成它。

【讨论】:

  • 哇,这个问题在一年半后仍未修复,令人震惊。
  • 它说“已修复”,但没有指向修复或如何使用它的链接。这很糟糕。
  • 链接(实际上)已损坏 - “Microsoft Connect 已停用”
  • 今天我遇到了另一个关于“的坏事:使用 CMD /C 命令时,需要在命令中的文件名中添加“,因为它们包含空格,在 Win7 中,你需要在命令前后额外添加” ,在Win10下是报错,只能通过查找系统版本来解决:# system version $ver = [Environment]::OSVersion.Version.ToString(); $ver = $ver.Substring(0,2); # 在windows 7中添加" if ($ver -Match "10") {$cc = $c} Else {$cc = '"' + $c + '"'} cmd /c $cc
  • 哦,我明白了。 所有试图解决这个问题的人都退休了,并且在未能解决这个问题的情况下不再在技术行业工作
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-30
  • 2011-09-03
  • 2018-10-08
  • 2020-03-19
  • 2012-01-21
  • 1970-01-01
  • 2023-03-19
相关资源
最近更新 更多