批处理脚本中如何转义双引号

113

我该如何替换批处理文件参数中的所有双引号为转义双引号?以下是我的当前批处理文件,它会在字符串中展开所有命令行参数:

@echo off
call bash --verbose -c "g++-linux-4.1 %*"

它然后使用该字符串调用Cygwin的bash,执行Linux交叉编译器。不幸的是,这样的参数被传递到我的批处理文件中:

"launch-linux-g++.bat" -ftemplate-depth-128 -O3 -finline-functions 
-Wno-inline -Wall  -DNDEBUG   -c 
-o "C:\Users\Me\Documents\Testing\SparseLib\bin\Win32\LinuxRelease\hello.o" 
"c:\Users\Me\Documents\Testing\SparseLib\SparseLib\hello.cpp"

在第一个传入的路径周围的第一个引号过早地结束了传递给GCC的字符串,并直接将其余参数传递给bash(失败得惊人)。

我想如果我可以将参数连接成单个字符串,然后转义引号,那么它应该可以正常工作,但我很难确定如何做到这一点。有人知道吗?

6个回答

119

在批处理脚本中,转义字符是^。但对于双引号字符串,请使用双引号:

"string with an embedded "" character"

6
双引号对我没有起作用,但使用 ^ 运算符效果很好。 - davenpcj
37
在非引用字符串中,^是转义字符;在双引号字符串中,则被视为文字。 与类Unix(POSIX风格)shell不同的是,在cmd.exe中,双引号内部的双引号没有标准化的_shell_处理方式,其解释留给被调用程序处理。 - mklement0
14
实际上,大多数可执行文件/脚本解释器采用了C语言的惯例,期望在双引号字符串内将 " 字符转义为 \" (适用于至少 C/C++、Python、Perl 和 Ruby)。 相比之下,"" 仅在少数情况下被识别: 在传递给批处理文件的参数中,"" 被视为嵌入的双引号,但在相应的 %<n> 参数中保持不变,即使通过 %~<n> 去除了双引号。 Python优雅地另外也接受 "" 作为 \" 的替代。 - mklement0

115

eplawless's own answer 简单而有效地解决了他特定的问题:它将整个参数列表中的所有 " 实例替换为 \",这是 Bash 要求在双引号内表示双引号的方式。

一般来说,回答如何使用 cmd.exe 转义双引号内的双引号字符串的问题,取决于调用哪个程序(无论是在命令行上 - 通常仍被错误地称为“DOS提示符” - 还是在批处理文件中):有关 PowerShell 的内容,请参见底部。

tl;dr:

答案取决于你调用的是哪个程序

  • 当传递参数给另一个批处理文件时,您必须使用"",而对于使用Microsoft的C/C++/.NET编译器创建的应用程序,您可以使用""(这些编译器还接受\"),在Windows上包括Python、Node.js和PowerShell (Core) 7+的CLI(pwsh),但不包括Windows PowerShell (powershell.exe):

    • 例如:foo.bat "We had 3"" of rain."
  • 以下仅适用于针对批处理文件

    • ""是让命令解释器(cmd.exe)将整个双引号字符串作为单个参数处理的唯一方法(尽管如果您只是将所有参数通过使用%*传递到另一个程序中,那么这并不重要)

    • 然而,不幸的是,不仅保留了封闭的双引号(像往常一样),连续的转义也被保留下来,因此获取预期的字符串是一个两步过程;例如,假设双引号字符串作为第一个参数传递:%1

      • set "str=%~1"删除封闭的双引号;set "str=%str:""="%"然后将重复的双引号转换为单个双引号。
        请确保在赋值部分周围使用封闭的双引号,以防止对值的不必要解释。
  • \"是许多其他程序所要求的唯一选项(例如Ruby、Perl、PHP以及使用CommandLineToArgv Windows API函数解析其命令行参数的程序),但它在cmd.exe中的使用不够稳健和安全:

    • \"是许多可执行文件和解释器在从外部(通过命令行)传递字符串时要么需要(包括Windows PowerShell),要么支持作为""的替代方案——不过,最终还是由目标程序来解析参数列表。
      • 例如:foo.exe "We had 3\" of rain."
    • 然而,使用\"可能会破坏调用,并至少在理论上会导致不想要的任意命令执行和/或输入/输出重定向
      • 以下字符存在这种风险:& | < >
      • 例如,下面的结果是无意中执行了ver命令;请参见下面的说明和下一个符号点以获取解决方法:
        • foo.exe "3\" of snow" "& ver."
    • 对于调用Windows PowerShellCLIpowershell.exe\""^""是健壮而受限的替代方案(请参见下面的“调

      背景

      注意:以下内容基于我的实验。如果有错误,请告诉我。

      类 Unix 系统上的 POSIX 类 shell,如 Bash,在将参数逐个传递给目标程序之前会对参数列表(字符串)进行分词处理:除了其他扩展之外,它们还将参数列表拆分为单个单词(单词拆分)并从结果单词中删除引号字符(引号去除)。目标程序接收到一个由单个、逐字传递的参数数组,即带有语法引号已被移除的参数。

      相比之下,Windows 命令解释器似乎不对参数列表进行分词处理,而是将包含所有参数的单个字符串(包括引号字符)直接传递给目标程序。
      然而,在将单个字符串传递给目标程序之前,一些预处理会发生:双引号字符串之外的 ^ 转义字符会被移除(它们转义后面的字符),并且变量引用(例如,%USERNAME%)会先进行插值处理。

      因此,与Unix不同的是,解析参数字符串并将其拆分为没有引号的单个参数的责任落在目标程序身上。因此,不同的程序可能需要不同的转义方法,并且没有一种单一的转义机制可以保证适用于所有程序 - https://dev59.com/6G855IYBdhLWcg3w5Igb#4094897 在Windows命令行解析方面提供了极好的背景信息。
      实际上,“\”非常常见,但是如上所述,它对于cmd.exe来说并不安全:
      由于cmd.exe本身不将“\”识别为转义的双引号,因此它可能会将命令行中后续的标记误认为是未加引号的标记,并将其解释为命令和/或输入/输出重定向。
      简而言之,如果以下任何一个字符跟随打开或未平衡的“\”,则问题就会出现:& | < >;例如:
      foo.exe "3\" of snow" "& ver."
      

      cmd.exe会将\"误解为普通的双引号,导致生成以下标记:

      • "3\"
      • of
      • snow" "
      • 其余:& ver.

      由于cmd.exe认为& ver.未加引号的,它将其解释为&(命令序列运算符),然后是一个要执行的命令的名称(ver. - .被忽略;ver报告了cmd.exe的版本信息)。
      整个效果是:

      • 首先,只使用前三个标记调用foo.exe
      • 然后,执行ver命令。

      即使在意外命令无害的情况下,由于并非所有参数都传递给它,因此总体命令也无法按设计工作。

      许多编译器/解释器仅识别 " - 例如,GNU C/C++ 编译器、Perl、Ruby、PHP 以及使用 CommandLineToArgv Windows API 函数来解析命令行参数的程序 - 对于它们,这个问题没有简单的解决方案。 基本上,您必须提前知道哪些部分的命令行被错误地解释为未引用,并有选择地在这些部分中转义所有出现的 & | < >
      相比之下,使用 "" 是安全的,但遗憾的是,只有基于 Microsoft 编译器的可执行文件和批处理文件支持它(在批处理文件的情况下,有上述怪癖),这显然排除了 PowerShell - 请参见下一节。

      cmd.exe或类POSIX shell中调用PowerShell的CLI:

      注意:有关在PowerShell内部处理引号的方法,请参见底部部分。

      当从外部调用时 - 例如从命令行或批处理文件中的cmd.exe

      • PowerShell [Core] v6+现在能够正确识别""(除了\"),这既是安全可用的,也能保留空格

        • pwsh -c " ""a & c"".length "不会出错,并且正确返回6
      • Windows PowerShell(最新版本为5.1,是遗留版)仅能识别\"""",后者是来自cmd.exe最佳选择,以"^"""的形式呈现(即使在双引号字符串内部,PowerShell使用`作为转义字符并且接受""——请参见底部部分),如下所述:

      cmd.exe / 批处理文件中调用 Windows PowerShell:
      • "" breaks,因为它基本上不受支持:

        • powershell -c " ""ab c"".length " -> 错误"The string is missing the terminator"
      • \"""" 原则上可以工作,但不是安全的

        • powershell -c " \"ab c\".length " 正常工作:输出5(注意2个空格)
        • 但这并不安全,因为cmd.exe元字符会破坏命令,除非进行转义:
          powershell -c " \"a& c\".length " 破坏了,因为有&,必须转义为^&
      • \""安全的,但会规范化内部空格,这可能是不希望的:

      • powershell -c " \""a& c\"".length " 输出4(!),因为2个空格被规范化为1个。

      • 对于Windows PowerShell,最佳选择是"^"" 感谢Venryx发现这种方法。,而对于PowerShell(Core)7+,则是""

        • Windows PowerShell:powershell -c " "^""a& c"^"".length " 正常工作:不会破坏-尽管有&-并且输出5,即正确保留了空格。

        • PowerShell Corepwsh -c """a& c"".length "

        • 有关更多信息,请参见this answer

      在类Unix平台(Linux、macOS)上,当从诸如bash的POSIX-shell调用PowerShell [Core]的CLI(即pwsh)时,您必须使用\",但这种方式既安全又保留空格。
      $ pwsh -c " \"a&  c\".length " # OK: 5
      
      # Alternative, with '...' quoting: no escaping of " needed.
      $ pwsh -c ' "a&  c".length ' # OK: 5
      

      相关信息

      • ^只能用作未加引号的字符串中的转义字符——在双引号内,^不是特殊字符,而被视为字面量。

        • 注意: ^用于传递给call语句的参数是错误的(这适用于调用其他批处理文件或二进制文件以及调用同一批处理文件中的子程序的两种用法):
          • 双引号值中的^实例会被不可思议地加倍,从而改变了被传递的值:例如,如果变量%v%包含字面值a^b,则call :foo "%v%"会在子程序:foo中将"a^^b"(!)分配给%1(第一个参数)。
          • 使用^call未加引号用法完全失效,因为无法再使用^来转义特殊字符:例如,call foo.cmd a^&b会悄悄地崩溃(而不是像没有call一样将字面值a&b传递给foo.cmd)——至少在Windows 7上是这样。
      • 转义字面的%是一个特例,不幸的是,这取决于字符串是在命令行还是批处理文件内部指定的,需要使用不同的语法;请参见https://dev59.com/m3I-5IYBdhLWcg3wVWzv#31420292

        • 简而言之,在批处理文件中,请使用%%。在命令行中,%无法转义,但如果在变量名的开头、结尾或内部(例如echo %^foo%中)的未加引号字符串中放置一个^,则可以防止变量扩展(插值);不属于变量引用的命令行上的%实例被视为字面量(例如100%)。
      • 通常,要安全地处理可能包含空格和特殊字符的变量值

        • 赋值: 变量名和值都封装在一对双引号中;例如,set "v=a & b"将字面值a & b分配给变量%v%(相比之下,set v="a & b"会使双引号成为值的一部分)。将字面的%实例转义为%%(仅适用于批处理文件——请参见上文)。
        • 引用: 将变量引用括在双引号中,以确保它们的值不被插值;例如,echo "%v%"不会将%v%的值插值,并打印"a & b"(但请注意,双引号总是被打印

          在 PowerShell 中引用:

          Windows PowerShell 是一个比 cmd.exe 更先进的 shell,它已经成为 Windows 的一部分很多年了(而 PowerShell Core 也将 PowerShell 体验带到了 macOS 和 Linux 上)。

          PowerShell 在引用方面内部保持一致:

          • 在双引号字符串中使用 `""" 转义双引号
          • 在单引号字符串中使用 '' 转义单引号

          这适用于 PowerShell 命令行以及在 PowerShell 中从脚本或函数传递参数时。

          (如上所述,从外部传递转义后的双引号到 PowerShell 需要使用 \" 或更可靠的是 \"" - 其他方式都不起作用)。

          很遗憾,当从PowerShell调用外部程序时,您需要适应PowerShell自身的引号规则,并对目标程序进行转义:
          • 这种问题行为也在此答案中讨论和总结; PowerShell Core 7.2.0-preview.5中引入的实验性PSNativeCommandArgumentPassing功能(假设它成为官方功能)将至少解决接受\"的那些外部程序的问题。

          双重-引号内的双重-引号:

          考虑字符串"3`" of rain",PowerShell在内部将其转换为文字3" of rain

          如果要将此字符串传递给外部程序,除了PowerShell本身的转义之外,您还需要应用目标程序的转义;假设您想将字符串传递给一个C程序,它期望嵌入的双引号被转义为\"
          foo.exe "3\`" of rain"
          

          请注意,为了使PowerShell工作正常,“`”必须存在,以及为了让目标程序正常工作,“\”也必须存在。
          同样的逻辑也适用于调用批处理文件,必须使用“””。
          foo.bat "3`"`" of rain"
          

          相比之下,在双引号字符串中嵌入单引号不需要任何转义。在单引号字符串中嵌入单引号不需要额外的转义;考虑“2' of snow”,这是PowerShell表示“2' of snow”的方式。
          foo.exe '2'' of snow'
          foo.bat '2'' of snow'
          

          PowerShell在传递单引号字符串到目标程序之前将其转换为双引号字符串。

          然而,在单引号字符串内部的双引号,它们不需要被转义为PowerShell,但是仍然需要被转义为目标程序

          foo.exe '3\" of rain'
          foo.bat '3"" of rain'
          

          PowerShell v3引入了神奇的选项--%,称为停止解析符号,它通过将其后的任何内容未经解释地传递给目标程序来减轻一些痛苦,除了类似于cmd.exe风格的环境变量引用(例如%USERNAME%)会被扩展;例如:

          foo.exe --% "3\" of rain" -u %USERNAME%
          

          注意,仅对目标程序转义嵌入的"\"(而不是像\`"那样也针对PowerShell)就足够了。
          然而,这种方法:
          • 不允许转义%字符以避免环境变量扩展。
          • 排除了直接使用PowerShell变量和表达式的可能性;相反,必须在第一步中在字符串变量中构建命令行,然后在第二步中使用Invoke-Expression调用它。
          解决此问题的替代方案*是通过一个包含整个命令行的单个参数调用cmd /c
          cmd /c "foo.exe `"3\`" of rain`" -u $env:USERNAME"
          

          因此,尽管PowerShell进行了许多改进,但在调用外部程序时并没有使转义更加容易 - 相反,它引入了对单引号字符串的支持。
          如果您不介意安装第三方模块(由我编写),Native 模块Install-Module Native)提供了向前和向后兼容的辅助函数ie,它消除了额外的转义需求,并包含了Windows上高级CLI的重要适应性。
          # Simply prepend 'ie' to your external-program calls.
          ie foo.exe '3" of rain' -u $env:USERNAME
          

3
我猜想微软的某个人认为这是个好主意,认为双插号会在第二阶段解析时被自动移除。但这是一个大失败,因为它在引号中不起作用,而且有效阻止了任何特殊字符的转义。例如 call echo cat^^&dog ,只靠插号是无法解决的。 - jeb
谢谢,@jeb,我甚至没有考虑过使用call和未引用的^,正如你所指出的那样,这是非常糟糕的。似乎在call echo cat^&dog(单个正确转义&^)中,目标命令(echo)甚至从未被调用!整个命令默默地失败了。我已经相应地更新了答案。 - mklement0
不错的回答。然而,我不建议使用 "" 作为转义 ",而是始终使用 \"(请参见我的答案以了解在 cmd 中使用它的更安全方法)。我不知道任何官方文档将 "" 定义为转义引号,但至少有两个文档提到了 \".NETVS。尽管文档错误,Win32 api 也遵循这些规则。 - T S
@TS:谢谢。然而,我并没有“推荐”任何东西,相反,我试图明确指出:没有“强大的、通用的解决方案”:你需要知道引用“特定目标程序”的期望。当简单测试足以验证实际行为时,您不需要官方文档。是的,\"可以在更多的目标程序中使用。 - mklement0
2
在所有这些谈话中,应该提到一个简单的规则,即cmd.exe实际上遵循了这一规则,以螺旋式进入所有这些复杂情况:由于命令行从左到右进行处理,双引号状态只是翻转。如果左侧有奇数个",则您处于标记中,分隔符字符将被禁用;如果左侧有偶数个",则分隔符将被启用。这就是为什么您可以省略最后一个"。而""不会作为分隔符,因为虽然它最终不会改变奇数/偶数状态,但内部没有分隔符。 - Glenn Slayden
显示剩余2条评论

25

最终,Google找到了答案。在批处理中进行字符串替换的语法如下:

set v_myvar=replace me
set v_myvar=%v_myvar:ace=icate%

这将生成“复制我”。现在我的脚本看起来像这样:

@echo off
set v_params=%*
set v_params=%v_params:"=\"%
call bash -c "g++-linux-4.1 %v_params%"

该代码用于将所有实例的 " 替换为在 bash 中适当转义的 \"


11
作为对 mklement0的精彩回答的补充:
几乎所有可执行文件都接受\"作为转义符号来表示"。然而,在cmd中安全使用这种方法几乎只有通过DELAYEDEXPANSION才能实现。
如果要将一个字面上的"发送到某个进程,则需要将\"赋值给一个环境变量,然后在需要传递引号的地方使用该变量。例如:
SETLOCAL ENABLEDELAYEDEXPANSION
set q=\"
child "malicious argument!q!&whoami"

注意 SETLOCAL ENABLEDELAYEDEXPANSION 似乎仅在批处理文件中起作用。要在交互式会话中启用 DELAYEDEXPANSION,请启动 cmd /V:ON
如果您的批处理文件无法使用 DELAYEDEXPANSION,则可以暂时启用它:
::region without DELAYEDEXPANSION

SETLOCAL ENABLEDELAYEDEXPANSION
::region with DELAYEDEXPANSION
set q=\"
echoarg.exe "ab !q! & echo danger"
ENDLOCAL

::region without DELAYEDEXPANSION

如果您想从包含转义引号的变量中传递动态内容,可以在扩展时使用\"替换""
SETLOCAL ENABLEDELAYEDEXPANSION
foo.exe "danger & bar=region with !dynamic_content:""=\"! & danger"
ENDLOCAL

这种替换方式不安全,不能与 %...% 样式扩展一起使用! 如果遇到 OP,则 bash -c "g++-linux-4.1 !v_params:"=\"!" 是安全版本。
如果由于某些原因暂时无法启用DELAYEDEXPANSION,可以继续阅读以下内容:
在cmd中使用 \" 会更加安全,如果总是需要转义特殊字符的话。 (如果保持一致,就不太可能忘记插入一个脱字符...)
为了实现这个目标,在引号前面加上一个脱字符 (^"),作为字面值传递给子进程的引号必须额外用反斜杠进行转义 (\^")。所有shell元字符也必须用^进行转义,例如:& => ^&; | => ^|; > => ^>; 等等。
示例:
child ^"malicious argument\^"^&whoami^"

来源:每个人都错误地引用命令行参数,请参阅“更好的引用方法”


为了传递动态内容,需要确保以下几点:
包含变量的命令部分必须被 cmd.exe“引用”(如果变量中包含引号,则无法实现 - 不要写成 %var:""=\"%)。为了实现这一点,在变量之前的最后一个 " 和变量之后的第一个 " 不应该进行 ^ 转义。在这两个 " 之间的 cmd 元字符不得转义。例如:

foo.exe ^"danger ^& bar=\"region with %dynamic_content% & danger\"^"

如果%dynamic_content%中包含不匹配的引号,那么这是不安全的。


@mklement0谢谢!是的,这真的很痛苦。它很恼人,而且很容易忘记元字符(因此我大多使用!q!方式)。注意:您的答案在最后一次编辑后略有不一致:在顶部附近,您说:“不要使用^“”。稍后,您将^“用作解决方法的一部分。也许您可以解释两种方法? (1)转义所有元字符(更系统地)/(2)选择性地转义“未引用”区域中的元字符(有时需要传递动态内容,例如foo.exe ^“ danger ^& bar = \”% dynamic_content%\“^“`-这样变量就被引用为cmd) - T S
好的,谢谢 - 回答已更新。我还更清楚地表明了MS编译器接受\"""。我已经链接到您的答案,以获取更复杂的基于变量的方法。现在让我知道它是否有意义。 - mklement0
1
@mklement0 您的回答总是很有道理 :-) 我只是想提供一些可能改进的想法。我还在我的答案中添加了关于 %dynamic_content% 的示例。您认为这已经足够详细了,还是我需要解释更多? - T S
谢谢,为了明确起见:我感激您的意见和交流。您提出了一个关于包含%....%变量的好观点。我得出结论,如果可行的话,您的延迟扩展方法真的是唯一一个可以系统地和稳健地工作的方法。请注意,使用%...%引用时,您无法_保证_cmd.exe将考虑这些引用是否在"..."内。我再次更新了我的答案。 - mklement0
3
谢谢告知,对于本地化setlocal delayedexpansion的想法很好,但你应该用endlocal(无参数)结束块。说实话,看了你的Gist后,我的头有点晕。我们真的在处理边缘情况,我认为未来的读者将在我们两个答案之间找到他们需要的一切。 - mklement0
显示剩余2条评论

0

如果字符串已经在引号内,则使用另一个引号来使其失效。

echo "Insert tablename(col1) Values('""val1""')" 

0

适用于 Windows 10 21H1。

如果我想要从批处理 (.bat) 文件运行 Everything 应用程序,我需要在双引号参数内使用 """

"C:\Program Files\Everything\Everything.exe" -search "<"""D:\My spaced folder""" | """Z:\My_non_spaced_folder"""> <*.jpg | *.jpeg | *.avi | *.mp4>"

希望对你有所帮助。


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