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

cfh9epnr  于 2022-12-24  发布在  Windows
关注(0)|答案(8)|浏览(224)

我遇到了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%可以工作,有更好的解决方案吗?
  • %对如何匹配?例如:
  • x一米十一纳米一x一米十二纳米一x → x一米十三纳米一x
  • x一米十四纳一x x一米十五纳一x → x一米十六纳一x
  • 如果一个变量包含双引号,我如何确保它作为一个参数传递给命令?
  • 当使用set命令时变量是如何存储的?例如,如果我执行set a=a" b,然后执行echo.%a%,我会得到a" b。但是如果我从UnxUtils使用echo.exe,我会得到a b%a%如何以不同的方式扩展?
ylamdve6

ylamdve61#

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

批处理行解析器:

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

    • 相位0)读取行:**
    • 阶段1)膨胀百分比:**
    • 阶段2)处理特殊字符、标记化和构建缓存命令块:**这是一个复杂的过程,受引号、特殊字符、标记分隔符和脱字符转义等因素的影响。
    • 阶段3)回显解析的命令**仅当命令块不以@开头,且ECHO在上一步骤开始时为ON时。
    • 阶段4)FOR %X变量扩展:**仅当FOR命令处于活动状态且DO之后的命令正在处理时。
    • 阶段5)延迟扩展:**仅当启用延迟扩展时
    • 阶段5.3)管道处理:**仅当命令位于管道的任一端时
    • 阶段5.5)执行重定向:**
    • 阶段6)CALL处理/插入字符加倍:**仅当命令标记为CALL时
    • 阶段7)执行:**执行命令

以下是每个阶段的详细信息:
请注意,下面描述的阶段只是批处理分析器工作方式的一个模型。实际的cmd.exe内部可能不会反映这些阶段。但此模型在预测批处理脚本的行为方面非常有效。

    • 相位0)读取行:**通过第一个<LF>读取输入行。
  • 当读取要解析为命令的行时,<Ctrl-Z>(0x1A)被读取为<LF>(LineFeed 0x0A)
  • GOTO或CALL在扫描时读取行:标签<Ctrl-Z>被视为自身-它***不***转换为<LF>
    • 阶段1)膨胀百分比:**
  • %%替换为单%
  • 参数扩展(%*%1%2等)
  • 扩展%var%,如果var不存在,则将其替换为空
  • 行在第一个<LF>处被截断,不在%var%扩展范围内
    • 阶段2)处理特殊字符、标记化和构建缓存命令块:**这是一个复杂的过程,受引号、特殊字符、标记分隔符和脱字符转义等因素的影响。

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

  • 标记只是被视为一个单元的字符串。
  • 标记由标记分隔符分隔。标准标记分隔符为<space><tab>;,=<0x0B><0x0C><0xFF>

连续的令牌分隔符被视为一个-令牌分隔符之间没有空令牌

  • 带引号的字符串中没有标记分隔符。整个带引号的字符串始终被视为单个标记的一部分。单个标记可以由带引号的字符串和不带引号的字符的组合组成。

根据上下文,以下字符在此阶段可能具有特殊含义:x一个米23纳米1 x一个米24纳米1 x一个米25纳米1 x一个米26纳米1 x一个米27纳米1 x一个米28纳米1 x一个米29纳米1 x一个米30纳米1 x一个米31纳米1 x一个米32纳米1 x一个米33纳米1 x一个米34纳米1 x一个米35纳米1 x一个米36纳米1 x一个米37纳米1 x一个米38纳米1 x一个米39纳米1 x
从左到右看每个字符:

  • 如果是<CR>,则删除它,就好像它从未存在过一样(除了奇怪的redirection behavior

  • 如果是插入符号(^),则对下一个字符进行转义,并删除转义插入符号。转义字符将失去所有特殊含义(<LF>除外)。

  • 如果是引号("),则切换引号标志。如果引号标志处于活动状态,则只有"<LF>是特殊的。所有其他字符将失去其特殊含义,直到下一个引号将引号标志切换为关闭。右引号不能转义。所有带引号的字符始终位于同一标记内。

  • <LF>总是关闭引号标志。其他行为因上下文而异,但引号永远不会改变<LF>的行为。

  • 逃逸<LF>

  • <LF>已剥离

  • 转义下一个字符。如果在行尾缓冲区,则下一行由阶段1和1.5读取和处理,并在转义下一个字符之前附加到当前行。如果下一个字符是<LF>,则将其视为文字,这意味着此过程不是递归的。

  • 未转义的<LF>不在括号内

  • <LF>被剥离并且终止对当前行的解析。

  • 行缓冲区中的任何剩余字符都将被忽略。

  • 带FOR IN括号的块中的未转义<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迭代命令

  • 所有FOR选项都在阶段2中完全解析。

  • IN括号子句将<LF>视为<space>。分析IN子句后,所有标记将连接在一起以形成单个标记。

  • 从FOR命令到DO,连续的未转义/未加引号的标记分隔符将折叠为一个空格。

  • DO之后的部分是正常解析的命令块,DO命令块的后续处理由阶段7的迭代控制。

  • 在第2阶段检测到的REM的处理方式与所有其他命令的处理方式截然不同。

  • 只解析一个参数标记-解析器忽略第一个参数标记之后的字符。

  • REM命令可能会出现在阶段3输出中,但该命令永远不会执行,并且原始参数文本会被回显-转义插入符号不会被删除,除了...

  • 如果只有一个参数标记以一个未转义的^结尾,那么这个参数标记将被丢弃,随后的行将被解析并追加到REM中,直到有多个标记,或者最后一个字符不是^

  • 如果命令令牌以:开始,并且这是阶段2的第一轮(不是由于阶段6中的CALL而重新启动),则

  • 标记通常被视为 * 未执行标签 *。

  • 行的其余部分被解析,但是)<>&|不再有特殊含义。行的整个其余部分被认为是标签“命令”的一部分。

  • ^仍然是特殊的,这意味着可以使用行继续符将下一行附加到标签上。

  • 带括号的块中的 Unexecuted Label 将导致致命的语法错误,除非它后面紧跟命令或下一行的 Executed Label

  • (对于 Unexecuted Label 后面的第一个命令不再具有特殊含义。

  • 标签解析完成后,该命令将中止。不会对标签执行后续阶段

  • 有三种例外情况会导致在阶段2中找到的标签被视为 Executed Label,并继续解析到阶段7。

  • 标签标记之前有重定向,并且行上有|管道或&&&||命令串联。

  • 标签标记之前有重定向,并且命令位于带括号的块中。

  • label标记是带括号的块中某行的第一个命令,上面的行以 Unexecuted Label 结尾。

  • 在阶段2中发现 Executed Label 时,将发生以下情况

  • 标签、其参数和重定向都被排除在阶段3的任何echo输出之外

  • 该行中任何后续的连接命令都将被完全解析并执行。

  • 有关 * 已执行标签 * 与 * 未执行标签 * 的详细信息,请参见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命令,则内部DO命令可以访问***所有当前定义的FOR变量。

  • FOR变量名可以在嵌套的FOR中重用。内部的FOR值优先,但一旦INNER FOR关闭,则外部的FOR值将恢复。

  • 如果在此阶段开始时ECHO为ON,则重复阶段3),以显示FOR变量展开后解析的DO命令。
    ***----从这一点开始,阶段2中识别的每个命令都将单独处理。

----完成一个命令的阶段5到阶段7,然后再进入下一个命令。***

**阶段5)延迟扩展:**仅当延迟扩展打开,命令不在parenthesized block on either side of a pipe中,并且命令不是"naked" batch script(脚本名称不带括号、CALL、命令串联或管道)时。

  • 命令的每个令牌被独立地解析以用于延迟扩展。

  • 大多数命令分析两个或多个标记-命令标记、参数标记和每个重定向目标标记。

  • FOR命令仅分析IN子句标记。

  • IF命令只解析比较值-一个或两个,具体取决于比较运算符。

  • 对于每个已解析的标记,首先检查它是否包含任何!。如果不包含,则不解析该标记-这对于^字符很重要。如果标记确实包含!,则从左到右扫描每个字符:

  • 如果是插入符号(^),则下一个字符没有特殊含义,插入符号本身将被删除

  • 如果是感叹号,则搜索下一个感叹号(不再观察到插入符号),展开到变量的值。

  • 连续开口!折叠成单个!

  • 删除任何剩余的未配对!

  • 在此阶段扩展VAR是"安全的",因为不再检测特殊字符(甚至<CR><LF>

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

管道的每一端都是独立和异步处理的。

  • 如果命令是cmd.exe内部的,或者是批处理文件,或者是带括号的命令块,则通过%comspec% /S /D /c" commandBlock"在新的cmd.exe线程中执行,因此命令块获得阶段重新启动,但这次是在命令行模式下。
  • 如果是带括号的命令块,则所有前后带有命令的<LF>都将转换为<space>&。其他<LF>将被剥离。
  • 管道命令的处理到此结束。
  • 有关管道解析和处理的详细信息,请参见Why does delayed expansion fail when inside a piped block of code?
    • 阶段5.5)执行重定向:**现在执行在阶段2中发现的任何重定向。
  • 第4阶段和第5阶段的结果可能会影响在第2阶段发现的重定向。
  • 如果重定向失败,则将中止命令的其余部分。Note that failed redirection does not set ERRORLEVEL to 1 unless || is used
    • 阶段6)CALL处理/插入字符加倍:**仅当命令标记为CALL,或者第一个出现的标准标记分隔符之前的文本为CALL时。如果CALL是从较大的命令标记中分析出来的,则在继续之前,未使用的部分将被置于参数标记之前。
  • 扫描参数标记中是否有未加引号的/?。如果在标记中找到任何地方,则中止阶段6并继续阶段7,在阶段7中将打印CALL的HELP。
  • 删除第一个CALL,以便可以堆叠多个CALL
  • 所有插入符号加倍
  • 重新开始阶段1、1.5和2,但不要继续进行阶段3
  • 任何加倍的插入符号只要没有被引用就会被还原为一个插入符号。但是不幸的是,被引用的插入符号仍然是加倍的。
  • 第1阶段稍有变化-步骤1.2或1.3中的扩展错误中止CALL,但错误不是致命的-批处理继续。
  • 第2阶段的任务稍有改变
  • 任何新出现的未加引号、未转义的重定向(在第2阶段的第一轮中未检测到)都会被检测到,但它会被删除(包括文件名),而不会实际执行重定向
  • 行尾新出现的任何未加引号、未转义的插入符号都将被删除,而不执行行继续符
  • 如果检测到以下任何情况,则CALL将中止,且不会出错
  • 新出现的无引号、无转义的&|
  • 生成的命令标记以未加引号、未转义的(开头
  • 删除CALL后的第一个令牌以@开头
  • 如果生成的命令是看似有效的IF或FOR,则执行随后将失败,并显示错误,指出IFFOR未被识别为内部或外部命令。
  • 当然,如果得到的命令令牌是以:开始的标签,则在阶段2的第二轮中CALL不被中止。
  • 如果结果命令令牌是CALL,则重新开始阶段6(重复直到不再有CALL)
  • 如果生成的命令令牌是批处理脚本或:label,则CALL的执行完全由阶段6的剩余部分处理。
  • 推送调用堆栈上的当前批处理脚本文件位置,以便在CALL完成时可以从正确的位置继续执行。
  • 使用所有生成的标记为CALL设置%0、%1、%2、... %N和%* 参数标记
  • 如果命令标记是以:开头的标签,则
  • 重新启动第5阶段。这可能会影响要调用的内容:标签。但由于已设置%0等标记,因此它不会更改传递给已调用例程的参数。
  • 执行GOTO label以将文件指针定位在子例程的开头(忽略:label后面可能的任何其他标记)有关GOTO如何工作的规则,请参见阶段7。
  • 如果缺少:label标记,或者没有找到:label,则立即弹出调用堆栈以恢复保存的文件位置,并且中止CALL。
  • 如果:label恰好包含/?,则打印GOTO帮助而不是搜索:label。文件指针不移动,因此CALL之后的代码执行两次,一次在CALL上下文中,然后在CALL返回后再次执行。有关详细信息,请参阅Why CALL prints the GOTO help message in this script?And why command after that are executed twice?
  • 否则将控制权转移到指定的批处理脚本。
  • 继续执行CALLed:标签或脚本,直到到达EXIT/B或文件结尾,此时弹出CALL堆栈,并从保存的文件位置继续执行。

对于已调用的脚本或:labels,不执行阶段7。

  • 否则,阶段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命令在启用变量名和扩展名之前带有引号

set "name=content" ignored**--〉**值= content
则将第一个等号和最后一个引号之间的文本用作内容(不包括第一个等号和最后一个引号)。忽略最后一个引号之后的文本。如果等号之后没有引号,则将该行的其余部分用作内容。

  • 如果SET命令的名称前没有引号

set name="content" not ignored**--〉**值= "content" not ignored
则等于之后的行的整个剩余部分被用作内容,包括可能存在的任何和所有引号。

  • 评估IF比较,并且取决于条件是真还是假,从阶段5开始处理适当的已经解析的依赖命令块。

  • FOR命令的IN子句被适当地迭代。

  • 如果这是一个迭代命令块输出的FOR /F,则:

  • IN子句通过CMD /C在新的cmd.exe进程中执行。

  • 命令块必须再次经历整个解析过程,但这次是在命令行上下文中

  • ECHO将启动,延迟扩张通常将启动禁用(取决于注册表设置)

  • 子cmd.exe进程终止后,IN子句命令块所做的所有环境更改都将丢失

  • 对于每次迭代:

  • FOR变量值已定义

  • 然后处理已经解析的DO命令块,从阶段4开始。

  • 后藤使用以下逻辑定位:label

  • 从第一个参数标记解析标签

  • 扫描标签的下一个匹配项

  • 从当前文件位置开始

  • 如果到达文件末尾,则循环回到文件开头并继续到原始起始点。

  • 扫描在找到的标签第一次出现时停止,并将文件指针设置为紧跟标签的行。脚本从该点继续执行。请注意,成功的真正后藤将立即中止任何已分析的代码块,包括FOR循环。

  • 如果找不到标签,或者标签标记丢失,则后藤失败,打印错误消息,并弹出调用堆栈。这实际上起到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都接受源路径和目标路径的通配符。但是Microsoft在记录通配符如何工作方面做得很糟糕,尤其是对于目标路径。可以在How does the Windows RENAME command interpret wildcards?中找到一组有用的通配符规则

      • 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)延迟扩展:**仅当启用延迟扩展时
  • 如果未定义var,则!var!保持不变。
    • 阶段7)执行命令**
  • 尝试调用或转到:标签将导致错误。
  • 如第7阶段所述,在不同情况下,执行的标签可能会导致错误。
  • 批处理执行的标签仅在以::开头时才会导致错误
  • 命令行执行的标签几乎总是会导致错误

解析整数值

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

  • SET /A
  • x1米190英寸1x
  • %var:~n,m%(可变子字符串扩展)
  • x1米192英寸1x
  • FOR /F "SKIP=n"
  • x1米194英寸1x
  • x1米195英寸1x

有关这些规则的详细信息,请访问Rules for how CMD.EXE parses numbers
对于希望改进cmd.exe解析规则的任何人,可以在discussion topic on the DosTips forum中报告问题并提出建议。
Jan Erik(Jeb)-相的原作者和发现者
Dave Benham(dbenham)-更多附加内容和编辑

j5fpnvbx

j5fpnvbx2#

从命令窗口调用命令时,命令行参数的标记化不是由cmd.exe完成的(又称为“ shell ”)。最经常地,标记化由新形成的进程的C/C运行时来完成,但这不是必须的--例如,如果新进程不是用C/C编写的,或者如果新进程选择忽略argv并为自己处理原始命令行(例如GetCommandLine())。在操作系统级别,Windows将命令行作为单个字符串传递给新进程。这与大多数 *nix shell形成对比,在这里,shell在将参数传递给新形成的进程之前,会以一致的、可预测的方式对参数进行标记化。所有这一切意味着,在Windows上的不同程序中,您可能会遇到非常不同的参数标记化行为,因为各个程序通常会自行进行参数标记化。
如果这听起来像是无政府状态,那么它确实是。然而,由于大量Windows程序 * 确实 * 使用Microsoft C/C++运行时的argv,因此理解how the MSVCRT tokenizes参数通常可能是有用的。以下是摘录:

  • 参数由白色分隔,空白可以是空格或制表符。
  • 无论字符串中是否包含空格,用双引号括起来的字符串都被解释为单个参数。用引号括起来的字符串可以嵌入参数中。请注意,插入符号(^)不能被识别为转义字符或分隔符。
  • 前有反斜杠"的双引号被解释为文字双引号(“)。
  • 反斜杠按字面解释,除非它们紧挨在双引号前面。
  • 如果偶数个反斜杠后面跟有双引号,则在argv数组中为每对反斜杠(\)放置一个反斜杠(),双引号(“)被解释为字符串分隔符。
  • 如果奇数个反斜杠后面跟有一个双引号,则在argv数组中为每对反斜杠(\)放置一个反斜杠(),剩下的反斜杠将双引号解释为转义序列,从而导致在argv中放置一个文本双引号(“)。

微软的“批处理语言”(.bat)对于这种无政府环境也不例外,并且它开发了自己独特的标记化和转义规则。看起来cmd.exe的命令提示符确实对命令行参数做了一些预处理(主要用于变量替换和转义),然后再将参数传递给新执行的进程。批处理语言和cmd转义的详细信息,请参阅本页上Jeb和dbenham的精彩回答。
让我们用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]
dgtucam1

dgtucam13#

百分比展开规则

下面是jeb's answer中第1阶段的详细说明(对批处理模式和命令行模式都有效)。

    • 阶段1)扩展百分比**从左开始,扫描每个字符以查找%<LF>。如果找到,则
      • 1.05(在<LF>处截断行)**
  • 如果字符为<LF>,则
  • 删除(忽略)从<LF>开始的行的其余部分
  • 转到第2.0阶段
  • 否则字符必须为%,因此请继续执行1.1
      • 1.1(转义%)***如果是命令行模式则跳过 *
  • 如果是批处理模式,然后是另一个%,则

%%替换为单个%并继续扫描

      • 1.2(扩展参数)***如果是命令行模式则跳过 *
  • 否则,如果为批处理模式,则
  • 如果后跟*并且启用了命令扩展,则

%*替换为所有命令行参数的文本(如果没有参数,则替换为nothing)并继续扫描。

  • 否则,如果后跟<digit>,则

%<digit>替换为参数值(如果未定义,则无替换)并继续扫描。

  • 否则,如果后跟~并且启用了命令扩展,则
  • 如果后跟可选的有效参数修饰符列表,后跟必需的<digit>,则

%~[modifiers]<digit>替换为修改后的参数值(如果未定义或指定了$PATH,则替换为nothing:修饰符未定义)并继续扫描。

  • 注意:修饰符不区分大小写,可以按任何顺序多次出现,$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:~[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- % parsing规则的另一个有趣的结果:变量包含:可以定义名称中的字符串,但除非禁用命令扩展,否则无法展开这些字符串。有一个例外-在启用命令扩展时,可以展开末尾包含单个冒号的变量名。但是,您无法对以冒号结尾的变量名执行子字符串或搜索和替换操作。下面的批处理文件(由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's answer中第5阶段的扩展和更准确的解释(对批处理模式和命令行模式都有效)

    • 阶段5)延迟扩展**

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

  • 延迟扩展已禁用。
  • 命令位于管道两侧的括号块中。
  • 传入的命令令牌是"裸"批处理脚本,这意味着它不与CALL、带括号的块、任何形式的命令串联(&&&||)或管道|关联。

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

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

不对不包含!的令牌进行更改。
对于每个至少包含一个!的标记,从左到右扫描每个字符以查找^!,如果找到,则

      • 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:[*]search=[replace]!替换为VAR值并继续扫描

  • 否则转至5.2.1
  • 否则转至5.2.1
  • 5.2.1
  • 如果是批处理模式,则删除前导!

否则保留前导!

  • 从保留的前导!之后的下一个字符开始继续扫描
cig3rfwq

cig3rfwq4#

如前所述,在μSoft land中,命令被传递了整个参数字符串,由命令自己将其解析为单独的参数以供使用。不同程序之间没有一致性,因此没有一套规则来描述这个过程。无论程序使用什么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"""]

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

理智的分析似乎永远被打破了。
为了方便您的娱乐,请尝试在这些示例中添加各种^\'&(等)字符。

nnt7mjpx

nnt7mjpx5#

你已经有一些很好的答案以上,但要回答你的问题的一部分:

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

这里发生的事情是,因为在=之前有一个空格,所以创建了一个名为%a<space>%的变量,所以当你echo %a %时,它的计算结果正确地为b
剩下的部分b% c%被计算为纯文本+一个未定义的变量% c%,它应该作为类型返回,对于我来说echo %a %b% c%返回bb% c%
我怀疑在变量名中包含空格的能力与其说是计划中的“特性”,不如说是一种疏忽

rggaifut

rggaifut6#

FOR-循环元变量展开

这是accepted answer中**阶段4)**的扩展说明(适用于批处理文件模式和命令行模式)。当然,for命令必须处于活动状态。下面介绍do子句之后的命令行部分的处理。请注意,在批处理文件模式下,由于前面的立即%-扩展阶段( 阶段1)),%%已经被转换为%

  • 扫描%-符号,从左侧开始直至行末;如果找到一个,则:
  • 如果启用Command Extensions(默认),则检查下一个字符是否为~;如果是,则:
  • 在不区分大小写的集合fdpnxsatz中,尽可能多地取定义for变量引用或$-符号的字符之前的下列字符(每个字符甚至取多次);如果遇到这样的$符号,则:
  • 扫描:1;如果找到,则:
  • 如果:后面有字符,则将其作为for变量引用并按预期展开,除非未定义,否则不展开并继续在该字符位置扫描;
  • 如果:是最后一个字符,cmd.exe将崩溃!
  • 否则(没有找到:)不展开任何内容;
  • 否则(如果未遇到$符号)使用所有修饰符扩展for变量,除非未定义,否则不扩展并继续在该字符位置扫描;
  • 否则(如果找不到~或命令扩展名被禁用)请检查下一个字符:
  • 如果没有更多字符可用,则不要展开任何内容;
  • 如果下一个字符是%,则不扩展任何内容,并返回到该字符位置2处的扫描开始处;
  • 否则使用下一个字符作为for变量引用并扩展,除非未定义,否则不扩展;
  • 返回到下一个字符位置的扫描起点(只要仍有字符可用);

1)$:之间的字符串被认为是环境变量的名称,甚至可以为空;因为环境变量不能有空的名字,所以行为与未定义的环境变量是一样的。
2)这意味着名为%for元变量不能在没有~修饰符的情况下展开。
原始来源:How to safely echo FOR variable %%~p followed by a string literal

t1rydlwq

t1rydlwq7#

编辑:请参阅已接受的答案,以下内容是错误的,仅解释了如何将命令行传递给TinyPerl。
关于报价,我的感觉是,行为如下:

  • 当找到"时,字符串匹配开始
  • 当字符串成滴发生时:
  • 不是"的每个字符都是全局匹配的
  • 当找到"时:
  • 如果后面跟有""(因此是三重"),则会在字符串中添加双引号
  • 如果后面跟有"(因此是双"),则会在字符串中添加双引号,字符串匹配结束
  • 如果下一个字符不是",则字符串匹配结束
  • 当线结束时,串滴结束。

简而言之:
"a """ b "" c"""由两个字符串组成:a " b "c"
如果"a"""a""""a""""位于行尾,则它们都是相同的字符串

ycggw6v2

ycggw6v28#

请注意,微软已经发布了终端的源代码。它的语法分析可能类似于命令行。也许有人有兴趣根据终端的解析规则测试反向工程的解析规则。
Link的源代码。

相关问题