提问人: 提问时间:11/4/2010 最后编辑:12 revs, 4 users 73%Benoit 更新时间:12/22/2022 访问量:92424
Windows 命令解释器 (CMD.EXE) 如何分析脚本?
How does the Windows Command Interpreter (CMD.EXE) parse scripts?
问:
我遇到了 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%
- 如果变量包含双引号,我如何确保变量作为单个参数传递给命令?
- 使用命令时如何存储变量?例如,如果我这样做了,然后我得到 .但是,如果我使用 UnxUtils,我会得到.如何以不同的方式扩展?
set
set a=a" b
echo.%a%
a" b
echo.exe
a b
%a%
答:
编辑:请参阅已接受的答案,接下来是错误的,仅解释如何将命令行传递给TinyPerl。
关于报价,我感觉行为如下:
- 找到 A 后,开始串通配
"
- 当字符串通配发生时:
- 每个不是 a 的字符都会被通配
"
- 当找到 a 时:
"
- 如果它后面跟着(因此是三元组),则在字符串中添加双引号
""
"
- 如果它后面跟着(因此是双引号),则在字符串和字符串通配的末尾添加双引号
"
"
- 如果下一个字符不是 ,则字符串通配结束
"
- 如果它后面跟着(因此是三元组),则在字符串中添加双引号
- 当行结束时,字符串通配结束。
- 每个不是 a 的字符都会被通配
总之:
"a """ b "" c"""
由两个字符串组成: 和a " b "
c"
"a""
,如果在一行的末尾,则都是相同的字符串"a"""
"a""""
评论
从命令窗口调用命令时,命令行参数的标记化不是由(也称为“shell”)完成的。大多数情况下,标记化是由新形成的进程的 C/C++ 运行时完成的,但事实并非如此——例如,如果新进程不是用 C/C++ 编写的,或者如果新进程选择忽略并自行处理原始命令行(例如使用 GetCommandLine())。在操作系统级别,Windows 将未标记的命令行作为单个字符串传递给新进程。这与大多数 *nix shell 形成鲜明对比,在大多数 *nix shell 中,shell 在将参数传递给新形成的进程之前,以一致、可预测的方式标记参数。所有这些都意味着,在 Windows 上的不同程序之间,您可能会遇到截然不同的参数标记化行为,因为各个程序通常会将参数标记化掌握在自己手中。cmd.exe
argv
如果这听起来像是无政府状态,那确实如此。但是,由于大量 Windows 程序确实使用 Microsoft C/C++ 运行时的 ,因此了解 MSVCRT 如何标记参数通常很有用。以下是摘录:argv
- 参数由空格分隔,空格可以是空格,也可以是制表符。
- 用双引号括起来的字符串被解释为单个参数,而不考虑其中包含的空格。带引号的字符串可以嵌入到参数中。请注意,插入符号 (^) 不会被识别为转义字符或分隔符。
- 前面有反斜杠 \“ 的双引号被解释为文字双引号 (”)。
- 反斜杠按字面意思解释,除非它们紧跟在双引号之前。
- 如果偶数个反斜杠后跟双引号,则每对反斜杠 (\) 都会在 argv 数组中放置一个反斜杠 (),双引号 (“) 被解释为字符串分隔符。
- 如果奇数个反斜杠后跟一个双引号,则每对反斜杠 (\) 都会在 argv 数组中放置一个反斜杠 (),并且剩余的反斜杠将双引号解释为转义序列,从而导致在 argv 中放置一个文字双引号 (“)。
Microsoft的“批处理语言”()也不例外,它已经发展了自己独特的标记化和转义规则。看起来 cmd.exe 的命令提示符确实对命令行参数进行了一些预处理(主要用于变量替换和转义),然后再将参数传递给新执行的进程。您可以在此页面上的 jeb 和 dbenham 的出色回答中阅读有关批处理语言和 cmd 转义的低级详细信息的更多信息。.bat
让我们用 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]
评论
[a "b" c]
[a "b] [c]
GetCommandLine
我们进行了实验来研究批处理脚本的语法。我们还研究了批处理模式和命令行模式之间的差异。
批处理线解析器:
以下是批处理文件行解析器中阶段的简要概述:
阶段 0) 读取行:
第 1 阶段)百分比扩展:
第 2 阶段)处理特殊字符、标记化和生成缓存的命令块:这是一个复杂的过程,受引号、特殊字符、标记分隔符和插入符号转义等因素的影响。
第 3 阶段)仅当命令块不以 开头,并且 ECHO 在上一步开始时处于 ON 状态时,才回显已解析的命令。@
第 4 阶段)FOR %X
变量扩展:仅当 FOR 命令处于活动状态并且正在处理 DO 之后的命令时。
第 5 阶段)延时扩容:仅开启延时扩容
阶段 5.3)管道处理:仅当命令位于管道的任一侧时
阶段 5.5) 执行重定向:
第 6 阶段)CALL 处理/插入符号加倍:仅当命令令牌为 CALL 时
第 7 阶段)执行:命令执行
以下是每个阶段的详细信息:
请注意,下面描述的阶段只是批处理解析器工作方式的模型。实际的 cmd.exe 内部结构可能无法反映这些阶段。但此模型在预测批处理脚本的行为方面是有效的。
阶段 0)读取行:首先读取输入行。<LF>
- 当读取要解析为命令的行时,(0x1A) 被读取为 (LineFeed 0x0A)
<Ctrl-Z>
<LF>
- 当 GOTO 或 CALL 在扫描 :label 时读取行时,将被视为其自身 - 它不会转换为
<Ctrl-Z>
<LF>
第 1 阶段)百分比扩展:
- 双精度被替换为单精度
%%
%
- 参数的扩展(、、等)
%*
%1
%2
- 的扩展,如果 var 不存在,则将其替换为 nothing
%var%
- 行最初被截断,不在扩展范围内
<LF>
%var%
- 有关完整的解释,请阅读 dbenham 的前半部分 同一线程: 百分比阶段
第 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>
被剥离,并终止对当前行的解析。- 行缓冲区中的任何剩余字符都将被忽略。
- 在带括号的 FOR IN 块中未转义
<LF>
<LF>
转换为<space>
- 如果在行缓冲区的末尾,则读取下一行并将其追加到当前行。
- 在带括号的命令块内取消转义
<LF>
<LF>
转换为 ,并且 被视为命令块下一行的一部分。<LF><space>
<space>
- 如果在行缓冲区的末尾,则读取下一行并将其追加到空格中。
- 逃脱
- 如果其中一个特殊字符 或 ,则此时拆分行以处理管道、命令连接和重定向。
&
|
<
>
- 对于管道 (),每一端都是一个单独的命令(或命令块),在阶段 5.3 中得到特殊处理
|
- 在 、 或命令串联的情况下,串联的每一端都被视为一个单独的命令。
&
&&
||
- 对于 、 、 或 redirection,将分析重定向子句,暂时删除该子句,然后将其追加到当前命令的末尾。重定向子句由可选的文件句柄数字、重定向运算符和重定向目标令牌组成。
<
<<
>
>>
- 如果重定向运算符前面的令牌是一个未转义的数字,则该数字指定要重定向的文件句柄。如果未找到句柄令牌,则输出重定向默认为 1 (stdout),输入重定向默认为 0 (stdin)。
- 对于管道 (),每一端都是一个单独的命令(或命令块),在阶段 5.3 中得到特殊处理
- 如果此命令的第一个标记(在将重定向移动到末尾之前)以 开头,则 具有特殊含义。(在任何其他情况下都不是特殊)
@
@
@
- 特殊功能被删除。
@
- 如果 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 阶段控制。
- 比较操作是一直流向第 7 阶段的实际命令
- FOR 在 DO 之后一分为二。FOR 构造中的语法错误将导致致命的语法错误。
- 通过 DO 的部分是实际的 FOR 迭代命令,该命令一直流经第 7 阶段
- 所有 FOR 选项都在第 2 阶段完全解析。
- 带括号的 IN 子句视为 .解析 IN 子句后,所有标记将连接在一起以形成单个标记。
<LF>
<space>
- 连续的未转义/不加引号的标记分隔符在整个 FOR 命令中通过 DO 折叠成一个空格。
- DO 后面的部分是正常解析的命令块。DO 命令块的后续处理由阶段 7 中的迭代控制。
- 通过 DO 的部分是实际的 FOR 迭代命令,该命令一直流经第 7 阶段
- 在第 2 阶段检测到的 REM 的处理方式与所有其他命令截然不同。
- 只解析一个参数标记 - 解析器忽略第一个参数标记后面的字符。
- REM 命令可能会出现在第 3 阶段输出中,但该命令永远不会执行,并且原始参数文本会被回显 - 转义的插入符号不会被删除,除了......
- 如果只有一个参数标记以结束该行的未转义结尾,则该参数标记将被丢弃,后续行将被解析并追加到 REM。重复此操作,直到有多个标记,或者最后一个字符不是 。
^
^
- 如果只有一个参数标记以结束该行的未转义结尾,则该参数标记将被丢弃,后续行将被解析并追加到 REM。重复此操作,直到有多个标记,或者最后一个字符不是 。
- IF 被拆分为两个或三个独立的部分,这些部分相互独立处理。IF 构造中的语法错误将导致致命的语法错误。
- 如果命令令牌以 开头,并且这是第 2 阶段的第一轮(由于第 6 阶段的 CALL,不是重新启动),则
:
- 令牌通常被视为未执行的标签。
- 但是,该行的其余部分被解析为 、 、 ,并且不再具有特殊含义。该行的整个其余部分被视为标签“command”的一部分。
)
<
>
&
|
- continue 是特殊的,这意味着行继续符可用于将后续行附加到标签中。
^
- 括号块中的“未执行标签”将导致致命的语法错误,除非紧跟在下一行的命令或“已执行标签”之后。
(
对于“未执行标签”后面的第一个命令不再具有特殊意义。
- 标签解析完成后,该命令将中止。标签不会进行后续阶段
- 但是,该行的其余部分被解析为 、 、 ,并且不再具有特殊含义。该行的整个其余部分被视为标签“command”的一部分。
- 有三种例外情况可能导致在第 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 变量(如 )转换为 。命令行对第 1 阶段具有不同的百分比扩展规则。这就是命令行使用但批处理文件用于 FOR 变量的原因。
%%X
%X
%X
%%X
- FOR 变量名称区分大小写,但不区分大小写。
~modifiers
~modifiers
优先于变量名称。如果后面的字符既是修饰符又是有效的 FOR 变量名,并且存在作为活动 FOR 变量名的后续字符,则该字符被解释为修饰符。~
- FOR 变量名是全局的,但仅在 DO 子句的上下文中。如果例程是从 FOR DO 子句中 CALLed 的,则 FOR 变量不会在 CALLed 例程中展开。但是,如果例程有自己的 FOR 命令,则内部 DO 命令可以访问所有当前定义的 FOR 变量。
- FOR 变量名称可以在嵌套的 FOR 中重用。内部 FOR 值优先,但一旦内部 FOR 关闭,则恢复外部 FOR 值。
- 如果 ECHO 在此阶段开始时处于 ON 状态,则在展开 FOR 变量后重复第 3) 阶段以显示解析的 DO 命令。
---- 从此时起,阶段 2 中标识的每个命令都将单独处理。
---- 完成一个命令的第 5 阶段到第 7 阶段,然后再继续执行下一个命令。
第 5 阶段)延迟扩展:仅当延迟扩展处于打开状态时,命令不在管道两侧的括号块中,并且该命令不是“裸”批处理脚本(不带括号的脚本名称、CALL、命令串联或管道)。
- 命令的每个令牌都会独立解析延迟扩展。
- 大多数命令分析两个或多个标记 - 命令标记、参数标记和每个重定向目标标记。
- FOR 命令仅解析 IN 子句标记。
- IF 命令仅解析比较值 - 一个或两个,具体取决于比较运算符。
- 对于每个已解析的令牌,首先检查它是否包含任何 .如果没有,则不会解析令牌 - 对于字符很重要。
如果令牌包含 ,则从左到右扫描每个字符:
!
^
!
- 如果是插入符号 (),则下一个字符没有特殊含义,则删除插入符号本身
^
- 如果是感叹号,则搜索下一个感叹号(不再观察到插入符号),展开到变量的值。
- 连续开口折叠成一个
!
!
- 任何剩余的未配对都将被删除
!
- 连续开口折叠成一个
- 在此阶段扩展 var 是“安全的”,因为不再检测到特殊字符(偶数或
<CR>
<LF>
) - 有关更完整的解释,请阅读 dbenham 同一线程的后半部分 - 感叹号阶段
- 如果是插入符号 (),则下一个字符没有特殊含义,则删除插入符号本身
阶段 5.3)管道处理:仅当命令位于管道的任一侧时,管道
的每一侧都是独立且异步处理的。
- 如果命令是 cmd.exe 内部的,或者它是批处理文件,或者它是带括号的命令块,则它通过 在新的 cmd.exe 线程中执行,因此命令块会进行阶段重新启动,但这次是在命令行模式下。
%comspec% /S /D /c" commandBlock"
- 如果命令块括号内,则所有带有命令前后的命令都将转换为 .其他的被剥离。
<LF>
<space>&
<LF>
- 如果命令块括号内,则所有带有命令前后的命令都将转换为 .其他的被剥离。
- 管道命令的处理到此结束。
- 请参阅为什么在管道代码块中延迟扩展失败? 有关管道分析和处理的详细信息
阶段 5.5)执行重定向:现在将执行在第 2 阶段发现的任何重定向。
- 第 4 阶段和第 5 阶段的结果可能会影响在第 2 阶段发现的重定向。
- 如果重定向失败,则命令的其余部分将中止。请注意,除非使用
||,
否则失败的重定向不会将 ERRORLEVEL 设置为 1。
第 6 阶段)CALL 处理/插入符号加倍:仅当命令标记为 CALL,或者第一个出现的标准标记分隔符之前的文本为 CALL 时。如果 CALL 是从较大的命令令牌解析的,则在继续操作之前,未使用的部分将附加到参数令牌前面。
- 扫描参数标记以查找未加引号的 .如果在令牌中的任何位置找到,则中止第 6 阶段并继续进入第 7 阶段,其中将打印 CALL 的 HELP。
/?
- 删除第一个,因此可以堆叠多个 CALL
CALL
- 将所有插入符号加倍
- 重新启动阶段 1、1.5 和 2,但不要继续执行第 3 阶段
- 任何双加嵌号都会减少到一个加号,只要它们不加引号。但不幸的是,引用的插入符号仍然翻倍。
- 第 1 阶段略有变化 - 步骤 1.2 或 1.3 中的扩展错误会中止 CALL,但该错误不是致命的 - 批处理将继续。
- 第 2 阶段的任务略有变化
- 将检测到在第 2 阶段的第一轮中未检测到的任何新出现的未加引号、未转义的重定向,但会将其删除(包括文件名),而不实际执行重定向
- 删除行尾任何新出现的未加引号、未转义的插入符号,而不执行行继续
- 如果检测到以下任何一种情况,则 CALL 将中止而不会出错
- 新出现的未加引号、未转义或
&
|
- 生成的命令令牌以未加引号、未转义开头
(
- 删除 CALL 后的第一个令牌以
@
- 新出现的未加引号、未转义或
- 如果生成的命令是看似有效的 IF 或 FOR,则执行随后将失败,并显示错误,指出或未被识别为内部或外部命令。
IF
FOR
- 当然,如果生成的命令令牌是以 开头的标签,则 CALL 不会在第 2 阶段的第 2 轮中中止。
:
- 如果生成的命令令牌为 CALL,则重新启动第 6 阶段(重复,直到不再 CALL)
- 如果生成的命令令牌是批处理脚本或 :label,则 CALL 的执行完全由第 6 阶段的其余部分处理。
- 推送调用堆栈上的当前批处理脚本文件位置,以便在 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 true 或 false 命令块、FOR DO 命令块或涉及命令串联,则执行内部命令
- 否则(必须是批处理模式下的独立命令)扫描当前文件夹和 PATH 以查找 .COM、.EXE、.BAT 或 .基本名称与原始命令令牌匹配的 CMD 文件
- 如果第一个匹配文件是 .BAT 或 .CMD,然后转到 7.3.exec 并执行该脚本
- 否则(未找到匹配项或第一个匹配项是 .EXE 或 .COM)执行记住的内部命令
- 否则,在第一次出现之前断开命令令牌,或者
如果前面的文本不是内部命令,则转到 7.2
否则,前面的文本可能是内部命令。记住这个命令。.
\
:
- 在第一次出现之前断开命令令牌 或
如果前面的文本是现有文件的路径,则转到 7.2
否则执行记住的内部命令。+
/
[
]
<space>
<tab>
,
;
=
- 如果从较大的命令令牌分析内部命令,则命令令牌的未使用部分将包含在参数列表中
- 仅仅因为命令令牌被解析为内部命令并不意味着它将成功执行。每个内部命令都有自己的规则,说明如何分析参数和选项,以及允许使用什么语法。
- 如果检测到,所有内部命令都将打印帮助,而不是执行其功能。大多数人会识别它是否出现在参数中的任何位置。但是,一些命令(如 ECHO 和 SET)仅在第一个参数标记以 开头时才打印帮助。
/?
/?
/?
- SET有一些有趣的语义:
- 如果 SET 命令在启用
变量名称和扩展名之前有引号 --> value=
,则第一个等号和最后一个引号之间的文本将用作内容(排除第一个等号和最后一个引号)。最后一个引号后面的文本将被忽略。如果等号后没有引号,则将该行的其余部分用作内容。set "name=content" ignored
content
- 如果 SET 命令在名称
--> value=
之前没有引号,则将等于之后的整个行的其余部分用作内容,包括可能存在的任何和所有引号。set name="content" not ignored
"content" not ignored
- 如果 SET 命令在启用
- 评估 IF 比较,并根据条件是 true 还是 false,从第 5 阶段开始处理相应的已解析的依赖命令块。
- 对 FOR 命令的 IN 子句进行适当迭代。
- 如果这是循环访问命令块输出的 FOR /F,则:
- IN 子句通过 CMD /C 在新的 cmd.exe 进程中执行。
- 命令块必须再次完成整个解析过程,但这次是在命令行上下文中
- ECHO 将开始打开,延迟扩展通常会开始禁用(取决于注册表设置)
- 一旦子 cmd.exe 进程终止,IN 子句命令块所做的所有环境更改都将丢失
- 对于每次迭代:
- 定义了 FOR 变量值
- 然后,从第 4 阶段开始处理已解析的 DO 命令块。
- 如果这是循环访问命令块输出的 FOR /F,则:
- 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 都接受源路径和目标路径的通配符。但是Microsoft在记录通配符的工作方式方面做得很糟糕,尤其是对于目标路径。可以在 Windows 重命名命令如何解释通配符中找到一组有用的通配符规则?
- 进行以下测试以确定不带引号的命令令牌是否表示内部命令:
- 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 阶段)百分比扩展:
- 否等参数扩展
%*
%1
- 如果 var 未定义,则保持不变。
%var%
- 没有对 进行特殊处理。如果 var=content,则展开为 .
%%
%%var%%
%content%
第 3 阶段)回显解析的命令
- 在第 2 阶段之后不执行此操作。它仅在 FOR DO 命令块的第 4 阶段之后执行。
第 5 阶段)延迟扩容:仅开启延时扩容
- 如果 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) - 更多内容和编辑
评论
)
REM
) Ignore this
echo OK & ) Ignore this
如前所述,命令在 μ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"""]
测验:如何将任何环境 var 的值作为单个参数(即 )传递给 bat 文件?%1
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.) 字符。^
\
'
&
评论
t
a "b c
a
"
b
c
%1
.cmd
args "%t:"=""%"
百分比扩展规则
以下是 jeb 答案中对阶段 1 的扩展解释(对批处理模式和命令行模式都有效)。
第 1 阶段)扩展百分比 从左开始,扫描每个字符的 或 。如果找到,则%
<LF>
- 1.05(截断线
<LF
>) - 如果字符是 那么
<LF>
- 从开始删除(忽略)该行的其余部分
<LF>
- 转到阶段 2.0
- 从开始删除(忽略)该行的其余部分
- 否则字符必须是 ,因此请继续执行 1.1
%
- 1.1 (转义
%
) 如果命令行模式,则跳过 - 如果批处理模式后跟另一个模式,则
替换为单个并继续扫描%
%%
%
- 1.2 (展开参数) 如果命令行模式,则跳过
- 否则,如果批处理模式,则
- 如果后跟 和 命令扩展,则替换为所有命令行参数的文本(如果没有参数,则
不替换为任何内容)并继续扫描。*
%*
- 否则,如果后跟,则替换为参数值(如果未定义,则
替换为任何内容)并继续扫描。<digit>
%<digit>
- 否则,如果后跟 和 命令扩展,则
~
- 如果后跟可选的有效参数修饰符列表,后跟必需,则
替换为修改后的参数值(如果未定义或指定,则不替换为任何内容 $PATH:未定义修饰符)并继续扫描。
注意:修饰符不区分大小写,可以按任意顺序多次出现,但$PATH除外:修饰符只能出现一次,并且必须是< 位
之前的最后一个修饰符><digit>
%~[modifiers]<digit>
- 否则,无效的修改参数语法会引发致命错误:所有已解析的命令都将中止,如果处于批处理模式,批处理将中止!
- 如果后跟可选的有效参数修饰符列表,后跟必需,则
- 如果后跟 和 命令扩展,则替换为所有命令行参数的文本(如果没有参数,则
- 1.3 (展开变量)
- 否则,如果禁用了命令扩展,则
查看下一个字符串,在缓冲区之前或末尾中断,并将它们称为 VAR(可能是一个空列表)%
- 如果下一个字符是 那么
%
- 如果定义了 VAR,则
替换为 VAR 的值并继续扫描%VAR%
- 否则,如果是批处理模式,则
删除并继续扫描%VAR%
- 否则转到 1.4
- 如果定义了 VAR,则
- 否则转到 1.4
- 如果下一个字符是 那么
- 否则,如果启用了命令扩展,则
查看下一个字符串,在缓冲区之前或末尾中断,并将它们称为 VAR(可能是一个空列表)。如果 VAR 在之前中断,并且后续字符将作为 VAR 中的最后一个字符包含在 .%
:
:
%
:
%
- 如果下一个字符是 那么
%
- 如果定义了 VAR,则
替换为 VAR 的值并继续扫描%VAR%
- 否则,如果是批处理模式,则
删除并继续扫描%VAR%
- 否则转到 1.4
- 如果定义了 VAR,则
- 否则,如果下一个字符是那么
:
- 如果 VAR 未定义,则
- 如果是批处理模式,则
删除并继续扫描。%VAR:
- 否则转到 1.4
- 如果是批处理模式,则
- 否则,如果下一个字符是那么
~
- 如果下一个字符串与 然后
的模式匹配 替换为 VAR 值的子字符串(可能导致空字符串)并继续扫描。[integer][,[integer]]%
%VAR:~[integer][,[integer]]%
- 否则转到 1.4
- 如果下一个字符串与 然后
- 否则,如果后跟 or 然后
无效的变量搜索和替换语法会引发致命错误:所有解析的命令都将中止,如果处于批处理模式,批处理将中止!=
*=
- 否则,如果下一个字符串与 的模式匹配,其中搜索可以包括除 之外的任何字符集,而 replace 可以包括除 之外的任何字符集,则
在执行搜索和替换后替换为 VAR 的值(可能导致空字符串)并继续扫描[*]search=[replace]%
=
%
%VAR:[*]search=[replace]%
- 否则转到 1.4
- 如果 VAR 未定义,则
- 如果下一个字符是 那么
- 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!"
延迟扩展规则
以下是 jeb 答案中对第 5 阶段的扩展且更准确的解释(对批处理模式和命令行模式都有效)
第 5 阶段)延迟扩展
如果满足以下任一条件,则跳过此阶段:
- 延迟扩展被禁用。
- 该命令位于管道两侧的括号块内。
- 传入的命令令牌是“裸”批处理脚本,这意味着它不与 、 括号块、任何形式的命令串联 ( 或 ) 或管道关联。
CALL
&
&&
||
|
延迟扩展过程独立应用于令牌。一个命令可以有多个标记:
- 命令令牌。对于大多数命令,命令名称本身就是一个标记。但是,一些命令具有专门的区域,这些区域被视为第 5 阶段的 TOKEN。
for ... in(TOKEN) do
if defined TOKEN
if exists TOKEN
if errorlevel TOKEN
if cmdextversion TOKEN
if TOKEN comparison TOKEN
,其中 comparison 是 、 、 、 、 或 之一==
equ
neq
lss
leq
gtr
geq
- 参数令牌
- 重定向的目标令牌(每个重定向一个)
不会对不包含 的标记进行任何更改。!
对于每个包含至少一个 的标记,从左到右扫描每个字符的 或 ,如果找到,则!
^
!
- 5.1(插入符号转义)需要 for 或文本
!
^
- 如果字符是插入符号,则
^
- 删除
^
- 扫描下一个字符并将其保留为文字
- 继续扫描
- 删除
- 5.2 (展开变量)
- 如果字符是 ,则
!
- 如果命令扩展被禁用,则
查看下一个字符串,在 或 之前中断,并将它们称为 VAR(可能是一个空列表)!
<LF>
- 如果下一个字符是 那么
!
- 如果定义了 VAR,则
替换为 VAR 的值并继续扫描!VAR!
- 否则,如果是批处理模式,则
删除并继续扫描!VAR!
- 否则转到 5.2.1
- 如果定义了 VAR,则
- 否则转到 5.2.1
- 如果下一个字符是 那么
- 否则,如果启用了命令扩展,则
查看下一个字符串,在 、 或 之前换行,并将它们称为 VAR(可能是一个空列表)。如果 VAR 在之前中断,并且后续字符将作为 VAR 中的最后一个字符包含在 VAR 中,并在之前中断!
:
<LF>
:
!
:
!
- 如果下一个字符是 那么
!
- 如果 VAR 存在,则
替换为 VAR 的值并继续扫描!VAR!
- 否则,如果是批处理模式,则
删除并继续扫描!VAR!
- 否则转到 5.2.1
- 如果 VAR 存在,则
- 否则,如果下一个字符是那么
:
- 如果 VAR 未定义,则
- 如果是批处理模式,则
删除并继续扫描!VAR:
- 否则转到 5.2.1
- 如果是批处理模式,则
- 否则,如果下一个字符是那么
~
- 如果下一个字符串与 然后 的模式匹配 替换为 VAR 值的子字符串(可能导致空字符串)并继续扫描。
[integer][,[integer]]!
!VAR:~[integer][,[integer]]!
- 否则转到 5.2.1
- 如果下一个字符串与 然后 的模式匹配 替换为 VAR 值的子字符串(可能导致空字符串)并继续扫描。
- 否则,如果下一个字符串与 的模式匹配,其中搜索可以包括除 之外的任何字符集,而 replace 可以包括除 之外的任何字符集,则
在执行搜索和替换后替换为 VAR 的值(可能导致空字符串)并继续扫描[*]search=[replace]!
=
!
!VAR:[*]search=[replace]!
- 否则转到 5.2.1
- 如果 VAR 未定义,则
- 否则转到 5.2.1
- 如果下一个字符是 那么
- 5.2.1
- 如果是批处理模式,则删除前导 否则保留前导
!
!
- 继续扫描,从保留的行距后的下一个字符开始
!
- 如果是批处理模式,则删除前导 否则保留前导
- 如果命令扩展被禁用,则
评论
%definedVar:a=b%
%undefinedVar:a=b%
%var:~0x17,-010%
%<digit>
%*
%~
您在上面已经有一些很好的答案,但要回答您问题的一部分:
set a =b, echo %a %b% c% → bb c%
那里发生的事情是,因为你在 = 之前有一个空格,所以会创建一个变量,称为 so 当你被正确计算为 .%a<space>%
echo %a %
b
然后将剩余部分计算为纯文本 + 一个未定义的变量,该变量应按键入的方式回显,对我来说返回b% c%
% c%
echo %a %b% c%
bb% c%
我怀疑在变量名称中包含空格的能力与其说是计划中的“功能”,不如说是一种疏忽
请注意,Microsoft 已经发布了其终端的源代码。在语法解析方面,它的工作方式可能与命令行类似。也许有人有兴趣根据终端的解析规则测试逆向工程的解析规则。
评论
FOR
-循环元变量扩展 (Loop Meta-Variable Expansion)
这是对已接受答案(适用于批处理文件模式和命令行模式)中阶段 4) 的扩展说明。当然,命令必须处于活动状态。下面介绍子句后命令行部分的处理。请注意,在批处理文件模式下,由于上述立即扩展阶段(阶段 1)),已转换为该阶段。for
do
%%
%
%
- 扫描 -sign,从左到行尾;如果找到一个,则:
%
- 如果启用了命令扩展(默认),请检查下一个字符是否为 ;如果是,则:
~
- 在不区分大小写的集合中,尽可能多地获取以下字符(甚至每个字符多次),这些字符位于定义变量引用或 -sign 的字符之前;如果遇到这样的 -sign,则:
fdpnxsatz
for
$
$
- 扫描 1;如果找到,则:
:
- 如果后面有一个字符,则将其用作变量引用并按预期展开,除非未定义,否则不要展开并继续扫描该字符位置;
:
for
- 如果是最后一个字符,
cmd.exe
将崩溃!:
- 如果后面有一个字符,则将其用作变量引用并按预期展开,除非未定义,否则不要展开并继续扫描该字符位置;
- 否则(未找到)不展开任何内容;
:
- 扫描 1;如果找到,则:
- 否则(如果没有遇到 -sign)使用所有修饰符展开变量,除非未定义,否则不要展开并继续扫描该字符位置;
$
for
- 在不区分大小写的集合中,尽可能多地获取以下字符(甚至每个字符多次),这些字符位于定义变量引用或 -sign 的字符之前;如果遇到这样的 -sign,则:
- else(如果未找到或禁用了命令扩展),请检查下一个字符:
~
- 如果没有更多可用的字符,请不要扩展任何内容;
- 如果下一个字符是 ,则不要展开任何内容,然后返回到此字符位置2 的扫描开始
%
; - 否则使用下一个字符作为变量引用并展开,除非未定义,否则不要展开;
for
- 如果启用了命令扩展(默认),请检查下一个字符是否为 ;如果是,则:
- 在下一个字符位置返回扫描的开始(只要仍有可用的字符);
1)$
和:
之间的字符串被认为是环境变量的名称,甚至可能是空的;由于环境变量不能具有空名称,因此其行为与未定义的环境变量相同。
2) 这意味着没有 ~
-修饰符就无法扩展名为 %
的
元变量。
原始来源:如何安全地回显 FOR 变量 %%~p 后跟字符串文字
评论
%~$:<any-meta-var>
评论