PowerShell从命令行参数中删除双引号

nw.*_*nw. 40 windows powershell command-line

最近,每当涉及双引号时,我在使用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"
Run Code Online (Sandbox Code Playgroud)

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

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

这里发生了什么?

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

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

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

下面是一些例子,与echo.exe从的GnuWin32:

PS C:\Documents and Settings\Nick> echo.exe "\`""
"
PS C:\Documents and Settings\Nick> echo.exe "\\\\\`""
\\"
PS C:\Documents and Settings\Nick> echo.exe "\\"
\\
Run Code Online (Sandbox Code Playgroud)

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

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

man*_*lds 32

这是一个众所周知的事情:

将参数传递给需要引用字符串的应用程序是很难的.我在IRC中用一个"忙碌的"PowerShell专家问过这个问题,并且花了一个小时让某人找到一种方法(我最初开始在这里发布它根本不可能).这完全打破了PowerShell作为通用shell的能力,因为我们不能做简单的事情,比如执行sqlcmd.命令shell的首要任务应该是运行命令行应用程序......例如,尝试使用SQL Server 2008中的SqlCmd,有一个-v参数,它接受一系列name:value参数.如果值中包含空格,则必须引用它...

...没有一种方法可以编写命令行来正确调用这个应用程序,所以即使你掌握了所有4种或5种不同的引用和转义方式之后,你仍然猜测哪种方法可行...或者,你可以向cmd外壳,并完成它.

  • 哇,令人震惊的是,一年半之后这个问题还没有解决. (9认同)
  • 它说'关闭为固定',但没有与修复或如何使用它的链接.这很糟糕. (8认同)
  • 链接已(有效)断开-“ Microsoft Connect已淘汰” (2认同)
  • 哦,我明白了。_所有试图解决此问题的人都已退休_并且不再能够解决此问题而不再从事技术行业 (2认同)

Mar*_* Ba 30

TL; 博士

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

ConvertTo-ArgvQuoteForPoSh.ps:Powershell V5(和 C# 代码)允许转义本机命令参数

我将尝试回答的问题

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

PS C:\Documents and Settings\Nick> echo.exe '"hello"'
hello 
PS C:\Documents and Settings\Nick> echo.exe '\"hello\"' 
"hello"
Run Code Online (Sandbox Code Playgroud)

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

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

这里发生了什么?

非 Powershell 背景

你需要用反斜杠转义引号的事实\没有对PowerShell的,但与CommandLineToArgvW所使用的所有的msvcrt和C#程序构建功能argv从单一字符串命令行的Windows进程被传递的数组。

详细信息在每个人都以错误的方式引用命令行参数中进行了解释,它基本上归结为这个函数历史上具有非常单调的转义规则的事实:

  • 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'"');
Run Code Online (Sandbox Code Playgroud)

Powershell 细节

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

这导致我们 -

Powershell 引用规则

什么辣妹不过做的是揣摩的字串是否š你把它作为参数传递给本机命令必须用引号括起来,因为它们含有空格。

PoSh -与此相反cmd.exe- 对你交给它的命令做了更多的解析,因为它必须解析变量并知道多个参数。

所以,给定一个命令

$firs  = 'whaddyaknow'
$secnd = 'it may have spaces'
$third = 'it may also have "quotes" and other \" weird \\ stuff'
EchoArgs.exe $firs $secnd $third
Run Code Online (Sandbox Code Playgroud)

Powershell 必须就如何为 Win32 (或者更确切地说是 C# )调用创建单个字符串 CommandLine采取立场,它最终必须这样做。CreateProcessProcess.Start

Powershell 采用的方法很奇怪,并且在 PoSh V7 中变得更加复杂,据我所知,它必须处理 powershell 如何处理未加引号的字符串中的不平衡引号。长话短说是这样的:

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

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;
    }
Run Code Online (Sandbox Code Playgroud)

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;
    }
Run Code Online (Sandbox Code Playgroud)

哦,是的,他们还添加了半成品的尝试,以正确转义 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('"');
Run Code Online (Sandbox Code Playgroud)

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>              |
------------------------------|-----------------------------|---------------------------
   ...
Run Code Online (Sandbox Code Playgroud)

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

Powershell 解决方案

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

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

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

我已将完整代码放在 Gist 上,因为这里包含的代码太长ConvertTo-ArgvQuoteForPoSh.ps::Powershell V5(和 C# 代码)允许转义本机命令参数

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

  • 感谢您的全面解释。这太可怕了。 (3认同)

Dro*_*roj 13

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

PS C:\Users\Droj> "string ""with`" quotes"
string "with" quotes
Run Code Online (Sandbox Code Playgroud)

单引号也是如此:

PS C:\Users\Droj> 'string ''with'' quotes'
string 'with' quotes
Run Code Online (Sandbox Code Playgroud)

向外部程序发送参数的奇怪之处在于,还有额外的引用评估级别.我不知道这是不是一个bug,但我猜它不会改变,因为当你使用Start-Process并传入参数时,行为是一样的.Start-Process为参数提供了一个数组,这使得实际上发送的参数数量更加清晰,但这些参数似乎需要额外的时间进行评估.

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

PS C:\cygwin\home\Droj> $aa = 'arg="foo"', 'arg=""""bar""""'
PS C:\cygwin\home\Droj> echo $aa
arg="foo"
arg=""""bar""""
Run Code Online (Sandbox Code Playgroud)

"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"
Run Code Online (Sandbox Code Playgroud)

可以预期这些参数将按原样传递给外部命令,因为它们是像'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"
Run Code Online (Sandbox Code Playgroud)

我不知道它的确切原因,但行为就好像在重新解析字符串的封面下有另一个未记录的步骤.例如,如果我将数组发送到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"
Run Code Online (Sandbox Code Playgroud)

...当我将这些参数发送到我的外部cygwin'echo.exe'时,这正是我得到的:

PS C:\cygwin\home\Droj> c:\cygwin\bin\echo 'arg="foo"' 'arg=""""bar""""'
arg=foo arg="bar"
Run Code Online (Sandbox Code Playgroud)


Abh*_*rma 5

使用 PowerShell 7.2.0,传递给本机可执行文件的参数终于可以按预期运行。目前这是一个实验性功能,需要手动启用。

Enable-ExperimentalFeature PSNativeCommandArgumentPassing
Run Code Online (Sandbox Code Playgroud)

之后编辑您的 PSProfile,例如使用记事本:

notepad.exe $PROFILE
Run Code Online (Sandbox Code Playgroud)

添加$PSNativeCommandArgumentPassing = 'Standard'到文件顶部。您也可以使用$PSNativeCommandArgumentPassing = 'Windows'它使用Legacy某些本机可执行文件的行为。这些差异记录在这个 pull request中。

最后,重新启动 PowerShell。命令参数将不再删除引号。


新的行为可以用这个小 C 程序来验证:

Enable-ExperimentalFeature PSNativeCommandArgumentPassing
Run Code Online (Sandbox Code Playgroud)

编译它并gcc传入一些带引号的参数,就像 JSON 字符串一样。

> gcc echo-test.c
> ./a.exe '{"foo": "bar"}'
Run Code Online (Sandbox Code Playgroud)

根据该Legacy行为,输出为{foo: bar}。但是,使用该Standard选项后,输出将变为{"foo": "bar"}.