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=aecho %a %b% c%%aac%
    • set a =becho %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语句一样,只要它紧跟着一个令牌定界符,换行符或文件结束符:行(令牌分隔符之后)被忽略。
  • 在这个阶段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
      • 否则转到1.4
    • 否则,如果启用命令扩展那么
      查看下一个string,在% :<LF>之前打破,并将它们称为VAR(可能是一个空列表)。 如果VAR在之前中断,并且后续字符为%则包括:作为VAR中的最后一个字符,并在%之前中断。
      • 如果下一个字符是%那么
        • 如果VAR定义的话
          %VAR%replace%VAR%值并继续扫描
        • 否则,如果批处理模式然后
          删除%VAR%并继续扫描
        • 否则转到1.4
      • 否则,如果下一个字符是:那么
        • 如果VAR是未定义的
          • 如果批处理模式然后
            删除%VAR:并继续扫描。
          • 否则转到1.4
        • 否则如果下一个字符是~那么
          • 如果下一个string匹配[integer][,[integer]]%那么
            %VAR:~[integer][,[integer]]%replace为VAR的子string(可能导致为空string)并继续扫描。
          • 否则转到1.4
        • 否则,如果后面跟着=*=那么
          无效的variablessearch和replace语法引发致命错误:所有parsing的命令都会中止,批处理将在批处理模式下中止!
        • 否则,如果下一串字符匹配[*]search=[replace]% ,其中search可能包括除了和<LF>以外的任何字符集,replace可能包括除%<LF>以外的任何字符集,则更换
          %VAR:[*]search=[replace]%执行search和replace后(可能导致空string), %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 – 第一阶段在确认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