165

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

但是,对于批处理脚本的语法,事物如何扩展或不扩展,以及如何对事物进行转义,我一直找不到很好的解释。

以下是我无法解决的示例问题:

  • 报价系统如何管理?我制作了一个TinyPerl脚本
    foreach $i (@ARGV) { print '*' . $i ; }),编译它并以这种方式调用它:
    • my_script.exe "a ""b"" c"→ 输出是 *a "b*c
    • my_script.exe """a b c"""→ 输出*"a*b*c"
  • 内部echo命令如何工作?该命令内部扩展了什么?
  • 为什么我必须for [...] %%I在文件脚本中使用,但for [...] %I在交互式会话中?
  • 什么是转义字符,在什么上下文中?如何逃避百分号?例如,我怎样才能%PROCESSOR_ARCHITECTURE%从字面上回应?我发现这echo.exe %""PROCESSOR_ARCHITECTURE%行得通,有更好的解决方案吗?
  • 双如何%搭配?例子:
    • set b=a, echo %a %b% c%%a a c%
    • set a =b, echo %a %b% c%bb% c%
  • 如果该变量包含双引号,如何确保变量作为单个参数传递给命令?
  • set使用命令时变量如何存储?例如,如果我这样做set a=a" b,然后echo.%a%我获得a" b. 但是,如果我echo.exe从 UnxUtils 使用,我会得到a b. 如何%a%以不同的方式扩展?

谢谢你的灯。

4

8 回答 8

244

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

批处理行解析器:

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

阶段 0) 读取线:

阶段 1) 扩张百分比:

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

阶段 3)仅当命令块不以 开头@且 ECHO 在上一步开始时为 ON 时才回显已解析的命令。

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

阶段 5) 延迟扩展:仅在启用延迟扩展的情况下

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

阶段 5.5) 执行重定向:

阶段 6) CALL 处理/插入符号加倍:仅当命令令牌为 CALL 时

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


以下是每个阶段的详细信息:

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

阶段 0) 读取行:通过 first 读取输入行<LF>

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

阶段 1) 扩张百分比:

  • 双倍%%被单倍取代%
  • 参数的扩展(%*, %1, %2, 等)
  • 的扩展%var%,如果 var 不存在,则将其替换为空
  • 行首先被截断,<LF>不在%var%扩展范围内
  • 要获得完整的解释,请阅读 dbenham 的前半部分同一线程:Percent Phase

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

在这个阶段,有些概念很重要。

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

根据上下文,以下字符在此阶段可能具有特殊含义:<CR> ^ ( @ & | < > <LF> <space> <tab> ; , = <0x0B> <0x0C> <0xFF>

从左到右查看每个字符:

  • 如果<CR>然后将其删除,就好像它从未存在过一样(奇怪的重定向行为除外)
  • 如果是插入符号 ( ^),则转义下一个字符,并删除转义的插入符号。转义字符失去所有特殊含义(除了<LF>)。
  • 如果是引号 ( "),则切换引号标志。如果报价标志处于活动状态,则只有"<LF>是特殊的。在下一个引号将引号标志关闭之前,所有其他字符都会失去其特殊含义。无法逃避结束报价。所有带引号的字符始终在同一个标​​记内。
  • <LF>总是关闭引用标志。其他行为因上下文而异,但引号永远不会改变<LF>.
    • 逃脱<LF>
      • <LF>被剥离
      • 下一个字符被转义。如果在行缓冲区的末尾,则下一行由阶段 1 和 1.5 读取和处理,并在转义下一个字符之前附加到当前行。如果下一个字符是<LF>,则将其视为文字,这意味着此过程不是递归的。
    • 未转义<LF>不在括号内
      • <LF>被剥离并终止对当前行的解析。
      • 行缓冲区中的任何剩余字符都将被忽略。
    • <LF>在 FOR IN 带括号的块中 未转义
      • <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 迭代命令
        • 所有 FOR 选项都在第 2 阶段完全解析。
        • IN 带括号的子句<LF>视为<space>. 解析 IN 子句后,将所有标记连接在一起以形成单个标记。
        • 连续的未转义/未加引号的标记分隔符在整个 FOR 命令中通过 DO 折叠成一个空格。
      • DO 之后的部分是正常解析的命令块。DO 命令块的后续处理由阶段 7 中的迭代控制。
    • 在阶段 2 中检测到的 REM 的处理方式与所有其他命令截然不同。
      • 仅解析一个参数标记 - 解析器忽略第一个参数标记之后的字符。
      • REM 命令可能出现在第 3 阶段的输出中,但该命令从未执行,并且原始参数文本被回显 - 转义的插入符号不会被删除,除了...
        • 如果只有一个以非转义^结尾的参数标记结束该行,则丢弃该参数标记,并解析后续行并将其附加到 REM。重复此操作,直到有多个标记,或者最后一个字符不是^
  • 如果命令令牌以 开头:,并且这是阶段 2 的第一轮(由于阶段 6 中的 CALL 而不是重新启动),那么
    • 令牌通常被视为Unexecuted Label
      • 该行的其余部分已被解析,但是, ), <,>不再具有特殊含义。该行的整个剩余部分被认为是标签“命令”的一部分。&|
      • 继续是特殊的^,这意味着可以使用续行将后续行附加到标签。
      • 带括号的块中的未执行标签将导致致命的语法错误,除非它紧跟在下一行 的命令或执行标签之后。
        • (Unexecuted Label之后的第一个命令不再具有特殊含义。
      • 标签解析完成后,该命令中止。标签不会发生后续阶段
    • 有三个例外情况会导致在阶段 2 中找到的标签被视为在阶段 7 继续解析的执行标签
      • 在标签标记之前有重定向,并且行上有|管道或&、、&&||命令连接。
      • 在标签标记之前有重定向,并且命令位于带括号的块内。
      • 标签标记是括号内的一行中的第一个命令,上面的行以Unexecuted Label结束。
    • 在第 2 阶段发现 执行标签时会发生以下情况
      • 标签、它的参数和它的重定向都被排除在第 3 阶段的任何回显输出之外
      • 该行上的任何后续连接命令都将被完全解析并执行。
    • 有关已执行标签执行标签的更多信息,请参阅https://www.dostips.com/forum/viewtopic.php?f=3&t=3803&p=55405#p55405

阶段 3)仅当命令块不以 开头@且 ECHO 在上一步开始时为 ON 时才回显已解析的命令。

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

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

---- 从现在开始,阶段 2 中确定的每个命令都将单独处理。
---- 完成一个命令的阶段 5 到 7,然后再进行下一个命令。

阶段 5) 延迟扩展:仅当延迟扩展打开时,命令不在管道两侧的带括号的块中,并且命令不是“裸”批处理脚本(脚本名称不带括号,CALL,命令连接,或管道)。

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

阶段 5.3) 管道处理:仅当命令位于管道的任一侧时,管道的
每一侧都被独立且异步地处理。

  • 如果 command 是 cmd.exe 内部的,或者它是一个批处理文件,或者它是一个带括号的命令块,那么它会在一个新的 cmd.exe 线程中通过 执行%comspec% /S /D /c" commandBlock",因此命令块会重新启动,但是这次在命令行模式下。
    • 如果是带括号的命令块,则所有<LF>带有命令的前后都将转换为<space>&. 其他<LF>被剥离。
  • 管道命令的处理到此结束。
  • 请参阅为什么在管道代码块中延迟扩展失败?有关管道解析和处理的更多信息

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

阶段 6) CALL 处理/插入符号加倍:仅当命令标记是 CALL,或者如果第一个出现的标准标记分隔符之前的文本是 CALL。如果 CALL 是从更大的命令标记中解析出来的,那么在继续之前,未使用的部分会被添加到参数标记之前。

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

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

  • 7.1 - 执行内部命令- 如果命令标记被引用,则跳过此步骤。否则,尝试解析出内部命令并执行。
    • 进行以下测试以确定未引用的命令标记是否代表内部命令:
      • 如果命令令牌与内部命令完全匹配,则执行它。
      • 否则在第一次出现+ / [ ] <space> <tab> , ;或之前中断命令标记=
        如果前面的文本是内部命令,则记住该命令
        • 如果在命令行模式下,或者如果命令来自带括号的块,IF真或假命令块,FOR DO命令块,或涉及命令串联,则执行内部命令
        • 否则(必须是批处理模式下的独立命令)扫描当前文件夹和 PATH 以查找基本名称与原始命令令牌匹配的 .COM、.EXE、.BAT 或 .CMD 文件
          • 如果第一个匹配文件是 .BAT 或 .CMD,则转到 7.3.exec 并执行该脚本
          • 否则(未找到匹配项或第一个匹配项是 .EXE 或 .COM)执行记住的内部命令
      • 否则在第一次出现. \或之前中断命令标记:
        如果前面的文本不是内部命令,则转到 7.2
        否则前面的文本可能是内部命令。记住这个命令。
      • 在第一次出现+ / [ ] <space> <tab> , ;或之前断开命令标记=
        如果前面的文本是现有文件的路径,则转到 7.2
        否则执行记住的内部命令。
    • 如果从较大的命令令牌中解析内部命令,则命令令牌的未使用部分包含在参数列表中
    • 仅仅因为命令令牌被解析为内部命令并不意味着它会成功执行。每个内部命令对于如何解析参数和选项以及允许使用什么语法都有自己的规则。
    • /?如果检测到,所有内部命令都将打印帮助而不是执行其功能。大多数人都知道/?它是否出现在论点的任何地方。但是像 ECHO 和 SET 这样的一些命令只有在第一个参数标记以/?.
    • SET 有一些有趣的语义:
      • 如果 SET 命令在启用变量名和扩展名之前有一个引号
        set "name=content" ignored --> value=content
        则第一个等号和最后一个引号之间的文本将用作内容(第一个等号和最后一个引号除外)。最后一个引号之后的文本被忽略。如果等号后没有引号,则该行的其余部分用作内容。

      • set name="content" not ignored 如果 SET 命令在 name --> value=之前没有引号,"content" not ignored
        则将 equal 之后的行的整个其余部分用作内容,包括可能存在的任何和所有引号。
    • 评估 IF 比较,并根据条件是真还是假,从阶段 5 开始处理适当的已解析依赖命令块。
    • FOR 命令的 IN 子句被适当地迭代。
      • 如果这是一个迭代命令块输出的 FOR /F,那么:
        • IN 子句通过 CMD /C 在新的 cmd.exe 进程中执行。
        • 命令块必须第二次经历整个解析过程,但这次是在命令行上下文中
        • ECHO 将启动,延迟扩展通常会启动禁用(取决于注册表设置)
        • 一旦子 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 都接受源路径和目标路径的通配符。但是微软在记录通配符的工作方式方面做得很糟糕,特别是对于目标路径。可以在Windows RENAME 命令如何解释通配符中找到一组有用的通配符规则?
  • 7.2 - 执行音量更改- 否则,如果命令标记不以引号开头,正好是两个字符,并且第二个字符是冒号,则更改音量
    • 所有参数标记都被忽略
    • 如果找不到第一个字符指定的卷,则中止并出现错误
    • 除非使用 SUBST为定义卷,否则命令标记 of将::始终导致错误::
      ::
  • 7.3 - 执行外部命令- 否则尝试将命令视为外部命令。
    • 如果在命令行模式下并且命令没有被引用并且不以卷规范开头,则空格、,,;或者=然后+在第一次出现<space> , ;or时中断命令标记,=并将其余部分添加到参数标记之前。
    • 如果命令标记的第 2 个字符是冒号,则验证第 1 个字符指定的卷是否可以找到。
      如果找不到该卷,则中止并出现错误。
    • 如果在批处理模式下并且命令标记以 开头:,则转到 7.4
      请注意,如果标签标记以 开头::,则不会达到此要求,因为前面的步骤将因错误而中止,除非使用 SUBST 为 定义卷::
    • 确定要执行的外部命令。
      • 这是一个复杂的过程,可能涉及当前卷、当前目录、PATH 变量、PATHEXT 变量和/或文件关联。
      • 如果无法识别有效的外部命令,则中止并出现错误。
    • 如果在命令行模式并且命令标记以 开头:,则转到 7.4
      请注意,这很少会达到,因为除非命令标记以 开头,否则前面的步骤将中止并出现错误::,并且 SUBST 用于定义卷::,并且整个命令令牌是外部命令的有效路径。
    • 7.3.exec - 执行外部命令。
  • 7.4 - 忽略标签- 如果命令标记以 . 开头,则忽略命令及其所有参数:
    7.2 和 7.3 中的规则可能会阻止标签到达这一点。

命令行解析器:

像 BatchLine-Parser 一样工作,除了:

阶段 1) 扩张百分比:

  • No%*%1参数扩展
  • 如果 var 未定义,则%var%保持不变。
  • 无特殊处理%%。如果 var=content,则%%var%%扩展为%content%.

阶段 3) 回显解析的命令

  • 这不会在阶段 2 之后执行。它仅在 FOR DO 命令块的阶段 4 之后执行。

阶段 5) 延迟扩展:仅在启用 DelayedExpansion 时

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

阶段 7) 执行命令

  • 尝试 CALL 或 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

这些规则的详细信息可以在CMD.EXE 如何解析数字的规则中找到


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

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

于 2010-11-04T09:04:30.513 回答
63

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

如果这听起来像无政府状态,那就是。但是,由于大量 Windows 程序确实使用 Microsoft C/C++ 运行时的,因此了解MSVCRT 如何标记参数argv通常很有用。这是一段摘录:

  • 参数由空格分隔,空格可以是空格,也可以是制表符。
  • 用双引号括起来的字符串被解释为单个参数,无论其中包含什么空格。带引号的字符串可以嵌入到参数中。请注意,插入符号 (^) 不会被识别为转义字符或分隔符。
  • 前面有反斜杠 \" 的双引号被解释为文字双引号 (")。
  • 反斜杠按字面意思解释,除非它们紧跟在双引号之前。
  • 如果偶数个反斜杠后跟双引号,则每对反斜杠 (\) 在 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] 始终是可执行文件的名称,为简洁起见,以下省略。在 Windows XP SP3 上测试。使用 Visual Studio 2005 编译。)

> 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]
于 2010-11-04T08:26:46.733 回答
51

#Percent 扩展规则这是jeb 回答中第 1 阶段的扩展解释(对批处理模式和命令行模式都有效)。

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

  • 1.05(在 处截断行<LF>
  • 如果字符是<LF>那么
    • 从后面删除(忽略)该行的其余<LF>部分
    • 转到阶段 2.0
  • 否则字符必须是%,所以继续 1.1
  • 1.1 (escape %) 如果命令行模式跳过
  • 如果批处理模式,然后是另一个,%
    替换%%为单个%并继续扫描
  • 1.2(扩展参数) 如果命令行模式跳过
  • 否则,如果是批处理模式,那么
    • 如果随后*启用命令扩展,则
      替换%*为所有命令行参数的文本(如果没有参数,则替换为空)并继续扫描。
    • 否则,如果后跟<digit>then
      替换%<digit>为参数值(如果未定义,则替换为空)并继续扫描。
    • 否则,如果后跟~和命令扩展被启用,则
      • 如果后面是可选的有效参数修饰符列表,然后是必需的,<digit>
        替换%~[modifiers]<digit>为修改后的参数值(如果未定义或指定 $PATH: 未定义修饰符,则替换为空)并继续扫描。
        注意:修饰符不区分大小写,可以以任意顺序出现多次,除了 $PATH: 修饰符只能出现一次,并且必须是最后一个修饰符之前<digit>
      • 其他无效的修改参数语法会引发致命错误:所有已解析的命令都被中止,如果在批处理模式下,批处理将中止!
  • 1.3(展开变量)
  • 否则,如果命令扩展被禁用,则
    查看下一个字符串,在%缓冲区之前或结尾中断,并将它们称为 VAR(可能是一个空列表)
    • 如果下一个字符是%then
      • 如果定义了 VAR,则
        替换%VAR%为 VAR 的值并继续扫描
      • 否则,如果是批处理模式,则
        删除%VAR%并继续扫描
      • 否则转到 1.4
    • 否则转到 1.4
  • 否则,如果启用了命令扩展,则
    查看下一个字符串,在% :缓冲区之前或结尾处中断,并将它们称为 VAR(可能是一个空列表)。如果 VAR 在之前中断:并且后续字符%:作为最后一个字符包含在 VAR 中并在之前中断%
    • 如果下一个字符是%then
      • 如果定义了 VAR,则
        替换%VAR%为 VAR 的值并继续扫描
      • 否则,如果是批处理模式,则
        删除%VAR%并继续扫描
      • 否则转到 1.4
    • 否则,如果下一个字符是:then
      • 如果 VAR 未定义,则
        • 如果是批处理模式,则
          删除%VAR:并继续扫描。
        • 否则转到 1.4
      • 否则,如果下一个字符是~then
        • 如果下一个字符串与 的模式匹配,[integer][,[integer]]%则用 VAR 值的子字符串
          替换%VAR:~[integer][,[integer]]%(可能导致空字符串)并继续扫描。
        • 否则转到 1.4
      • 否则,如果后面跟着=or *=then
        无效的变量搜索和替换语法会引发致命错误:所有已解析的命令都被中止,如果在批处理模式下,批处理中止!
      • 否则,如果下一个字符串与 的模式匹配[*]search=[replace]%,其中 search 可以包括除 之外的任何字符集=,并且 replace 可以包括除 之外的任何字符集%,然后在执行搜索和
        替换%VAR:[*]search=[replace]%后用 VAR 的值替换(可能导致空字符串)并继续扫描
      • 否则转到 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 项不能被转义(尽管它可以被引用)。百分比替换字符串可能需要也可能不需要转义或引号,具体取决于您的意图。

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

#Delayed Expansion Rules Here是jeb答案中第5阶段的扩展和更准确的解释(对批处理模式和命令行模式都有效)

阶段 5) 延迟扩张

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

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

延迟扩展过程独立应用于令牌。一个命令可能有多个标记:

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

不包含!.

对于每个确实包含至少一个 的标记,!从左到右扫描每个字符以查找^or !,如果找到,则

  • 5.1(插入符号转义)需要!^文字
  • 如果字符是插入符号^,那么
    • 去除^
    • 扫描下一个字符并将其保存为文字
    • 继续扫描
  • 5.2(展开变量)
  • 如果字符是!,那么
    • 如果命令扩展被禁用,则
      查看下一个字符串,在!or之前中断<LF>,并将它们称为 VAR(可能是一个空列表)
      • 如果下一个字符是!then
        • 如果定义了 VAR,则
          替换!VAR!为 VAR 的值并继续扫描
        • 否则,如果是批处理模式,则
          删除!VAR!并继续扫描
        • 否则转到 5.2.1
      • 否则转到 5.2.1
    • 否则,如果启用了命令扩展,则
      查看下一个字符串,在 、 或 之前中断!:并将<LF>它们称为 VAR(可能是一个空列表)。如果 VAR 在之前中断:并且后续字符!:作为最后一个字符包含在 VAR 中并在之前中断!
      • 如果下一个字符是!then
        • 如果 VAR 存在,则
          替换!VAR!为 VAR 的值并继续扫描
        • 否则,如果是批处理模式,则
          删除!VAR!并继续扫描
        • 否则转到 5.2.1
      • 否则,如果下一个字符是:then
        • 如果 VAR 未定义,则
          • 如果是批处理模式,则
            删除!VAR:并继续扫描
          • 否则转到 5.2.1
        • 否则,如果下一个字符是~then
          • 如果下一个字符串与 的模式匹配,[integer][,[integer]]!则用 VAR 值的子字符串替换!VAR:~[integer][,[integer]]!(可能导致空字符串)并继续扫描。
          • 否则转到 5.2.1
        • 否则,如果下一个字符串与 的模式匹配[*]search=[replace]!,其中 search 可以包括除 之外的任何字符集=,并且 replace 可以包括除 之外的任何字符集!,然后在执行搜索和
          替换!VAR:[*]search=[replace]!后用 VAR 的值替换(可能导致空字符串)和继续扫描
        • 否则转到 5.2.1
      • 否则转到 5.2.1
    • 5.2.1
      • 如果是批处理模式,则删除前导!
        否则保留前导!
      • 从保留的前导后的下一个字符开始继续扫描!
于 2011-11-01T18:16:11.330 回答
8

正如所指出的,命令在 μ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"""]

测验:如何将任何环境变量的值作为单个参数(即 as %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!

理智的解析似乎永远被打破了。

为了您的娱乐,请尝试在这些示例中添加杂项^, \, ', &(&c.) 字符。

于 2011-01-17T15:58:14.297 回答
5

您在上面已经有了一些很好的答案,但要回答您问题的一部分:

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

那里发生的情况是,因为在 = 之前有一个空格,%a<space>% 所以当您echo %a %正确评估为时,会创建一个变量,因此调用该变量b

然后将剩余部分b% c%评估为纯文本 + 一个未定义的变量% c%,它应该作为类型回显,因为我echo %a %b% c%返回bb% c%

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

于 2014-08-11T21:01:13.023 回答
3

FOR-循环元变量扩展

这是已接受答案第 4 阶段的扩展解释(适用于批处理文件模式和命令行模式)。当然,命令必须处于活动状态。下面描述子句后命令行部分的处理。请注意,在批处理文件模式下,由于前面的立即扩展阶段(阶段 1) ,已经转换为。fordo%%%%

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

1)和之间的字符串$:认为是环境变量的名称,甚至可以为空;由于环境变量不能有空名称,因此行为与未定义的环境变量相同。
2) 这意味着没有-修饰符就不能扩展for命名的元变量。%~


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

于 2020-12-10T22:19:52.970 回答
0

edit: see accepted answer, what follows is wrong and explains only how to pass a command line to TinyPerl.


Regarding quotes, I have the feeling that the behaviour is the following:

  • when a " is found, string globbing begins
  • when string globbing occurs:
    • every character that is not a " is globbed
    • when a " is found:
      • if it is followed by "" (thus a triple ") then a double quote is added to the string
      • if it is followed by " (thus a double ") then a double quote is added to the string and string globbing ends
      • if the next character is not ", string globbing ends
    • when line ends, string globbing ends.

In short:

"a """ b "" c""" consists of two strings: a " b " and c"

"a"", "a""" and"a"""" are all the same string if at the end of a line

于 2010-11-04T08:06:42.327 回答
-2

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

链接到源代码。

于 2020-06-08T18:00:52.043 回答