Windows 命令解释器(CM.EXE)如何解析脚本?

我遇到了 Ss64.com,它在如何编写 Windows 命令解释器将要运行的批处理脚本方面提供了很好的帮助。

然而,我一直无法找到一个很好的解释 语法的批处理脚本,如何扩展或不扩展的东西,以及如何转义的东西。

下面是一些我没能解决的问题:

  • 报价系统是如何管理的? 我做了一个 TinyPerl脚本
    (foreach $i (@ARGV) { print '*' . $i ; }) ,编译它,并这样称呼它:
    • 输出为 *a "b*c
    • 输出 *"a*b*c"
  • 内部 echo命令是如何工作的? 在这个命令中展开了什么?
  • 为什么我必须在文件脚本中使用 for [...] %%I,而在交互式会话中使用 for [...] %I
  • 什么是转义字符,在什么上下文中?如何转义百分号?例如,我怎样才能从字面上重复 %PROCESSOR_ARCHITECTURE%?我发现 echo.exe %""PROCESSOR_ARCHITECTURE%工作,有没有更好的解决方案?
  • %对是如何匹配的? 例如:
    • set b=aecho %a %b% c%%a a c%
    • set a =becho %a %b% c%bb% c%
  • 如果变量包含双引号,我如何确保变量作为单个参数传递给命令?
  • 使用 set命令时如何存储变量?例如,如果我先做 set a=a" b,然后做 echo.%a%,我就得到 a" b。然而,如果我使用 UnxUtils 中的 echo.exe,我得到的是 a b。为什么 %a%会以不同的方式膨胀?

谢谢你的灯光。

85235 次浏览

编辑: 查看接受的答案,接下来的内容是错误的,只解释了如何将命令行传递给 TinyPerl。


至于引用,我觉得行为如下:

  • 当找到 "时,字符串水球效应就开始了
  • 当发生字符串球化时:
    • 每个非 "的字符都是球状的
    • 当发现 "时:
      • 如果后面跟着 ""(因此是三重 ") ,那么字符串中将添加一个双引号
      • 如果后面跟着 "(因此是一个双 ") ,那么字符串和字符串 globbing 结束将添加一个双引号
      • 如果下一个字符不是 ",则字符串水滴结束
    • 当线结束时,线球化结束。

简而言之:

"a """ b "" c"""由两个字符串组成: a " b "c"

如果在一行的末尾,"a"""a""""a""""都是相同的字符串

当从命令窗口调用命令时,命令行参数的标记化不是由 cmd.exe(也称为“ the shell”)完成的。大多数情况下,标记化是由新形成的进程的 C/C + + 运行时完成的,但这并不一定——例如,如果新进程不是用 C/C + + 编写的,或者如果新进程选择忽略 argv并为自己处理原始命令行(例如使用 GetCommandLine ())。在操作系统级别,Windows 将未标记为单个字符串的命令行传递给新进程。这与大多数 * nix shell 不同,后者在将参数传递给新形成的进程之前,以一致的、可预测的方式对参数进行标记。所有这些都意味着你可能会在 Windows 上的不同程序中体验到大相径庭的参数标记化行为,因为各个程序经常自己动手进行参数标记化。

如果这听起来像是无政府主义的话,那就是了。但是,由于大量的 Windows 程序 使用 Microsoft C/C + + 运行时的 argv,所以理解 MSVCRT 如何标记参数通常是有用的。以下是一段摘录:

  • 参数由空格分隔,空格可以是空格,也可以是制表符。
  • 双引号包围的字符串被解释为单个参数,而不管其中包含的空白。可以在参数中嵌入带引号的字符串。注意,插入符号(^)不被识别为转义字符或分隔符。
  • 双引号前面加上反斜杠“ ,解释为文字双引号(”)。
  • 反斜杠按字面意思解释,除非它们立即位于双引号之前。
  • 如果一个偶数个反斜杠后跟一个双引号,那么每对反斜杠()在 argv 数组中放置一个反斜杠() ,双引号(”)被解释为字符串分隔符。
  • 如果一个奇数个反斜杠后面跟着一个双引号,那么对于每对反斜杠() ,在 argv 数组中放置一个反斜杠() ,并且双引号被剩余的反斜杠解释为一个转义序列,导致在 argv 中放置一个字面双引号(”)。

微软的“批处理语言”(.bat)在这种无政府环境中也不例外,它已经为标记化和转义开发了自己独特的规则。在将参数传递给新执行的进程之前,cmd.exe 的命令提示符似乎确实对命令行参数进行了一些预处理(主要用于变量替换和转义)。在本页的 jeb 和 dbenham 的精彩回答中,您可以了解更多关于批处理语言和 cmd 转义的底层细节。


让我们用 C 语言构建一个简单的命令行实用程序,看看它是如何描述测试用例的:

int main(int argc, char* argv[]) {
int i;
for (i = 0; i < argc; i++) {
printf("argv[%d][%s]\n", i, argv[i]);
}
return 0;
}

(注意: argv [0]总是可执行文件的名称,为了简洁起见,下面省略了它。在 WindowsXPSP3上测试。使用 VisualStudio2005编译)

> test.exe "a ""b"" c"
argv[1][a "b" c]


> test.exe """a b c"""
argv[1]["a b c"]


> test.exe "a"" b c
argv[1][a" b c]

还有一些我自己的测试:

> test.exe a "b" c
argv[1][a]
argv[2][b]
argv[3][c]


> test.exe a "b c" "d e
argv[1][a]
argv[2][b c]
argv[3][d e]


> test.exe a \"b\" c
argv[1][a]
argv[2]["b"]
argv[3][c]

我们进行了实验来研究批处理脚本的语法。我们还研究了批处理和命令行模式之间的差异。

批处理线解析器:

以下是批处理文件行解析器中各个阶段的简要概述:

第0阶段)读线:

第一阶段扩展百分比:

阶段2)处理特殊字符,标记,并构建一个缓存的命令块: 这是一个复杂的进程,受到诸如引号、特殊字符、标记分隔符和插入符转义等因素的影响。

阶段3)仅当命令块没有以 @开头,且 ECHO 在前一步开始时为 ON 时,才回显解析命令

阶段4)用于 %X变量扩展: 仅当 FOR 命令处于活动状态且 DO 之后的命令正在处理时。

阶段5)延迟扩展: 只有当延迟扩展被启用时

阶段5.3)管道处理: 仅当命令位于管道的两侧时

第5.5阶段)执行重定向:

阶段6) CALL 处理/符号加倍: 仅当命令标记为 CALL 时

阶段7)执行: 命令被执行


以下是每个阶段的详情:

请注意,下面描述的阶段只是批处理解析器工作方式的一个模型。实际的 cmd.exe 内部可能不反映这些阶段。但是该模型在预测批处理脚本的行为方面是有效的。

读取行: 通过第一个 <LF>读取输入行。

  • 当读取要解析为命令的行时,<Ctrl-Z>(0x1A)被读取为 <LF>(LineFeed 0x0A)
  • 当 GOTO 或 CALL 在扫描 a: label (<Ctrl-Z>)时读取行时,它被视为自身-它被 转换为 <LF>

第一阶段扩展百分比:

  • 一个双 %%被一个单 %所取代
  • 扩展参数(%*%1%2等)
  • 展开 %var%,如果 var 不存在,则将其替换为空
  • 线是截断在第一个 <LF>不在 %var%扩展
  • 要获得完整的解释,请阅读来自 dbenham 相同的线程: 百分比阶段的前半部分

阶段2)处理特殊字符,标记,并构建一个缓存的命令块: 这是一个复杂的进程,受到诸如引号、特殊字符、标记分隔符和插入符转义等因素的影响。下面是这个过程的近似值。

在这个阶段中有一些重要的概念。

  • 标记只是作为单位处理的字符串。
  • 令牌由令牌分隔符分隔。标准令牌分隔符是 <space> <tab> ; , = <0x0B> <0x0C><0xFF>
    连续的令牌分隔符被视为一个-令牌分隔符之间没有空的令牌
  • 引号字符串中没有标记分隔符。整个引号字符串始终被视为单个标记的一部分。单个标记可以由引号字符串和非引号字符组成。

以下字符在这个阶段可能有特殊的意义,这取决于上下文: <CR> ^ ( @ & | < > <LF> <space> ^0 ^1 ^2 ^3 ^4 ^5 ^6

从左到右看每个字符:

  • 如果 <CR>然后删除它,就好像它从来没有在那里(除了怪异的 重定向行为重定向行为)
  • 如果是插入符号(^) ,则转义下一个字符,并删除转义插入符号。转义字符失去所有特殊意义(除了 <LF>)。
  • 如果是引号(") ,则切换引号标志。如果引号标志是活动的,那么只有 "<LF>是特殊的。所有其他字符都失去了它们的特殊意义,直到下一个引号关闭了引号标志。不可能转义结束报价。所有引号字符始终位于同一标记内。
  • <LF>总是关闭引用标志。其他行为取决于上下文,但引号从不改变 <LF>的行为。
    • 逃出 <LF>
      • <LF>被剥离
      • 转义下一个字符。如果在行缓冲区的末尾,那么下一行将被阶段1和1.5读取和处理,并在转义下一个字符之前追加到当前行。如果下一个字符是 <LF>,那么它将被视为一个文字,这意味着这个过程不是递归的。
    • 未转义的 <LF>不在括号内
      • 剥离 <LF>并终止对当前行的解析。
      • 行缓冲区中的任何剩余字符都将被忽略。
    • 在带括号的 FOR 块中取消转义 <LF>
      • <LF>被转换成 <space>
      • 如果在行缓冲区的末尾,则读取下一行并将其追加到当前行。
    • 在带括号的命令块中取消转义 <LF>
      • <LF>被转换成 <LF><space>,而 <space>被视为命令块下一行的一部分。
      • 如果在行缓冲区的末尾,那么下一行将被读取并追加到空格中。
  • 如果其中一个特殊字符 & | <>,则在此处拆分该行,以便处理管道、命令连接和重定向。
    • 在管道(|)的情况下,每一端都是一个单独的命令(或命令块) ,在阶段5.3中得到特殊处理
    • &&&||命令串联的情况下,串联的每一边都被视为一个单独的命令。
    • <<<>>>重定向的情况下,解析重定向子句,临时删除它们,然后将其追加到当前命令的末尾。重定向子句由可选的文件句柄数字、重定向运算符和重定向目标标记组成。
      • 如果重定向操作符之前的令牌是一个单个非转义数字,则该数字指定要重定向的文件句柄。如果找不到句柄标记,则输出重定向默认为1(stdout) ,输入重定向默认为0(stdin)。
  • 如果这个命令的第一个标记(在将重定向移动到末尾之前)以 @开始,那么 @就有特殊的意义。(@在任何其他情况下都不特殊)
    • 特殊的 @被删除。
    • 如果 ECHO 为 ON,则此命令以及此行上的任何后续连接命令将被排除在第3阶段回显之外。如果 @位于开口 (之前,则整个括号内的块被排除在第3阶段回声之外。
  • 进程括号(提供跨多行的复合语句) :
    • 如果解析器不寻找命令标记,则 (不是特殊的。
    • 如果解析器正在查找命令标记并找到 (,那么启动一个新的复合语句并增加括号计数器
    • 如果圆括号计数器 > 0,那么 )终止复合语句并减去圆括号计数器。
    • 如果到达了行末尾,括号计数器大于0,那么下一行将被追加到复合语句(从阶段0再次开始)
    • 如果括号计数器为0,并且解析器正在查找命令,那么 )的功能类似于 REM语句,只要它后面紧跟一个标记分隔符、特殊字符、换行符或文件结束符
      • 除了 ^(行连接是可能的)之外,所有特殊字符都失去了它们的意义
      • 一旦到达逻辑行的末尾,就会丢弃整个“命令”。
  • 每个命令都被解析为一系列标记。第一个令牌始终被视为命令令牌(在剥离特殊 @并将重定向移动到末尾之后)。
    • 去掉命令标记之前的前导标记分隔符
    • 在解析命令标记时,除了标准的标记分隔符之外,(还起到命令标记分隔符的作用
    • 后续令牌的处理取决于命令。
  • 大多数命令只是将命令标记之后的所有参数连接到一个参数标记中。保留所有参数标记分隔符。参数选项通常直到第7阶段才被解析。
  • 三个命令得到特殊处理-IF、 FOR 和 REM
    • IF 被分成两个或三个独立处理的不同部分。IF 结构中的语法错误将导致致命的语法错误。
      • 比较操作是贯穿到第7阶段的实际命令
        • 所有 IF 选项在第2阶段都被完全解析。
        • 连续的标记分隔符折叠成单个空格。
        • 根据比较运算符的不同,将标识一个或两个值标记。
      • True 命令块是条件之后的一组命令,与其他命令块一样进行解析。如果要使用 ELSE,则必须将 True 块括起来。
      • 可选的 False 命令块是 ELSE 之后的一组命令。
      • True 和 False 命令块不会自动流入后续阶段。它们的后续处理由第7阶段控制。
    • FOR 在 DO 后一分为二。FOR 结构中的语法错误将导致致命的语法错误。
      • 通过 DO 的部分是贯穿第7阶段的实际 FOR 迭代命令
        • 在第2阶段完全解析所有 FOR 选项。
        • IN 括号中的子句将 <LF>视为 <space>。解析 IN 子句后,将所有标记连接在一起,形成单个标记。
        • 通过 DO,连续的非转义/非引号令牌分隔符在 FOR 命令中折叠成单个空格。
      • DO 后面的部分是通常解析的命令块。DO 命令块的后续处理由阶段7中的迭代控制。
    • 在第二阶段检测到的 REM 与其他所有命令的处理方式有很大的不同。
      • 只解析一个参数标记——解析器忽略第一个参数标记之后的字符。
      • REM 命令可能出现在第3阶段的输出中,但是该命令永远不会执行,并且回显了原始参数文本-转义符号不会被删除,除非..。
        • 如果只有一个参数令牌以一个未转义的 ^结束,那么该参数令牌将被丢弃,随后的一行将被解析并追加到 REM。这样重复,直到有多个标记,或者最后一个字符不是 ^
  • 如果命令标记以 :开始,并且这是第2阶段的第一轮(由于第6阶段的 CALL 而没有重新启动) ,那么
    • 标记通常作为 未执行标签处理。
      • 行的其余部分被解析,但是 )<>&|不再有特殊的含义。该行的整个剩余部分被认为是标签“ command”的一部分。
      • ^仍然是特殊的,这意味着可以使用行延续将后续行追加到标签。
      • 带括号的块中的 未执行标签将导致致命的语法错误,除非紧接着在下一行中出现命令或 执行标签
        • 对于 未执行标签之后的第一个命令,(不再具有特殊意义。
      • 该命令在标签解析完成后中止。标签的后续阶段不会发生
    • 有三个异常可能导致在阶段2中发现的标签被视为继续在阶段7中进行解析的 执行标签
      • 在标签标记之前有一个重定向,并且在行上有一个 |管道或 &&&||命令串联。
      • 在标签标记之前有一个重定向,该命令位于一个带括号的块中。
      • Label 标记是括号内的一行中的第一个命令,上面的一行以 未执行标签结束。
    • 当在阶段2中发现 执行标签时发生以下情况
      • 标签、它的参数和它的重定向都被排除在第3阶段的任何回送输出之外
      • 该行上的任何后续连接命令都将被完全解析和执行。
    • 有关 执行标签未执行标签的更多信息,请参见 https://www.dostips.com/forum/viewtopic.php?f=3&t=3803&p=55405#p55405

阶段3)仅当命令块没有以 @开头,且 ECHO 在前一步开始时为 ON 时,才回显解析命令

阶段4)用于 %X变量扩展: 仅当 FOR 命令处于活动状态且 DO 之后的命令正在处理时。

  • 此时,批处理的第一阶段已经将类似 %%X的 FOR 变量转换为 %X。对于阶段1,命令行具有不同的百分比展开规则。这就是命令行使用 %X而批处理文件使用 %%X作为 FOR 变量的原因。
  • FOR 变量名是区分大小写的,但是 ~modifiers不区分大小写。
  • ~modifiers优先于变量名。如果 ~后面的字符既是修饰符又是有效的 FOR 变量名,并且后面存在一个活动的 FOR 变量名,则该字符将被解释为修饰符。
  • FOR 变量名是全局的,但仅限于 DO 子句的上下文中。如果从 FORDO 子句中调用例程,则 FOR 变量不会在 CALLed 例程中展开。但是如果例程有自己的 FOR 命令,那么内部 DO 命令可以访问当前定义的 FOR 变量。
  • 可以在嵌套的 FORs 中重用 FOR 变量名。内部 FOR 值具有优先级,但是一旦关闭内部 FOR 值,则还原外部 FOR 值。
  • 如果 ECHO 在这个阶段的开始是 ON,那么在 FOR 变量被展开之后,重复阶段3)来显示已解析的 DO 命令。

——从这一点开始,阶段2中标识的每个命令都将分别处理。
——在转移到下一个命令之前,完成一个命令的第5阶段到第7阶段。

阶段5)延迟扩展: 只有当延迟扩展处于打开状态时,命令才不在 管道两边带括号的块中,命令也不是 “裸体”批处理脚本(没有括号的脚本名称、 CALL、命令连接或管道)。

  • 对命令的每个令牌进行分析以独立进行延迟扩展。
    • 大多数命令解析两个或多个令牌-命令令牌、参数令牌和每个重定向目标令牌。
    • FOR 命令仅解析 IN 子句标记。
    • IF 命令只解析比较值——一个或两个,取决于比较运算符。
  • 对于每个已解析的标记,首先检查它是否包含任何 !。如果没有,则不解析标记-对于 ^字符非常重要。 如果令牌包含 !,则从左到右扫描每个字符:
    • 如果它是一个插入符号(^) ,下一个字符没有特殊含义,那么将删除该插入符号本身
    • 如果它是一个叹号,搜索下一个叹号(不再观察插入符号) ,展开到变量的值。
      • 连续打开的 !折叠成一个单独的 !
      • 任何剩余的未配对 !被移除
    • 在这个阶段展开 vars 是“安全的”,因为不再检测到特殊字符(甚至 <CR><LF>)
    • 要获得更完整的解释,请阅读本文的后半部分 相同的线程叹号阶段

阶段5.3)管道处理: 仅当命令位于管道的两侧时
管道的每一端都是独立和异步处理的。

阶段5.5)执行重定向: 在阶段2中发现的任何重定向现在都被执行。

  • 阶段4和5的结果可能会影响在阶段2中发现的重定向。
  • 如果重定向失败,则中止命令的其余部分。

阶段6) CALL 处理/插入符加倍: 仅当命令标记为 CALL,或者在第一个出现的标准标记分隔符之前的文本为 CALL。如果从较大的命令标记解析 CALL,则在继续之前将未使用的部分预先添加到参数标记中。

  • 扫描参数标记以查找未引号的 /?。如果在令牌中的任何地方找到,则中止第6阶段并继续到第7阶段,在那里将打印针对 CALL 的 HELP。
  • 删除第一个 CALL,这样可以堆叠多个 CALL
  • 全部加倍
  • 重新启动阶段1、1.5和2,但不要继续进行阶段3
    • 任何加倍的插入符号只要没有被引用,就会减少到一个插入符号。但不幸的是,引用的插入符号仍然是原来的两倍。
    • 第一阶段有点变化 - 步骤1.2或1.3中的展开错误中止 CALL,但该错误不致命-批处理继续。
    • 第二阶段的任务稍作改动
      • 检测到任何在第二阶段的第一轮中未检测到的新出现的未引号、未转义的重定向,但是在没有实际执行重定向的情况下将其删除(包括文件名)
      • 任何新出现的未引用、未转义的插入符号在行的末尾都将被删除,而不执行行续行
      • 如果检测到下列任何一项,则 CALL 无错中止
        • 新出现的未引用,未转义的 &|
        • 结果命令标记以无引号、无转义的 (开头
        • 删除的 CALL 之后的第一个标记以 @开始
      • 如果结果命令是一个看似有效的 IF 或 FOR,那么执行将随后失败,并出现一个错误,指出 IFFOR不能识别为内部或外部命令。
      • 当然,如果结果命令标记是以 :开头的标签,则 CALL 在第2阶段的第2轮中不会中止。
  • 如果结果命令令牌是 CALL,那么重新启动阶段6(重复直到没有更多的 CALL)
  • 如果生成的命令令牌是一个批处理脚本或者一个: label,那么 CALL 的执行将由第6阶段的其余部分完全处理。
    • 在调用堆栈上推送当前批处理脚本文件的位置,以便在 CALL 完成时可以从正确的位置恢复执行。
    • 使用所有结果标记为 CALL 设置% 0、% 1、% 2、 ...% N 和% * 参数标记
    • 如果命令标记是以 :开头的标签,则
      • 重启第五阶段。这会影响: label 被调用。但是由于已经设置了% 0等令牌,因此它不会更改传递给 CALLed 例程的参数。
      • 执行 GOTO 标签将文件指针定位在子例程的开头(忽略: label 后面可能出现的任何其他标记) ,有关 GOTO 如何工作的规则,请参见第7阶段。
    • 否则将控制转移到指定的批处理脚本。
    • 标签或脚本的执行一直持续到到达 EXIT/B 或文件末尾,此时将弹出 CALL 堆栈并从保存的文件位置继续执行。
      第7阶段不会对 CALLed 脚本或: label 执行。
  • 否则,第6阶段的结果将进入第7阶段执行。

阶段7)执行: 命令被执行

  • 7.1-执行内部命令 -如果引用了命令标记,则跳过此步骤。否则,尝试解析出内部命令并执行。
    • 下列测试用于确定未加引号的命令标记是否代表内部命令:
      • 如果命令标记与内部命令完全匹配,则执行它。
      • 否则在 + / [ ] <space> <tab> , ;=第一次出现之前中断命令标记
        如果前面的文本是内部命令,则请记住该命令
        • 如果处于命令行模式,或者命令来自括号内的命令块,IF true 或 false 命令块,FOR DO 命令块,或者与命令串联有关,那么执行内部命令
        • 否则(必须是批处理模式下的独立命令)扫描当前文件夹和 PATH 中的。COM,.EXE.BAT,或者。基名与原始命令标记匹配的 CMD 文件
          • 如果第一个匹配的文件是. BAT 或. CMD,那么转到7.3. exec 并执行该脚本
          • 否则(找不到匹配项或第一个匹配项为.EXE 或.COM)执行记忆的内部命令
      • 否则在 .\:第一次出现之前中断命令标记
        如果前面的文本不是内部命令,则转到7.2
        否则,前面的文本可能是内部命令。请记住这个命令。
      • + / [ ] <space> <tab> , ;=第一次出现之前中断命令标记
        如果前面的文本是现有文件的路径,则转到7.2
        否则执行记住的内部命令。
    • 如果从较大的命令标记解析内部命令,则命令标记的未使用部分包含在参数列表中
    • 仅仅因为将命令标记解析为内部命令并不意味着它将成功执行。每个内部命令对于如何解析参数和选项以及允许的语法都有自己的规则。
    • 如果检测到 /?,所有内部命令将打印帮助,而不是执行其功能。如果 /?出现在参数中的任何位置,大多数都能识别它。但是像 ECHO 和 SET 这样的命令只有在第一个参数标记以 /?开头时才打印帮助。
    • SET 有一些有趣的语义:
      • 如果 SET 命令在启用变量名和扩展名之前有引号
        值 = content
        然后使用第一个等号和最后一个引号之间的文本作为内容(排除第一个等号和最后一个引号)。忽略最后一个引号后面的文本。如果等号后面没有引号,那么行的其余部分将用作内容。
      • 如果 SET 命令的名称前面没有引号
        值 = "content" not ignored
        然后,等号后面行的整个剩余部分被用作内容,包括可能出现的所有引号。
    • 将计算 IF 比较,并根据条件为 true 还是 false,从阶段5开始处理适当的已解析依赖命令块。
    • 对 FOR 命令的 IN 子句进行适当的迭代。
      • 如果这是一个用于迭代命令块输出的 FOR/F,那么:
        • IN 子句通过 CMD/C 在新的 CMD.exe 进程中执行。
        • 命令块必须第二次遍历整个解析过程,但这次是在命令行上下文中
        • ECHO 将启动 ON,延迟扩展通常将启动禁用(取决于注册表设置)
        • 一旦子 cmd.exe 进程终止,IN 子句命令块所做的所有环境更改都将丢失
      • 对于每次迭代:
        • 定义 FOR 变量值
        • 然后处理已经解析的 DO 命令块,从阶段4开始。
    • GOTO 使用以下逻辑来定位: label
      • 从第一个参数标记解析标签
      • 扫描标签的下一次出现
        • 从当前文件位置开始
        • 如果到达了文件的末尾,那么循环回到文件的开始,并继续到原始的起始点。
      • 扫描在找到标签的第一个匹配项处停止,并将文件指针设置为紧跟在标签后面的行。脚本的执行从那时开始恢复。注意,一个成功的真 GOTO 将立即中止任何已解析的代码块,包括 FOR 循环。
      • 如果找不到标签,或标签标记丢失,则 GOTO 失败,打印错误消息,并弹出调用堆栈。这有效地起到 EXIT/B 的作用,除了当前命令块中任何已经解析过的命令(在 GOTO 之后)仍然被执行,但是在 CALLer 的上下文中(存在于 EXIT/B 之后的上下文)
      • 有关标签解析规则的更精确描述,请参见 https://www.dostips.com/forum/viewtopic.php?t=3803,有关标签扫描规则,请参见 https://www.dostips.com/forum/viewtopic.php?t=8988
    • RENAME 和 COPY 都接受源路径和目标路径的通配符。但是微软在记录通配符是如何工作的方面做得很糟糕,特别是对于目标路径。在 WindowsRENAME 命令如何解释通配符?中可以找到一组有用的通配符规则
  • 7.2-执行音量更改 -否则,如果命令标记不以引号开头,正好是两个字符长度,并且第二个字符是冒号,则更改音量
    • 忽略所有参数标记
    • 如果找不到由第一个字符指定的卷,则中止操作并出现错误
    • 除非使用 SUBST 为 ::定义卷,否则 ::的命令标记总是会导致错误
      如果使用 SUBST 为 ::定义一个卷,那么该卷将被更改,它不会被视为一个标签。
  • 7.3-执行外部命令 -否则尝试将该命令视为外部命令。
    • 如果在命令行模式下,命令没有引号,并且没有以卷规范、空格、 ,;=+开始,那么在 <space> , ;=的第一次出现时中断命令标记,并将其余部分添加到参数标记。
    • 如果命令标记的第2个字符是冒号,则验证可以找到由第1个字符指定的卷。
      如果找不到卷,则中止操作并出现错误。
    • 如果处于批处理模式并且命令标记以 :开始,那么转到7.4
      请注意,如果标签标记以 ::开头,那么将无法达到这个值,因为除非使用 SUBST 为 ::定义卷,否则前面的步骤将中止并出现错误。
    • 确定要执行的外部命令。
      • 这是一个复杂的过程,可能涉及当前卷、工作目录、 PATH 变量、 PATHEXT 变量和或文件关联。
      • 如果无法识别有效的外部命令,则以错误中止。
    • 如果在命令行模式下,命令标记以 :开头,那么转到7.4
      请注意,这种情况很少发生,因为除非命令标记以 ::开头,否则前面的步骤将中止,并且 SUBST 用于为 ::定义卷,并且整个命令标记是到外部命令的有效路径。
    • 7.3. exec -执行外部命令。
  • 7.4-忽略标签 -如果命令令牌以 :开头,则忽略命令及其所有参数。
    7.2和7.3中的规则可能会阻止标签达到这一点。

命令行解析器:

与 BatchLine-Parser 类似,除了:

第一阶段扩展百分比:

  • %*%1等参数扩展
  • 如果 var 未定义,则 %var%保持不变。
  • 没有对 %%的特殊处理。如果 var = content,则 %%var%%展开为 %content%

阶段3)回应已解析的命令

  • 这在阶段2之后不执行。它只在阶段4之后对 FORDO 命令块执行。

阶段5)延迟扩展: 只有当延迟扩展被启用

  • 如果 var 未定义,则 !var!保持不变。

第七阶段)执行命令

  • 尝试调用或 GOTO a: label 将导致错误。
  • 正如第7阶段中已经记录的那样,在不同的场景下,执行的标签可能会导致错误。
    • 批处理执行的标签只有在以 ::开头时才会导致错误
    • 执行的命令行标签几乎总是导致错误

整数值的解析

在许多不同的上下文中,cmd.exe 从字符串中解析整数值,而且规则是不一致的:

  • SET /A
  • IF
  • %var:~n,m%(变量子字符串扩展)
  • FOR /F "TOKENS=n"
  • FOR /F "SKIP=n"
  • FOR /L %%A in (n1 n2 n3)
  • EXIT [/B] n

有关这些规则的详细信息,请参阅 CMDEXE 解析数字的规则


对于任何希望改进 cmd.exe 解析规则的人,都有一个 论坛的讨论话题,可以在其中报告问题并提出建议。

希望能有帮助
Jan Erik (jeb)-阶段的原始作者和发现者
Dave Benham (dbenham)-更多附加内容和编辑

正如前面所指出的,命令是在 μSoft 中传递整个参数字符串的,它们可以将其解析为单独的参数以供自己使用。在不同的程序之间没有一致性,因此没有一套规则来描述这个过程。您确实需要检查程序使用的任何 C 库的每个角落用例。

至于系统 .bat文件,下面是这个测试:

c> type args.cmd
@echo off
echo cmdcmdline:[%cmdcmdline%]
echo 0:[%0]
echo *:[%*]
set allargs=%*
if not defined allargs goto :eof
setlocal
@rem Wot about a nice for loop?
@rem Then we are in the land of delayedexpansion, !n!, call, etc.
@rem Plays havoc with args like %t%, a"b etc. ugh!
set n=1
:loop
echo %n%:[%1]
set /a n+=1
shift
set param=%1
if defined param goto :loop
endlocal

现在我们可以运行一些测试,看看你能不能弄清楚 μSoft 到底想做什么:

C>args a b c
cmdcmdline:[cmd.exe ]
0:[args]
*:[a b c]
1:[a]
2:[b]
3:[c]

到目前为止还不错。(从现在开始,我将省略无趣的 %cmdcmdline%%0。)

C>args *.*
*:[*.*]
1:[*.*]

没有文件名扩展。

C>args "a b" c
*:["a b" c]
1:["a b"]
2:[c]

没有引号剥离,虽然引号确实可以防止参数分裂。

c>args ""a b" c
*:[""a b" c]
1:[""a]
2:[b" c]

连续的双引号会导致他们失去任何特殊的解析能力。@Beniot 的例子:

C>args "a """ b "" c"""
*:["a """ b "" c"""]
1:["a """]
2:[b]
3:[""]
4:[c"""]

测试: 如何将任何环境变量的值作为 单身参数(即 %1)传递给 bat 文件?

c>set t=a "b c
c>set t
t=a "b c
c>args %t%
1:[a]
2:["b c]
c>args "%t%"
1:["a "b]
2:[c"]
c>Aaaaaargh!

理智的解析似乎永远无法实现。

为了您的娱乐,尝试添加杂项 ^\'&(等)字符到这些例子。

百分比展开规则

下面是对 Jeb 的回答中第1阶段的详细解释(对于批处理模式和命令行模式都有效)。

第一阶段)扩展百分比 从左边开始,扫描每个字符的 %<LF>。如果找到的话

  • 1.05(在 <LF>截断线)
  • 如果字符是 <LF>,则
    • <LF>开始删除(忽略)行的其余部分
    • 后藤2.0阶段
  • 否则字符必须是 %,因此继续到1.1
  • 1.1(转义 %) < em > 跳过 if 命令行模式
  • 如果批处理模式,后面跟着另一个 %然后
    用单个 %代替 %%并继续扫描
  • 1.2(扩展参数) < em > 跳过 if 命令行模式
  • 否则,如果批处理模式然后
    • 如果后跟 *和命令扩展名,则启用
      用所有命令行参数的文本替换 %*(如果没有参数,则不替换任何参数) ,然后继续扫描。
    • 如果后面跟着 <digit>,那么
      %<digit>替换为参数值(如果未定义,则不替换任何值)并继续扫描。
    • 否则,如果后跟 ~,则启用命令扩展名
      • 如果后面跟着可选的有效参数修饰符列表,后面跟着必需的 <digit>,那么
        用修改过的参数值替换 %~[modifiers]<digit>(如果没有定义或指定 $PATH: 修饰符未定义,则不替换任何内容) ,然后继续扫描。
        注意: 修饰符不区分大小写,可以以任何顺序出现多次,除了 $PATH: 修饰符只能出现一次,并且必须是 <digit>之前的最后一个修饰符
      • 否则无效的修改参数语法将引发 致命错误: 如果处于批处理模式,所有解析命令都将中止,批处理将中止!
  • 1.3(扩展变量)
  • 否则,如果禁用命令扩展名,则
    查看下一个字符串,在 %之前或缓冲区结束之前中断,并将其称为 VAR (可能是一个空列表)
    • 如果下一个字符是 %,那么
      • 如果定义了 VAR,那么
        用 VAR 值替换 %VAR%并继续扫描
      • 否则,如果批处理模式然后
        删除 %VAR%并继续扫描
      • 其他的升到1.4
    • 其他的升到1.4
  • 否则,如果启用了命令扩展名
    查看下一个字符串,在 % :之前或缓冲区结束之前中断,并将其称为 VAR (可能是一个空列表)。如果 VAR 在 :之前中断,而后续字符是 %,那么将 :作为 VAR 中的最后一个字符并在 %之前中断。
    • 如果下一个字符是 %,那么
      • 如果定义了 VAR,那么
        用 VAR 值替换 %VAR%并继续扫描
      • 否则,如果批处理模式然后
        删除 %VAR%并继续扫描
      • 其他的升到1.4
    • 否则,如果下一个字符是 :那么
      • 如果 VAR 未定义,则
        • 如果批处理模式然后
          删除 %VAR:并继续扫描。
        • 其他的升到1.4
      • 否则,如果下一个字符是 ~那么
        • 如果下一个字符串与 [integer][,[integer]]%的模式匹配,则
          用值为 VAR 的子字符串替换 %VAR:~[integer][,[integer]]%(可能导致字符串为空)并继续扫描。
        • 其他的升到1.4
      • 否则,如果后面跟着 =*=那么
        无效的变量搜索和替换语法引发 < em > < strong > 致命错误: 所有已解析的命令都将中止,如果处于批处理模式,则批处理将中止!
      • 否则,如果下一个字符串匹配 [*]search=[replace]%的模式,其中搜索可能包括除 =之外的任何字符集,替换可能包括除 %之外的任何字符集,那么
        在执行搜索和替换(可能导致空字符串)之后,用 VAR 值替换 %VAR:[*]search=[replace]%并继续扫描
      • 其他的升到1.4
  • 1.4(带%)
    • 那么如果批处理模式
      删除 %并继续扫描,从 %之后的下一个字符开始
    • 否则保留前导 %并继续扫描,从保留前导 %后的下一个字符开始

以上解释了为什么这一批

@echo off
setlocal enableDelayedExpansion
set "1var=varA"
set "~f1var=varB"
call :test "arg1"
exit /b
::
:test "arg1"
echo %%1var%% = %1var%
echo ^^^!1var^^^! = !1var!
echo --------
echo %%~f1var%% = %~f1var%
echo ^^^!~f1var^^^! = !~f1var!
exit /b

给出以下结果:

%1var% = "arg1"var
!1var! = varA
--------
%~f1var% = P:\arg1var
!~f1var! = varB

注1 -第1阶段发生在识别 REM 语句之前。这一点非常重要,因为它意味着如果注释的参数展开语法无效或变量搜索和替换语法无效,那么即使是注释也可能产生致命错误!

@echo off
rem %~x This generates a fatal argument expansion error
echo this line is never reached

注意2 -% 解析规则的另一个有趣的结果: 可以定义名称中包含: 的变量,但是除非禁用命令扩展,否则不能展开它们。有一个例外-在启用命令扩展时,在末尾包含单个冒号的变量名可以展开。但是,不能对以冒号结尾的变量名执行子字符串或搜索和替换操作。下面的批处理文件(由 jeb 提供)演示了这种行为

@echo off
setlocal
set var=content
set var:=Special
set var::=double colon
set var:~0,2=tricky
set var::~0,2=unfortunate
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%
echo Now with DisableExtensions
setlocal DisableExtensions
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%

注3 -jeb 在他的文章中列出的解析规则顺序的一个有趣的结果: 当执行 find 并用延迟展开替换时,find 和 place 术语中的特殊字符必须转义或引用。但是百分比展开的情况不同——查找项不能转义(尽管它可以被引用)。根据您的意图,百分比替换字符串可能需要也可能不需要转义或引号。

@echo off
setlocal enableDelayedExpansion
set "var=this & that"
echo %var:&=and%
echo "%var:&=and%"
echo !var:^&=and!
echo "!var:&=and!"

延迟展开规则

下面是对 Jeb 的回答中第5阶段的更详细、更准确的解释(对于批处理模式和命令行模式都有效)

第五阶段)延迟扩展

如果应用以下任何条件,则跳过此阶段:

  • 延迟扩展被禁用。
  • 该命令位于管道两侧的括号内。
  • 传入的命令标记是一个“裸”批处理脚本,这意味着它不与 CALL、括号内的块、任何形式的命令串联(&&&||)或管道 |相关联。

延迟扩展过程是独立地应用于令牌的。一个命令可以有多个令牌:

  • 令牌。对于大多数命令,命令名本身是一个标记。但是一些命令具有专门的区域,这些区域被认为是阶段5的 TOKEN。
    • for ... in(TOKEN) do
    • if defined TOKEN
    • if exists TOKEN
    • if errorlevel TOKEN
    • if cmdextversion TOKEN
    • if TOKEN comparison TOKEN,其中比较是 ==equneqlssleqgtrgeq之一
  • 参数标记
  • 重定向的目标标记(每个重定向一个)

不更改不包含 !的令牌。

对于每个包含至少一个 !的令牌,从左到右扫描每个字符以查找 ^!,如果找到,则

  • 5.1(插入符号转义) !^文字所需
  • 如果字符是一个插入符号 ^那么
    • 移除 ^
    • 扫描下一个字符并将其保留为文字
    • 继续扫描
  • 5.2(展开变量)
  • 如果字符是 !,则
    • 如果禁用命令扩展名,则
      查看下一个字符串,在 !<LF>之前中断,并将其称为 VAR (可能是一个空列表)
      • 如果下一个字符是 !,那么
        • 如果定义了 VAR,则
          用 VAR 值替换 !VAR!并继续扫描
        • 否则,如果批处理模式然后
          删除 !VAR!并继续扫描
        • 其他选择5.2.1
      • 其他选择5.2.1
    • 否则,如果启用了命令扩展名
      查看下一个字符串,在 !:<LF>之前中断,并将其称为 VAR (可能是一个空列表)。如果 VAR 在 :之前中断,而后续字符是 !,那么将 :作为 VAR 中的最后一个字符并在 !之前中断
      • 如果下一个字符是 !,那么
        • 如果 VAR 存在,那么
          用 VAR 值替换 !VAR!并继续扫描
        • 否则,如果批处理模式然后
          删除 !VAR!并继续扫描
        • 其他选择5.2.1
      • 否则,如果下一个字符是 :那么
        • 如果 VAR 未定义,则
          • 如果批处理模式然后
            删除 !VAR:并继续扫描
          • 其他选择5.2.1
        • 否则,如果下一个字符是 ~那么
          • 如果下一个字符串匹配 [integer][,[integer]]!的模式,那么用值为 VAR 的子字符串替换 !VAR:~[integer][,[integer]]!(可能导致空字符串)并继续扫描。
          • 其他选择5.2.1
        • 否则,如果下一个字符串匹配 [*]search=[replace]!的模式,其中搜索可能包括除 =之外的任何字符集,替换可能包括除 !之外的任何字符集,那么
          在执行搜索和替换(可能导致一个空字符串)之后,用 VAR 值替换 !VAR:[*]search=[replace]!并继续扫描
        • 其他选择5.2.1
      • 其他选择5.2.1
    • 5.2.1
      • 如果批处理模式,则删除前导 !
        否则保留领先的 !
      • 从保留的前导 !之后的下一个字符开始,继续扫描

你已经有了一些很棒的答案,但是要回答你问题的一部分:

set a =b, echo %a %b% c% → bb c%

这里发生的情况是,因为在 = 之前有一个空格,所以创建了一个名为 %a<space>%的变量 所以当你的 echo %a %被正确的评估为 b

其余部分的 b% c%然后计算为纯文本 + 一个未定义的变量 % c%,它应该回显为类型,对我来说,echo %a %b% c%返回 bb% c%

我怀疑在变量名中包含空格的能力更多的是一个疏忽,而不是一个计划好的“特性”

请注意,微软已经发布了其终端的源代码。在语法分析方面,它的工作方式类似于命令行。也许有人对根据终端的解析规则测试逆向工程的解析规则感兴趣。

链接 到源代码。

循环元变量展开

这是 接受的答案第四阶段的扩展说明(同时适用于批处理文件模式和命令行模式)。当然,for命令必须是活动的。下面描述了在 do子句之后命令行部分的处理。注意,在批处理文件模式下,由于前面提到的即时 %扩展阶段(第一阶段)) ,%%已经转换为 %

  • 扫描 %符号,从左边开始一直到行尾; 如果找到一个,那么:
    • 如果启用了 命令扩展(默认) ,检查下一个字符是否为 ~; 如果是,则:
      • 在不区分大小写的集合 fdpnxsatz中,在定义 for变量引用或 $符号的字符之前尽可能多地采用以下字符(每个字符甚至多次) ; 如果遇到这样的 $符号,则:
        • 扫描一个 :1; 如果找到,那么:
          • 如果在 :后面有一个字符,使用它作为 for变量引用并按预期展开,除非它没有定义,否则不要展开并在该字符位置继续扫描;
          • 如果 :是最后一个字符,则为 cmd.exe会崩溃!
        • Else (没有找到 :)不要展开任何东西;
      • Else (如果没有遇到 $符号)使用所有修饰符展开 for变量,除非它没有定义,那么不要展开并继续扫描该字符位置;
    • Else (如果没有找到 ~或禁用命令扩展)检查下一个字符:
      • 如果没有更多的字符可用,不要扩展任何东西;
      • 如果下一个字符是 %,不要展开任何内容,回到这个字符位置 2的扫描开始位置;
      • 否则使用下一个字符作为 for变量引用并展开,除非这样没有定义,否则不要展开;
  • 在下一个字符位置返回到扫描的开始(只要还有字符可用) ;

1)在 $:之间的字符串被认为是一个环境变量的名字,甚至可能是空的,因为一个环境变量不能有一个空的名字,这种行为就像一个未定义的环境变量一样。
2)这意味着一个名为 %for元变量不能在没有 ~修饰符的情况下展开。


来源: 如何安全地回显 FOR 变量%% ~ p 后跟字符串文字