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


142

我遇到了ss64.com,它提供了有关如何编写Windows Command Interpreter将运行的批处理脚本的良好帮助。

但是,我无法找到关于批处理脚本的语法,事物如何扩展或不扩展以及如何逃避事物的很好的解释。

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

  • 报价系统如何管理?我制作了一个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=aecho %a %b% c%%a a c%
    • set a =becho %a %b% c%bb c%
  • 如果该变量包含双引号,如何确保该变量作为单个参数传递给命令?
  • 使用set命令时如何存储变量?例如,如果我这样做set a=a" b,则echo.%a%获得a" b。但是,如果我echo.exe从UnxUtils 使用,我会得到a b。如何%a%以不同的方式扩展?

谢谢你的灯。


Answers:


200

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

批处理行解析器:

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

阶段0)读取行:

第1阶段)扩展百分比:

阶段2)处理特殊字符,标记化,并构建缓存的命令块:这是一个复杂的过程,受诸如引号,特殊字符,标记定界符和插入符转义符的影响。

阶段3)仅在命令块未以开头@且回显在上一步开始时为ON的情况下,回显已解析的命令

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

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

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

阶段5.5)执行重定向:

阶段6)CALL处理/ Caret加倍:仅当命令令牌为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阅读本上半年同一个线程:百分比相

阶段2)处理特殊字符,标记化,并构建缓存的命令块:这是一个复杂的过程,受诸如引号,特殊字符,标记定界符和插入符转义符的影响。接下来是该过程的近似过程。

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

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

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

从左到右查看每个字符:

  • 如果<CR>然后将其删除,就好像它从未出现过一样(怪异的重定向行为除外)
  • 如果是尖号(^),则转义下一个字符,并删除转义的尖号。转义字符失去所有特殊含义(除外<LF>)。
  • 如果是引号("),请切换引号标志。如果quote标志是活动的,则仅"<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的实际命令
        • 在阶段2中,所有IF选项都已完全解析。
        • 连续的令牌定界符会折叠成一个空格。
        • 根据比较运算符,将标识一个或两个值令牌。
      • True命令块是条件之后的一组命令,并且像其他任何命令块一样进行解析。如果要使用ELSE,则必须对True块加上括号。
      • 可选的False命令块是ELSE之后的命令集。同样,此命令块将正常解析。
      • True和False命令块不会自动进入后续阶段。他们的后续处理由阶段7控制。
    • DO在DO之后被一分为二。FOR构造中的语法错误将导致致命的语法错误。
      • 通过DO的部分是实际的FOR迭代命令,该命令一直贯穿阶段7
        • 在阶段2中,将完全解析所有FOR选项。
        • 中括号条款对待<LF><space>。解析IN子句后,所有令牌都串联在一起形成单个令牌。
        • 连续的未转义/不加引号的令牌定界符在整个DO命令中通过DO折叠为一个空格。
      • DO之后的部分是正常解析的命令块。DO命令块的后续处理由阶段7中的迭代控制。
    • 在阶段2中检测到的REM与所有其他命令的处理方式截然不同。
      • 仅解析一个参数令牌-解析器将忽略第一个参数令牌之后的字符。
      • REM命令可能会出现在第3阶段的输出中,但是该命令从不执行,并且回显了原始参数文本-不会删除转义的插入号,除非...
        • 如果只有一个参数标记以未转义的^结尾而结束,则该标记结束了该行,则该参数标记将被丢弃,然后分析下一行并将其附加到REM。重复此过程,直到有多个令牌,或者最后一个字符不是为止^
  • 如果命令令牌以开头:,并且这是阶段2的第一轮(由于阶段6中的CALL而没有重启),则
    • 令牌通常被视为未执行标签
      • 该行的剩余部分进行解析,但是)<>&|不再有特殊的意义。该行的其余部分被认为是标签“命令”的一部分。
      • ^继续是特殊的,这意味着续行可用于追加下一行的标签。
      • 括号块中的未执行标签将导致致命的语法错误,除非在下一行立即跟随命令或执行标签
        • (对于跟随未执行标签的第一个命令不再具有特殊含义。
      • 标签解析完成后,该命令将中止。标签不会进行后续阶段
    • 共有三种例外情况,可能导致在阶段2中找到的标签被视为“已执行标签”,该标签将继续解析到阶段7。
      • 有重定向先于标签的道理,并有一个|管道或&&&||在命令行的级联。
      • 标签标记之前有重定向,该命令在括号内。
      • 标签令牌是带括号的块中一行上的第一个命令,而上面的行以Unexecuted Label结尾。
    • 在阶段2中发现 执行标签时,会发生以下情况
      • 标签,其参数及其重定向都从阶段3的任何回显输出中排除
      • 该行上的所有后续串联命令都将完全解析并执行。
    • 有关“已执行标签”与“ 执行标签”的更多信息,请参见https://www.dostips.com/forum/viewtopic.php?f=3&t=3803&p=55405#p55405

阶段3)仅在命令块未以开头@且回显在上一步开始时为ON的情况下,回显已解析的命令

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

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

----从现在开始,将分别处理阶段2中标识的每个命令。
----一个命令的第5到第7阶段已完成,然后继续执行下一个命令。

阶段5)延迟扩展:仅当延迟扩展处于启用状态时,该命令才不在管道两侧的括号的块中,并且该命令不是“裸”批处理脚本(脚本名称不带括号,CALL,命令串联,或管道)。

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

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

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

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

阶段6)CALL处理/ Caret加倍:仅当命令令牌为CALL或第一个出现的标准令牌定界符之前的文本为CALL时。如果从较大的命令令牌解析了CALL,则在继续操作之前,未使用的部分将被放在参数令牌之前。

  • 扫描参数令牌以查找未加引号的/?。如果在令牌内的任何地方找到,请中止阶段6并进入阶段7,在该阶段将打印“呼叫帮助”。
  • 删除第一个CALL,以便可以堆叠多个CALL
  • 将所有插入符加倍
  • 重新启动阶段1、1.5和2,但不要继续进行阶段3
    • 只要不加引号,任何加倍的插入号都会还原为一个插入号。但是不幸的是,引用的插入号仍然翻倍。
    • 第一阶段有些变化
      • 步骤1.2或1.3中的扩展错误中止了CALL,但该错误不是致命的-批处理继续进行。
    • 第2阶段的任务有所更改
      • 在阶段2的第一轮中未检测到的任何新出现的未加引号且未转义的重定向都被检测到,但是在没有实际执行重定向的情况下将其删除(包括文件名)
      • 在行的末尾删除任何新出现的未引用,未转义的插入号,而无需执行行继续
      • 如果检测到以下任何一项,则CALL将中止且没有错误
        • 新出现的未加引号,未转义&|
        • 结果命令令牌以未加引号,未转义的开头 (
        • 删除的CALL之后的第一个令牌始于 @
      • 如果产生的命令是一个看似有效IF或FOR,然后执行随后将失败,并指出错误IFFOR不被识别为一个内部或外部的命令。
      • 当然,如果结果命令令牌是以开头的标签,则在阶段2的第二轮中CALL不会中止:
  • 如果生成的命令令牌是CALL,则重新启动阶段6(重复直到不再有CALL)
  • 如果生成的命令令牌是批处理脚本或:label,则阶段6的其余部分将完全处理CALL的执行。
    • 将当前批处理脚本文件位置压入调用堆栈,以便在CALL完成后可以从正确的位置恢复执行。
    • 使用所有结果标记为CALL设置%0,%1,%2,...%N和%*参数标记
    • 如果命令令牌是以开头的标签:,则
      • 重新启动阶段5。这可能会影响:label被调用的内容。但是,由于已经设置了%0等标记,因此它不会更改传递给CALLed例程的参数。
      • 执行GOTO标签将文件指针定位在子例程的开头(忽略:label后面的任何其他标记)有关GOTO工作原理的规则,请参见阶段7。
        • 如果缺少:label标记,或找不到:label,则立即弹出调用堆栈以恢复保存的文件位置,并且CALL被中止。
        • 如果:label恰好包含/ ?,那么将显示GOTO帮助,而不是搜索:label。文件指针不会移动,这样CALL之后的代码将执行两次,一次在CALL上下文中,然后在CALL返回之后再次执行。请参阅为什么CALL在此脚本中打印GOTO帮助消息?为什么在此之后执行两次命令?有关更多信息。
    • 否则将控制转移到指定的批处理脚本。
    • 继续执行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命令在名称
        set name="content" not ignored -> value = 之前没有引号"content" not ignored
        ,则将等于之后的行的其余部分用作内容,包括可能存在的所有引号。
    • 评估IF比较,并根据条件是对还是错,从阶段5开始处理适当的已经解析的从属命令块。
    • 正确迭代FOR命令的IN子句。
      • 如果这是对命令块的输出进行迭代的FOR / F,则:
        • IN子句通过CMD / C在新的cmd.exe进程中执行。
        • 命令块必须第二次执行整个解析过程,但这一次是在命令行上下文中
        • ECHO将开始为ON,并且延迟扩展通常将开始为禁用(取决于注册表设置)
        • 一旦子cmd.exe进程终止,由IN子句命令块进行的所有环境更改将丢失。
      • 对于每次迭代:
        • 定义了FOR变量值
        • 然后从阶段4开始处理已解析的DO命令块。
    • GOTO使用以下逻辑定位:label
      • 标签是从第一个参数标记解析的
      • 扫描脚本以查找下一次出现的标签
        • 扫描从当前文件位置开始
        • 如果到达文件末尾,则扫描将循环回到文件的开头并继续到原始起点。
      • 扫描在找到的标签的第一次出现时停止,并且文件指针被设置为紧随标签之后的行。从该点开始执行脚本。请注意,成功的真实GOTO将立即中止所有已分析的代码块,包括FOR循环。
      • 如果找不到标签,或者标签标记丢失,则GOTO失败,打印错误消息,并弹出调用堆栈。这有效地用作EXIT / B,除了在GOTO之后的当前命令块中任何已解析的命令仍将执行,但是在CALLer的上下文中(EXIT / B之后的上下文)
      • 请参阅https://www.dostips.com/forum/viewtopic.php?f=3&t=3803,以更准确地描述用于解析标签的规则。
    • RENAME和COPY都接受源路径和目标路径的通配符。但是Microsoft所做的工作很糟糕,记录了通配符的工作原理,尤其是对于目标路径。Windows RENAME命令如何解释通配符,可以找到一组有用的通配符规则
  • 7.2-执行音量更改 -否则,如果命令令牌不是以引号开头,正好是两个字符,而第二个字符是冒号,则更改音量
    • 所有参数标记都将被忽略
    • 如果找不到第一个字符指定的音量,则错误中止
    • ::除非使用SUBST为定义卷,否则命令令牌的将始终导致错误。::
      如果使用SUBST为其定义卷::,则该卷将被更改,因此不会被视为标签。
  • 7.3-执行外部命令 -否则尝试将该命令视为外部命令。
    • 如果在命令行模式和命令不被引用,并且不与卷说明书中,空白开始,,;=+然后中断在第一次出现的令牌的命令<space> , ;=与预先设置其余的参数标记(或多个)。
    • 如果命令令牌的第二个字符是冒号,则验证可以找到第一个字符指定的容量。
      如果找不到该卷,则异常中止。
    • 如果处于批处理模式且命令令牌以开头:,则转到7.4。
      请注意,如果标签令牌以开头::,则将无法实现,因为除非使用SUBST为它定义卷,否则上一步将因错误而中止::
    • 标识要执行的外部命令。
      • 这是一个复杂的过程,可能涉及当前卷,当前目录,PATH变量,PATHEXT变量和/或文件关联。
      • 如果无法识别有效的外部命令,则中止并返回错误。
    • 如果在命令行模式下并且命令标记以开头:,则转到7.4。
      请注意,很少执行此操作,因为除非命令标记以开头::,并且SUBST用于为和定义卷::,否则上一步将因错误而中止。整个命令令牌是指向外部命令的有效路径。
    • 7.3.exec-执行外部命令。
  • 7.4-忽略标签 -如果命令标记以开头,则忽略命令及其所有参数:
    7.2和7.3中的规则可能会阻止标签到达这一点。

命令行解析器:

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

第1阶段)扩展百分比:

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

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

  • 在阶段2之后不执行此操作。仅在阶段4之后执行FOR DO命令块。

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

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

阶段7)执行命令

  • 尝试调用或转到: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)-许多其他内容和编辑


4
您好jeb,谢谢您的见解……这可能很难理解,但我会尽力加以考虑!您似乎进行了很多测试!感谢您的翻译(administrator.de/...
贝努瓦

2
批处理阶段5)-在阶段1中,%% a已更改为%a,因此for循环扩展确实扩展了%a。另外,我在下面的答案中添加了批处理阶段1的更详细说明(我没有编辑权限)
dbenham 2011年

3
杰布-也许阶段0可以移动并与阶段6结合?这对我来说更有意义,还是有理由将它们分开吗?
dbenham 2012年

1
@aschipfl-我更新了该部分。该)确实功能几乎像一个REM命令时,括号计数器是这两个命令行0尝试:) Ignore thisecho OK & ) Ignore this
dbenham

1
@aschipfl是的,这是正确的,因此有时您会看到'set“ var =%expr%”!'最后一个感叹号将被删除,但会强制执行阶段5
jeb

62

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

如果听起来像无政府状态,那就是。但是,由于大量Windows程序确实利用了Microsoft C / C ++运行时argv,因此了解MSVCRT如何对参数进行标记通常可能很有用。这是节选:

  • 参数由空格分隔,空格可以是空格或制表符。
  • 字符串中包含双引号的字符串将解释为单个参数,而不管其中包含的空格如何。带引号的字符串可以嵌入参数中。请注意,记号(^)不被识别为转义字符或定界符。
  • 以反斜杠\“开头的双引号被解释为文字双引号(”)。
  • 反斜杠将按字面意义进行解释,除非它们直接在双引号之前。
  • 如果偶数个反斜杠后跟一个双引号,则对于每对反斜杠(\),在argv数组中放置一个反斜杠(),并且双引号(“)被解释为字符串定界符。
  • 如果奇数个反斜杠后跟双引号,则每对反斜杠(\)都会在argv数组中放置一个反斜杠(),并且剩余的反斜杠会将双引号解释为转义序列,从而导致放在argv中的文字双引号(“)。

Microsoft的“批处理语言”(.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]

谢谢您的回答。这让我为难,更要看到,TinyPerl不会输出你的程序的输出,我有困难,了解如何[a "b" c]能成为[a "b] [c]做后期处理。
Benoit

现在我考虑了一下,命令行的这种标记化可能完全由C运行时完成。可执行文件可以被编写为甚至不使用C运行时,在这种情况下,我认为它必须逐字处理命令行,并负责进行自己的标记化(如果愿意)。甚至如果您的应用程序确实使用C运行时,则可以选择忽略argc和argv,而仅通过Win32等获得原始​​命令行GetCommandLine。也许TinyPerl忽略了argv并只是通过自己的规则对原始命令行进行标记。
Mike Clark,2010年

4
“请记住,从Win32的角度来看,命令行只是一个被复制到新进程的地址空间中的字符串。启动进程和新进程如何解释此字符串的方式不是由规则而是由约定控制。” -Raymond Chen blogs.msdn.com/b/oldnewthing/archive/2009/11/25/9928372.aspx
Mike Clark,2010年

2
谢谢您的回答。我认为这可以解释很多。这也解释了为什么有时我发现使用Windows确实很糟糕……
Benoit 2010年

对于Win32 C ++程序,在从命令行转换为argv的过程中,我发现有关反斜杠和引号的问题。当最后一个反斜杠后面跟有dblquote时,反斜杠计数仅除以2,而如果之前有偶数个反斜杠,则dblquote终止字符串。
Benoit

47

扩展百分比规则

这是jeb答案中第1阶段的扩展说明(批处理模式和命令行模式均有效)。

阶段1)扩充百分比 从左侧开始,扫描%或的每个字符<LF>。如果找到的话

  • 1.05(在处截断行<LF>
    • 如果该字符<LF>,然后
      • 降(忽略)的行的剩余部分从所述<LF>向前
      • 转到1.5期(Strip <CR>
    • 否则字符必须是%,所以继续进行1.1
  • % 如果在命令行模式下跳过1.1(转义
    • 如果是批处理模式,然后是另一个模式,%
      替换%%为单个%并继续扫描
  • 如果使用命令行模式,则跳过1.2(扩展参数)
    • 否则,如果是批处理模式,则
      • 如果后跟*和启用了命令扩展,则
        %*所有命令行参数的文本替换(如果没有参数,则不替换任何内容)并继续扫描。
      • 否则,<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的值并继续扫描
        • 否则,如果为批处理模式,则
          删除%VAR%并继续扫描
        • 其他转到1.4
      • 否则,如果下一个字符是:那么
        • 如果未定义VAR,则
          • 如果是批处理模式,则
            删除%VAR:并继续扫描。
          • 其他转到1.4
        • 否则,如果下一个字符是~那么
          • 如果下一个字符串与模式匹配,[integer][,[integer]]%
            替换%VAR:~[integer][,[integer]]%为VAR值的子字符串(可能导致字符串为空)并继续扫描。
          • 其他转到1.4
        • 否则,如果随后=还是*=那么
          无效变量的搜索和替换语法引发致命错误:所有解析命令中止,并在批处理模式批处理可放弃,如果!
        • 否则,如果下一个字符串与模式匹配[*]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在他的帖子中提出的解析规则顺序的一个有趣结果:当执行延迟延迟的查找和替换时,必须对查找和替换项中的特殊字符进行转义或引用。但是扩展百分比的情况有所不同-不能逃避查找项(尽管可以引用)。百分比替换字符串可能需要也可能不需要转义或引号,具体取决于您的意图。

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

延迟扩充规则

这是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其中的比较是一个==equneqlssleqgtr,或geq
  • 参数令牌
  • 重定向的目标令牌(每个重定向一个)

不包含的令牌不会进行任何更改!

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

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

3
+1,此处和%definedVar:a=b%vs %undefinedVar:a=b%%var:~0x17,-010%表格仅缺少冒号语法和规则
jeb 2011年

2
好点-我扩展了变量扩展部分,以解决您的问题。我还扩展了参数扩展部分,以填充一些缺少的细节。
dbenham

2
从jeb获得一些其他私人反馈后,我添加了一个以冒号结尾的变量名称的规则,并添加了注释2。我还添加了注释3只是因为我认为它有趣且重要。
dbenham

1
@aschipfl-是的,我考虑过对此进行更详细的介绍,但不想钻进那个兔子洞。当我使用[integer]一词时,我是故意不置可否的。Rules中有更多有关CMD.EXE如何解析数字的信息
dbenham '16

1
我错过了扩规为CMD背景下,像有变量名状的第一个字符没有保留字符%<digit>%*%~。对于未定义的变量,其行为也会发生变化。也许您需要打开第二个答案
jeb

7

如前所述,命令在μ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"""]

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

Sane解析似乎永远崩溃了。

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


要将%t%作为单个参数传递,可以使用“%t:” = \“%”,即,使用%VAR:str = replacement%语法进行变量扩展。像|一样的Shell元字符 和&在变量中的内容仍然可以被暴露出来,并且弄乱了外壳,除非您再次逃脱它们
。...–艰难的

@Toughy在我的示例中ta "b c。你有没有得到那些6个字符配方(a2×空间"bc)显示为%1.cmd?我喜欢你的想法。args "%t:"=""%"非常接近:-)
bobbogo

5

上面您已经有了一些不错的答案,但是可以回答一部分问题:

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

发生的情况是,因为您在=前面有一个空格,%a<space>% 所以会创建一个名为的变量,当您echo %a %将其正确评估为时b

b% c%然后将其余部分评估为纯文本+一个未定义的变量% c%,应将其作为输入回显,以供我echo %a %b% c%返回bb% c%

我怀疑在变量名中包含空格的能力比计划的“功能”更多地是疏忽大意


0

编辑:请参阅接受的答案,以下内容是错误的,仅说明如何将命令行传递给TinyPerl。


关于引号,我有以下行为:

  • "找到a时,字符串开始
  • 当发生字符串阻塞时:
    • 每个不是的字符都会"被混淆
    • "找到a时:
      • 如果其后跟""(因此是三元组"),则将双引号添加到字符串中
      • 如果其后跟"(因此是double "),则在字符串中添加双引号,并且字符串的结尾
      • 如果下一个字符不是",字符串结束
    • 当行结束时,字符串遍历结束。

简而言之:

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

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


标记程序和字符串的全局性取决于命令!“集合”的工作原理不同于“呼叫”甚至“如果”的知识
jeb 2010年

是的,但是外部命令呢?我猜cmd.exe总是将相同的参数传递给他们?
Benoit

1
cmd.exe始终将扩展结果作为字符串而不是令牌传递给外部命令。它取决于外部命令如何进行转义和标记化,findstr使用反斜杠,下一个可以使用其他
斜杠

0

请注意,Microsoft已发布其终端的源代码。就语法分析而言,它可能类似于命令行。也许有人对根据终端的解析规则测试反向工程解析规则感兴趣。

链接到源代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.