最近,每当涉及双引号时,我在使用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。
答案 0 :(得分:31)
这是known thing:
将参数传递给需要引用字符串的应用程序是很难的。我在IRC中用一个“忙碌的”PowerShell专家问过这个问题,并且花了一个小时让某人找到一种方法(我最初开始在这里发布它根本不可能)。这完全打破了PowerShell作为通用shell的能力,因为我们不能做简单的事情,比如执行sqlcmd。命令shell的首要任务应该是运行命令行应用程序......例如,尝试使用SQL Server 2008中的SqlCmd,有一个-v参数,它接受一系列name:value参数。如果值中包含空格,则必须引用它...
...没有一种方法可以编写命令行来正确调用这个应用程序,所以即使你掌握了所有4种或5种不同的引用和转义方法之后,你仍然在猜测哪种方法可行......或者,你可以向cmd外壳,并完成它。
答案 1 :(得分:15)
如果仅需要Powershell 5的解决方案,请参见:
...,看来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不需要使用反斜杠\
来排除引号的事实,但是使用了所有msvcrt和C#使用的CommandLineToArgvW
函数程序,以通过Windows进程传递的单字符串命令行来构建argv
数组。
详细信息在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 5(包括Win10 / 1909上的PoSh 5.1.18362.145) PoSh基本上都对这些规则了如指掌,也不应该争论,因为这些规则并不是真正的通用,因为任何从理论上讲,您调用的可执行文件可以使用其他方法来解释所传递的命令行。
哪个引导我们-
PoSh做的 试图弄清楚是否将您传递给字符串的字符串 s 作为本机命令的参数,因为它们包含空格。 / p>
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
)调用创建单个字符串命令行表示立场。
Powershell采取的方法是weird并得到more complicated in PoSh V7,据我所知,它必须执行powershell如何处理未加引号的字符串中的不平衡引号。长话短说是:
Powershell将自动引号(括在<"
>中)
字符串,如果它包含空格和,则空格不与
(未转义的)双引号数量不均匀。
PoSh V5的特定引用规则使其不可能将某个类别的字符串作为单个参数传递给子进程。
PoSh V7修复了此问题,因此,只要所有引号都转义\"
(无论如何都需要通过引号CommandLineToArgvW
来使它们),我们可以将PoSh的所有字符串传递给使用CommandLineToArgvW
的子可执行文件。
以下是从PoSh github存储库中提取的C#代码规则,用于我们的工具类:
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;
}
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('"');
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> |
------------------------------|-----------------------------|---------------------------
...
由于时间原因,我在这里截取了更多示例。无论如何,他们不应该为答案增加太多。
要使用CommandLineToArgvW
将任意字符串从Powershell传递到本机命令,我们必须:
I've put the full code on Gist,因为此处包含的时间太长: ConvertTo-ArgvQuoteForPoSh.ps
: Powershell V5 (and C# Code) to allow escaping native command arguments
答案 2 :(得分:13)
我个人避免使用'\'来逃避PowerShell中的事情,因为它在技术上并不是shell转义字符。我用它得到了不可预知的结果。在双引号字符串中,您可以使用""
来获取嵌入的双引号,或者使用反向引号将其转义:
PS C:\Users\Droj> "string ""with`" quotes"
string "with" quotes
单引号也是如此:
PS C:\Users\Droj> 'string ''with'' quotes'
string 'with' quotes
向外部程序发送参数的奇怪之处在于,还有额外的引用评估级别。我不知道这是不是一个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""""
'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"
答案 3 :(得分:2)
依赖CMD来解决接受的答案中指出的问题并不适用于我,因为双引号在调用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进行测试和调试答案 4 :(得分:1)
在撰写本文时,最近的PowerShell版本似乎已经解决了这个问题,因此不再需要担心。
如果您仍然认为您看到此问题,请记住它可能与其他内容有关,例如调用PowerShell的程序,因此如果在从命令提示符或ISE直接调用PowerShell时无法重现它,你应该在其他地方调试。
例如,我在调查使用Process.Start
从C#代码运行PowerShell脚本时报价消失的问题时发现了这个问题。问题实际上是 C# Process Start needs Arguments with double quotes - they disappear 。
答案 5 :(得分:0)
哦,亲爱的。显然,试图将双引号转义以使它们从命令行进入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说“ hi”
因此,您正在尝试用string重新引入的引号。Replace()将会消失!