Windows命令解释器(CMD.EXE)如何parsing脚本?
我碰到了ss64.com ,它提供了有关如何编写Windows命令解释器将运行的批处理脚本的很好的帮助。
但是,我一直无法很好地解释批处理脚本的语法 ,如何扩展或不扩展,以及如何逃避。
以下是我无法解决的示例问题:
- 报价系统如何pipe理? 我做了一个TinyPerl脚本
(foreach $i (@ARGV) { print '*' . $i ; }
),编译并调用它:-
my_script.exe "a ""b"" c"
→输出是*a "b*c
-
my_script.exe """abc"""
→输出*"a*b*c"
-
- 内部
echo
命令如何工作? 这个命令里面扩展了什么? - 为什么我必须在文件脚本中使用
for [...] %%I
,而在交互式会话中使用for [...] %I
? - 什么是转义字符,在什么情况下? 如何逃避百分号? 例如,我怎样才能从字面上回显
%PROCESSOR_ARCHITECTURE%
? 我发现echo.exe %""PROCESSOR_ARCHITECTURE%
作品,有没有更好的解决scheme? - 配对如何匹配? 例:
-
set b=a
,echo %a %b% c%
→%aac%
-
set a =b
,echo %a %b% c%
→bb c%
-
- 我如何确保一个variables传递给一个命令作为单个参数,如果这个variables包含双引号?
- 如何使用
set
命令存储variables? 例如,如果我set a=a" b
,然后echo.%a%
我会得到a" b
。 如果我使用UnxUtils中的echo.exe
,那么我得到ab
。%a%
如何以不同的方式扩展?
谢谢你的灯光。
我做了一些/很多的实验,这似乎是主要的结果。
为了更好地理解批处理是如何工作的,为什么有时候逃避工作和其他时间似乎失败了。 我通过很多实验来解决这个问题,而且我build立了testing,以便能够确定离散阶段的顺序。
有多个方面需要检查。 我得到了
- BatchLineParser – batch file中的分析器,用于行或块
- CmdLineParser – 与BatchLineParser类似,但直接在命令提示符下工作不同
- LabelParser – 如何调用/转到和标签工作
- CommandBlockCaching – 括号和caching如何工作
- Tokenizer – 单个令牌(字符组)如何构build以及在哪个阶段
BatchLineParser:
batch file中的一行代码有多个阶段(在命令行上扩展是不同的!)。
该过程从阶段1开始
相/订单
1)阶段(百分比):
- 双
%%
被replace为单个%
- 参数variables的扩展(
%1
,%2
等) -
%var%
扩展,如果var不存在,则replace为空 - 对于一个完整的解释从dbenham阅读这个同样的线程:百分比扩展
1.5)从行中删除所有的<CR>
(CarriageReturn 0x0d)
2)阶段(特殊字符, "
<LF>
^
&
|
<
>
(
)
:看每个字符
- 如果是报价(
"
)切换报价标志,如果报价标志处于活动状态,则以下特殊字符不再是特殊的:^
&
|
<
>
(
)
。 - 如果是插入符(
^
),则下一个字符没有特殊含义,如果插入符号是行的最后一个字符,则附加下一行,下一行的第一个字符总是作为逃跑了。-
<LF>
立即停止parsing,但没有在前面插入一个脱字符号
-
- 如果它是
&
的特殊字符之一<
>
在这一点上分割线,在pipe道(|
)的情况下,两个部分得到阶段重新启动(有点复杂…) 有关如何分析和处理pipe道的更多信息,看看这个问题和答案: 为什么在pipe道代码块内部延迟扩展会失败? - 在这个阶段主令牌列表是build立的,令牌分隔符是
<space>
<tab>
;
=
和<0xFF>
(也称为不间断空间) - 进程括号(提供跨多行的复合语句):
- 如果parsing器没有查找命令标记,那么
(
不是特别的。 - 如果parsing器正在查找命令标记并find
(
然后启动新的复合语句并递增括号计数器 - 如果括号计数器大于0,则终止复合语句并递减括号计数器。
- 如果到达行结束并且括号计数器大于0,则下一行将被添加到复合语句(再次从阶段1开始)
- 如果括号计数器= 0,并且parsing器正在查找命令,则函数就像
REM
语句一样,只要它紧跟着一个令牌定界符,换行符或文件结束符:行(令牌分隔符之后)被忽略。
- 如果parsing器没有查找命令标记,那么
- 在这个阶段REM,IF和FOR被检测到,以便对它们进行特殊处理。
- 如果第一个标记是“
rem
”,则只处理两个标记,这对于多行插入符号很重要
3)相位(回声):如果“回声打开”打印阶段1和2的结果
- For-loop-blocks被多次回声,第一次在for循环的环境中,与未扩展的for-loop-vars
- 对于每次迭代,该块都与扩展的for-loop-vars回显
—-这两个阶段并不是真正直接的,但没有区别
4)阶段(for-loop-vars扩展):扩展%%a
等等
5)阶段(感叹号):只有在延迟展开的时候,看每个字符
- 如果它是一个脱字号(
^
),下一个字符没有特殊含义,脱字符本身被删除 - 如果是感叹号,则search下一个感叹号(不再有逗号),扩展到variables的内容
- 连续开放
!
被折叠成一个单一的!
- 剩下的
!
不能配对的东西被删除
- 连续开放
- 如果在这个阶段没有发现感叹号,则结果将被丢弃,而是使用阶段4的结果(对于插入符来说是重要的)
- 重要提示:在此阶段,引号和其他特定字符将被忽略
- 在这个阶段扩大variables是“安全的”,因为不再检测到特殊字符(甚至
<CR>
或<LF>
)
6)阶段(呼叫/插入加倍):只有当cmd令牌是CALL
- 如果第一个标记是“
call
”,则再次从阶段1开始,但是在阶段2之后停止,延迟的扩展在这里不再被处理 - 删除第一个
CALL
,可以堆叠多个CALL - 双倍所有插入(正常插入似乎保持不变,因为在阶段2他们减less到一,但在报价中,他们有效地加倍)
7)阶段(执行):执行命令
- 这里使用不同的标记,取决于执行的内部命令
- 如果
set "name=content"
,则将该行的第一个等号到最后一个报价的完整内容用作内容标记,如果等号后没有报价,则使用该行的其余部分。
CmdLineParser:
像BatchLine-Parser一样工作,但是:
- 转到/呼叫标签是不允许的
阶段1(百分比):
- %var%将被replace为var的内容,如果var未定义,则expression式将保持不变
- %%没有特别的处理,第二个百分比可能是var的开始,set var = content,%% var %%扩展到%Content%
Phase5(感叹号):只有当“DelayedExpansion”被启用
- !VAR! 将被replace为var的内容,如果var未定义,则expression式将保持不变
for循环命令块
例如for /F "usebackq" %%a IN (
命令块) DO echo %%a
命令块将被parsing两次,首先是BatchLineParser(循环在批处理内)或CmdLineParser(在cmd行的循环)处于活动状态,第二次运行时总是CmdLineParser处于活动状态。 在第二次运行中,DelayedExpansion仅在启用了registry项的情况下处于活动状态
第二次运行就像使用cmd /c
调用行一样
variables的设置因此不是持久的。
希望能帮助Jan Erik
从命令窗口调用命令时,命令行参数的标记化不是由cmd.exe
(又名“shell”)完成的。 大多数情况下,标记是由新形成的进程的C / C ++运行时完成的,但这不一定是这样 – 例如,如果新进程不是用C / C ++编写的,或者新进程select忽略argv
和处理原始命令行(例如使用GetCommandLine() )。 在操作系统级别,Windows将未被指定为单个string的命令行传递给新进程。 这与大多数* nix shell相反,shell在将parameter passing给新形成的进程之前,以一致的,可预测的方式将参数标记为参数。 所有这一切意味着你可能会在Windows上的不同程序中经历非常不同的参数标记化行为,因为个别程序通常会将参数标记化转化为自己的手。
如果听起来像无政府状态,那就是。 但是,由于大量的Windows程序确实使用Microsoft C / C ++运行库的argv
,所以理解MSVCRT如何标记参数可能通常很有用。 这是一个摘录:
- 参数是由空格分隔的,空格是一个空格或一个制表符。
- 由双引号包围的string被解释为单个参数,而不pipe其中包含的空白。 带引号的string可以embedded到参数中。 请注意,脱字符(^)不被识别为转义字符或分隔符。
- 双引号前加一个反斜杠“\”,被解释为文字双引号(“)。
- 反斜杠从字面上解释,除非它们立即在双引号之前。
- 如果偶数个反斜杠之后是双引号,则对于每对反斜杠(\),将一个反斜杠()放在argv数组中,双引号(“)被解释为string分隔符。
- 如果奇数个反斜杠后面跟着一个双引号,则对于每对反斜杠(\),将一个反斜杠()放置在argv数组中,而双引号通过剩余的反斜杠解释为转义序列,从而导致一个文字双引号(“)被放置在argv。
微软的“批处理语言”( .bat
)也不例外,这个无政府状态的环境,它已经制定了自己独特的标记和转义规则。 它看起来像cmd.exe的命令提示符做的一些预处理命令行参数(主要是为了variablesreplace和转义),然后将parameter passing给新执行的进程。 您可以阅读更多关于批处理语言的低级别细节,并在本页面的jeb和dbenham的优秀答案中转义cmd。
让我们在C中构build一个简单的命令行工具,看看它对你的testing用例的看法:
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上testing。用Visual Studio 2005编译)
> test.exe "a ""b"" c" argv[1][a "b" c] > test.exe """abc""" argv[1]["abc"] > test.exe "a"" bc argv[1][a" bc]
还有一些我自己的testing:
> test.exe a "b" c argv[1][a] argv[2][b] argv[3][c] > test.exe a "bc" "de argv[1][a] argv[2][bc] argv[3][de] > test.exe a \"b\" c argv[1][a] argv[2]["b"] argv[3][c]
以下是jeb回答中阶段1的扩展说明(对于批处理模式和命令行模式均有效)。
1)(百分比)从左边开始,扫描每个字符的%
。 如果find了
- 如果命令行模式跳过 1.1(escape
%
)- 如果批处理模式和其他
%
然后
将%%
replace为单个%
并继续扫描
- 如果批处理模式和其他
- 1.2(扩展参数) 如果命令行模式被跳过
- 否则,如果批处理模式然后
- 如果后面跟有
*
和命令扩展名被启用,那么
将%*
replace为所有命令行参数的文本 - 否则,如果之后是
<digit>
将%<digit>
replace为参数值(如果未定义,则replace为无)并继续扫描 - 否则,如果后跟
~
,则命令扩展被启用- 如果后跟可选的有效参数修饰符列表,然后是所需的
<digit>
用修改的参数值replace%~[modifiers]<digit>
(如果没有定义,或者如果指定了$ PATH:modifier没有定义,则replace为无)并继续扫描。
注:修饰符不区分大小写,可以按任意顺序出现多次,除了$ PATH:修饰符只能出现一次,并且必须是<digit>
之前的最后一个修饰符。 - 否则无效的修改参数语法会引发致命错误:所有已parsing的命令都会中止,批处理将在批处理模式下中止!
- 如果后跟可选的有效参数修饰符列表,然后是所需的
- 如果后面跟有
- 否则,如果批处理模式然后
- 1.3(展开variables)
- 否则,如果命令扩展被禁用,那么
查看下一个string,在%
或<LF>
之前打破,并将它们称为VAR(可能是一个空列表)- 如果下一个字符是
%
那么- 如果VAR定义的话
将%VAR%
replace%VAR%
值并继续扫描 - 否则,如果批处理模式然后
删除%VAR%
并继续扫描 - 否则转到1.4
- 如果VAR定义的话
- 否则转到1.4
- 如果下一个字符是
- 否则,如果启用命令扩展那么
查看下一个string,在%
:
或<LF>
之前打破,并将它们称为VAR(可能是一个空列表)。 如果VAR在之前中断,并且后续字符为%
则包括:
作为VAR中的最后一个字符,并在%
之前中断。- 如果下一个字符是
%
那么- 如果VAR定义的话
将%VAR%
replace%VAR%
值并继续扫描 - 否则,如果批处理模式然后
删除%VAR%
并继续扫描 - 否则转到1.4
- 如果VAR定义的话
- 否则,如果下一个字符是
:
那么- 如果VAR是未定义的
- 如果批处理模式然后
删除%VAR:
并继续扫描。 - 否则转到1.4
- 如果批处理模式然后
- 否则如果下一个字符是
~
那么- 如果下一个string匹配
[integer][,[integer]]%
那么
将%VAR:~[integer][,[integer]]%
replace为VAR的子string(可能导致为空string)并继续扫描。 - 否则转到1.4
- 如果下一个string匹配
- 否则,如果后面跟着
=
或*=
那么
无效的variablessearch和replace语法引发致命错误:所有parsing的命令都会中止,批处理将在批处理模式下中止! - 否则,如果下一串字符匹配
[*]search=[replace]%
,其中search可能包括除了和<LF>
以外的任何字符集,replace可能包括除%
和<LF>
以外的任何字符集,则更换
%VAR:[*]search=[replace]%
执行search和replace后(可能导致空string),%VAR:[*]search=[replace]%
与VAR的值,并继续扫描 - 否则转到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 – 第一阶段在确认REM报表之前发生。 这是非常重要的,因为这意味着如果它有无效的参数扩展语法或无效的variablessearch和replace语法,甚至可以产生一个致命的错误!
@echo off rem %~x This generates a fatal argument expansion error echo this line is never reached
注2 – %parsing规则的另一个有趣的结果是:名称中包含:的variables可以被定义,但是除非命令扩展被禁用,否则它们不能被扩展。 有一个例外 – 当启用命令扩展时,可以扩展最后包含单个冒号的variables名称。 但是,您不能对以冒号结尾的variables名执行子string或search和replace操作。 下面的batch file(由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在他的文章中阐述的parsing规则顺序的一个有趣的结果是:当执行search并以正常扩展replace时,特殊字符不应该被转义(虽然它们可能被引用)。 但是当执行search并用延迟扩展replace时,特殊字符必须被转义(除非被引用)。
@echo off setlocal enableDelayedExpansion set "var=this & that" echo %var:&=and% echo "%var:&=and%" echo !var:^&=and! echo "!var:&=and!"
正如所指出的那样,命令被传递给μSoft域中的整个参数string,由他们来parsing这个单独的参数供自己使用。 不同的项目之间没有一致性,因此没有一套规则来描述这个过程。 你真的需要检查你的程序使用的任何C库的每个angular落案例。
至于系统.bat
文件去,这是testing:
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
现在我们可以运行一些testing。 看看你是否能够弄清楚μSoft正在做什么:
C>args abc cmdcmdline:[cmd.exe ] 0:[args] *:[abc] 1:[a] 2:[b] 3:[c]
目前为止很好。 (从现在起,我会省略无趣的%cmdcmdline%
和%0
。)
C>args *.* *:[*.*] 1:[*.*]
没有文件名扩展。
C>args "ab" c *:["ab" c] 1:["ab"] 2:[c]
没有报价剥离,虽然引号确实防止参数分裂。
c>args ""ab" c *:[""ab" c] 1:[""a] 2:[b" c]
连续的双引号导致它们失去了它们可能具有的任何特殊的parsing能力。 @ Beniot的例子:
C>args "a """ b "" c""" *:["a """ b "" c"""] 1:["a """] 2:[b] 3:[""] 4:[c"""]
测验:如何将任何环境variables的值作为单个参数(即, %1
)传递给bat文件?
c>set t=a "bc c>set t t=a "bc c>args %t% 1:[a] 2:["bc] c>args "%t%" 1:["a "b] 2:[c"] c>Aaaaaargh!
理智parsing似乎永远破碎。
为了您的娱乐,请尝试在这些示例中添加其他^
, \
, '
, &
(&c。)字符。
上面已经有一些很好的答案,但回答你的问题的一部分:
set a =b, echo %a %b% c% → bb c%
发生什么事情是因为在=之前有一个空格,所以创build一个名为%a<space>%
的variables,所以当你echo %a %
,正确评估为b
。
其余部分b% c%
然后被评估为纯文本+一个未定义的variables% c%
,应该被回显为input,对于我echo %a %b% c%
returns bb% c%
我怀疑在variables名中包含空格的能力比计划的“特性”
编辑:看到接受的答案,接下来是错误的,只解释了如何将命令行传递给TinyPerl。
关于报价,我觉得这个行为是这样的:
- 当一个
"
被find,string通配开始 - 当string发生时:
- 每一个不是
"
angular色都是"
- 当一个
"
被发现:- 如果后面跟着
""
(因此是一个三元组),那么双引号将被添加到string中 - 如果后面跟着
"
(因此是一个double"
),那么双引号将被添加到string和string结尾 - 如果下一个字符不是
"
,则string通配结束
- 如果后面跟着
- 当行结束时,string通配结束。
- 每一个不是
简而言之:
"a """ b "" c"""
由两个string组成: a " b "
和c"
如果在一行的末尾, "a"""
"a""
, "a"""
"a""
, "a"""
和"a""""
都是相同的string