繁体   English   中英

PowerShell 从命令行中去除双引号 arguments

[英]PowerShell stripping double quotes from command line arguments

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

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

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 应该处理 escaping 可能需要的任何东西,但它会调用命令。

这里发生了什么?

目前,修复是根据这些规则转义命令行 arguments (这似乎由CreateProcess API 调用(PowerShell 用于调用.exe 文件)使用(间接):

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

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

下面是一些例子,来自 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 文档中。

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

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

这是一个已知的事情

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

...没有一种方法可以编写命令行来正确调用此应用程序,因此即使您掌握了所有 4 或 5 种不同的引用方式和 escaping 的东西,您仍然在猜测什么时候会起作用......或者,您可以将 shell 输出到 cmd,然后完成。

TL;博士

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

ConvertTo-ArgvQuoteForPoSh.ps : Powershell V5(和 C# 代码)以允许 escaping 本机命令 ZDBC11CAABD8BDA2777776

我将尝试回答的问题

...,似乎 PowerShell 正在从命令行 arguments 中删除双引号,即使正确转义也是如此。

 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 应该处理 escaping 可能需要的任何东西,但它会调用命令。

这里发生了什么?

非 Powershell 背景

The fact that you need to escape the quotes with backslashes \ has nothing to to with powershell, but with the CommandLineToArgvW function that is used by all msvcrt and C# programs to build the argv array from the single-string command line that the Windows process gets通过了。

详细信息在每个人都以错误的方式引用命令行 arguments 进行了解释,它基本上归结为这个 function 历史上具有非常缺乏营养的 escaping 规则:

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

导致描述的通用 escaping function (这里的逻辑短引用):

 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所做的是尝试弄清楚您将其作为 arguments 传递给本机命令的字符串s是否需要被引用,因为它们包含空格。

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

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

$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 )调用创建单个字符串 CommandLine 采取立场,它最终必须做。

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

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

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

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

这是从 PoSh github repo 中提取的 C# 代码的规则,用于我们的工具 class:

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;
    }

哦,是的, 他们还添加了半生不熟的尝试,以正确转义 V7 中引用字符串的 and :

 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 源代码正确 escaping 所有 arguments 到任何本机命令

I've put the full code on Gist , as it got too long to include here: ConvertTo-ArgvQuoteForPoSh.ps : Powershell V5 (and C# Code) to allow escaping native command arguments

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

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

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

单引号也是如此:

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

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

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

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"

人们会期望这些 arguments 按原样传递给外部命令,就像它们传递给 '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"

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

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

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

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

例如,我在调查使用Process.Start从 C# 代码运行 PowerShell 脚本时引号消失的问题时发现了这个问题。 问题实际上是C# Process Start 需要 Arguments 带双引号 - 它们消失了

依靠 CMD 到 shell 解决已接受答案中指示的问题对我不起作用,因为在调用 CMD 可执行文件时,双引号仍然被删除。

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

$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

在这种情况下,围绕 arguments 的双引号将正确传递给调用的命令。

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

哦亲爱的。 显然,试图转义双引号以从命令行将它们放入 PowerShell,或者更糟糕的是,您用于生成此类命令行的其他语言,或可能链接 PowerShell 脚本的执行环境,可能会浪费大量时间。

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

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

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

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

然后,我猜,将其 arguments 解析为 Write-Host 说“嗨”

因此,您努力使用 string.Replace() 重新引入的引号将消失!

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

Enable-ExperimentalFeature PSNativeCommandArgumentPassing

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

notepad.exe $PROFILE

$PSNativeCommandArgumentPassing = 'Standard'添加到文件顶部。 您也可以改为使用$PSNativeCommandArgumentPassing = 'Windows' ,它对某些本机可执行文件使用Legacy行为。 差异记录在此拉取请求中。

最后重启PowerShell。 命令 arguments 将不再删除引号。


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

#include <stdio.h>

int main(int argc, char** argv) {
    for (int i = 1; i < argc; i++) {
        puts(argv[i]);
    }
    return 0;
}

使用gcc编译它并传入一些带有引号的 arguments,例如 JSON 字符串。

> gcc echo-test.c
> ./a.exe '{"foo": "bar"}'

对于Legacy行为, output 是{foo: bar} 但是,使用Standard选项, output 变为{"foo": "bar"}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM