为什么在管道代码块内部延迟扩展会失败?

这里是一个简单的批处理文件,演示如果延迟扩展失败,如果它在一个正在管道的块内。 (失败是脚本的结尾)任何人都可以解释为什么这是?

我有一个解决方法,但它需要创建一个临时文件。 我最初遇到这个问题,同时在Windows批处理文件中查找文件和按大小排序

@echo off setlocal enableDelayedExpansion set test1=x set test2=y set test3=z echo( echo NORMAL EXPANSION TEST echo Unsorted works ( echo %test3% echo %test1% echo %test2% ) echo( echo Sorted works ( echo %test3% echo %test1% echo %test2% ) | sort echo( echo --------- echo( echo DELAYED EXPANSION TEST echo Unsorted works ( echo !test3! echo !test1! echo !test2! ) echo( echo Sorted fails ( echo !test3! echo !test1! echo !test2! ) | sort echo( echo Sort workaround ( echo !test3! echo !test1! echo !test2! )>temp.txt sort temp.txt del temp.txt 

这是结果

 NORMAL EXPANSION TEST Unsorted works z x y Sorted works x y z --------- DELAYED EXPANSION TEST Unsorted works z x y Sorted fails !test1! !test2! !test3! Sort workaround x y z 

    正如Aacini所表明的那样,许多事情似乎在一个管道中失败了。

     echo hello | set /p var= echo here | call :function 

    但实际上,了解管道是如何工作的只是一个问题。

    管道的每一侧都在自己的异步线程中启动它自己的cmd.exe。
    这就是为什么这么多东西似乎被打破的原因。

    但有了这些知识,你可以避免这种情况,并创造新的效果

     echo one | ( set /p varX= & set varX ) set var1=var2 set var2=content of two echo one | ( echo %%%var1%%% ) echo three | echo MYCMDLINE %%cmdcmdline%% echo four | (cmd /v:on /c echo 4: !var2!) 

    编辑:深入分析

    正如dbenham所示,管道的两侧在膨胀阶段是等效的。
    主要规则似乎是:

    正常的批解析器阶段完成
    百分比扩张
    特殊字符相位/块开始检测
    ..延迟扩展(但只有当延迟扩展启用,它不是一个命令块)

    C:\Windows\system32\cmd.exe /S /D /c"<BATCH COMMAND>"启动cmd.exe
    这些扩展遵循cmd行解析器的规则,而不是批处理行解析器。

    百分比扩张
    ..延迟扩展(但只有当延迟扩展启用)

    如果在一个圆括号内, <BATCH COMMAND>将被修改。

     ( echo one %%cmdcmdline%% echo two ) | more 

    称为C:\Windows\system32\cmd.exe /S /D /c" ( echo one %cmdcmdline% & echo two )" ,所有换行符都改为&运算符。

    为什么延期扩张阶段受括号的影响?
    我想,它不能在批处理解析器阶段展开,因为一个块可以由许多命令组成,并且延迟扩展在执行一行时生效。

     ( set var=one echo !var! set var=two ) | more 

    很显然!var! 无法在批处理上下文中进行评估,因为这些行仅在cmd行上下文中执行。

    但是为什么在这种情况下可以在批处理环境中进行评估?

     echo !var! | more 

    在我的观点中,这是一个“错误”或不切实际的行为,但它不是第一个

    编辑:添加低频技巧

    正如dbenham所表明的那样,通过cmd行为似乎有一些限制,将所有的换行变为&

     ( echo 7: part1 rem This kills the entire block because the closing ) is remarked! echo part2 ) | more 

    这导致
    C:\Windows\system32\cmd.exe /S /D /c" ( echo 7: part1 & rem This ...& echo part2 ) "
    rem会标出完整的行尾,所以即使是右括号也是如此。

    但是你可以通过嵌入自己的换行来解决这个问题!

     set LF=^ REM The two empty lines above are required ( echo 8: part1 rem This works as it splits the commands %%LF%% echo part2 ) | more 

    这将导致C:\Windows\system32\cmd.exe /S /D /c" ( echo 8: part1 %cmdcmdline% & rem This works as it splits the commands %LF% echo part2 )"

    而且由于%lf%在解析器解析父项时被展开,所以得到的代码看起来像

     ( echo 8: part1 & rem This works as it splits the commands echo part2 ) 

    这个%LF%行为总是在括号内,也在批处理文件中工作。
    但不是在“正常”的行,有一个<linefeed>将停止解析这一行。

    编辑:异步不是完整的事实

    我说这两个线程是异步的,通常这是真的。
    但实际上,当管道数据没有被正确的线程使用时,左边的线程可以锁定自己。
    在“管道”缓冲区中似乎有约1000个字符的限制,然后线程被阻塞,直到数据被消耗。

     @echo off ( ( for /L %%a in ( 1,1,60 ) DO ( echo A long text can lock this thread echo Thread1 ##### %%a > con ) ) echo Thread1 ##### end > con ) | ( for /L %%n in ( 1,1,6) DO @( ping -n 2 localhost > nul echo Thread2 ..... %%n set /px= ) ) 

    有趣的事情! 我不知道答案,我知道的是,管道操作在Windows批处理中应该不存在原始MS-DOS批处理(如果这样的功能可以在旧的MS-DOS批处理中执行)一致失败,所以我怀疑在开发新的Windows批处理功能时引入了错误。

    这里有些例子:

    echo Value to be assigned | set /p var=

    上一行不会将值赋给变量,所以我们必须这样修复它:

    echo Value to be assigned > temp.txt & set /p var=< temp.txt

    另一个:

     ( echo Value one echo Value two echo Value three ) | call :BatchSubroutine 

    不起作用。 这样修复:

     ( echo Value one echo Value two echo Value three ) > temp.txt call :BatchSubroutine < temp.txt 

    但是,这种方法在某些情况下工作。 以DEBUG.COM为例:

     echo set tab=9> def_tab.bat ( echo e108 echo 9 echo w echo q ) | debug def_tab.bat call def_tab echo ONE%tab%TWO 

    以前的节目表演:

    ONE TWO

    在哪些情况下工作,哪些不是? 只有上帝(和微软)可能知道,但它似乎与新的Windows批量特性有关:SET / P命令,延迟扩展,括号内的代码块等。

    编辑:异步批处理文件

    :我修改了这个部分来纠正我的错误。 看到我最后一个评论jeb的细节。

    正如jeb所说,管道两端的执行会创建两个异步进程,即使不使用START命令,也可以执行异步线程。

    Mainfile.bat:

     @echo off echo Main start. Enter lines, type end to exit First | Second echo Main end 

    First.bat:

     @echo off echo First start :loop set /P first= echo First read: %first% if /I not "%first%" == "end" goto loop echo EOF echo First end 

    Second.bat:

     @echo off echo Second start :loop set /P second=Enter line: echo Second read: %second% echo/ if not "%second%" == "EOF" goto loop echo Second end 

    我们可以使用这个功能来开发一个相当于Expect应用程序的程序 (以类似Phyton模块的方式工作),这样可以控制任何交互式程序:

     Input | anyprogram | Output 

    Output.bat文件将通过分析程序的输出来实现“Expect”部分,Input.bat将通过向程序提供输入来实现“Sendline”部分。 输出到输入模块的后向通信将通过具有所需信息的文件和通过存在/不存在一个或两个标志文件来控制的简单信号量系统来实现。

    我不确定是否应该编辑我的问题,或张贴这个答案。

    我已经隐约知道一个管道在自己的CMD.EXE“会话”中执行左右两边。 但是Aacini和jeb的回应迫使我真正思考和调查管道正在发生的事情。 (谢谢jeb演示当SET / P管道进入时发生了什么!)

    我开发了这个调查脚本 – 这有助于解释很多,但也表现出一些奇怪和意想不到的行为。 我将发布脚本,然后输出。 最后我会提供一些分析。

     @echo off cls setlocal disableDelayedExpansion set var1=value1 set "var2=" setlocal enableDelayedExpansion echo on @echo NO PIPE - delayed expansion is ON echo 1: %var1%, %var2%, !var1!, !var2! (echo 2: %var1%, %var2%, !var1!, !var2!) @echo( @echo PIPE LEFT SIDE - Delayed expansion is ON echo 1L: %%var1%%, %%var2%%, !var1!, !var2! | more (echo 2L: %%var1%%, %%var2%%, !var1!, !var2!) | more (setlocal enableDelayedExpansion & echo 3L: %%var1%%, %%var2%%, !var1!, !var2!) | more (cmd /v:on /c echo 4L: %%var1%%, %%var2%%, !var1!, !var2!) | more cmd /v:on /c echo 5L: %%var1%%, %%var2%%, !var1!, !var2! | more @endlocal @echo( @echo Delayed expansion is now OFF (cmd /v:on /c echo 6L: %%var1%%, %%var2%%, !var1!, !var2!) | more cmd /v:on /c echo 7L: %%var1%%, %%var2%%, !var1!, !var2! | more @setlocal enableDelayedExpansion @echo( @echo PIPE RIGHT SIDE - delayed expansion is ON echo junk | echo 1R: %%var1%%, %%var2%%, !var1!, !var2! echo junk | (echo 2R: %%var1%%, %%var2%%, !var1!, !var2!) echo junk | (setlocal enableDelayedExpansion & echo 3R: %%var1%%, %%var2%%, !var1!, !var2!) echo junk | (cmd /v:on /c echo 4R: %%var1%%, %%var2%%, !var1!, !var2!) echo junk | cmd /v:on /c echo 5R: %%var1%%, %%var2%%, !var1!, !var2! @endlocal @echo( @echo Delayed expansion is now OFF echo junk | (cmd /v:on /c echo 6R: %%var1%%, %%var2%%, !var1!, !var2!) echo junk | cmd /v:on /c echo 7R: %%var1%%, %%var2%%, !var1!, !var2! 

    这是输出

     NO PIPE - delayed expansion is ON C:\test>echo 1: value1, , !var1!, !var2! 1: value1, , value1, C:\test>(echo 2: value1, , !var1!, !var2! ) 2: value1, , value1, PIPE LEFT SIDE - Delayed expansion is ON C:\test>echo 1L: %var1%, %var2%, !var1!, !var2! | more 1L: value1, %var2%, value1, C:\test>(echo 2L: %var1%, %var2%, !var1!, !var2! ) | more 2L: value1, %var2%, !var1!, !var2! C:\test>(setlocal enableDelayedExpansion & echo 3L: %var1%, %var2%, !var1!, !var2! ) | more 3L: value1, %var2%, !var1!, !var2! C:\test>(cmd /v:on /c echo 4L: %var1%, %var2%, !var1!, !var2! ) | more 4L: value1, %var2%, value1, !var2! C:\test>cmd /v:on /c echo 5L: %var1%, %var2%, !var1!, !var2! | more 5L: value1, %var2%, value1, Delayed expansion is now OFF C:\test>(cmd /v:on /c echo 6L: %var1%, %var2%, !var1!, !var2! ) | more 6L: value1, %var2%, value1, !var2! C:\test>cmd /v:on /c echo 7L: %var1%, %var2%, !var1!, !var2! | more 7L: value1, %var2%, value1, !var2! PIPE RIGHT SIDE - delayed expansion is ON C:\test>echo junk | echo 1R: %var1%, %var2%, !var1!, !var2! 1R: value1, %var2%, value1, C:\test>echo junk | (echo 2R: %var1%, %var2%, !var1!, !var2! ) 2R: value1, %var2%, !var1!, !var2! C:\test>echo junk | (setlocal enableDelayedExpansion & echo 3R: %var1%, %var2%, !var1!, !var2! ) 3R: value1, %var2%, !var1!, !var2! C:\test>echo junk | (cmd /v:on /c echo 4R: %var1%, %var2%, !var1!, !var2! ) 4R: value1, %var2%, value1, !var2! C:\test>echo junk | cmd /v:on /c echo 5R: %var1%, %var2%, !var1!, !var2! 5R: value1, %var2%, value1, Delayed expansion is now OFF C:\test>echo junk | (cmd /v:on /c echo 6R: %var1%, %var2%, !var1!, !var2! ) 6R: value1, %var2%, value1, !var2! C:\test>echo junk | cmd /v:on /c echo 7R: %var1%, %var2%, !var1!, !var2! 7R: value1, %var2%, value1, !var2! 

    我测试了管道的左侧和右侧以证明处理是双方对称的。

    试验1和2表明,在正常的批次情况下,括号对延迟膨胀没有任何影响。

    测试1L,1R:延时扩展按预期工作。 Var2是未定义的,所以%var2%和!var2! 输出表明这些命令是在命令行上下文中执行的,而不是批处理上下文。 换句话说,使用命令行解析规则而不是批量解析。 (请参阅Windows命令解释器(CMD.EXE)如何解析脚本? ) 编辑 – !VAR2! 在父批处理中进行扩展

    测试2L,2R:括号禁用延迟扩展! 在我看来,这是非常奇怪的和意外的。 编辑 – jeb认为这是一个MS错误或设计缺陷。 我同意,不一致的行为似乎没有任何理性的原因

    测试3L,3R: setlocal EnableDelayedExpansion不起作用。 但这是预料之中的,因为我们在命令行上下文中。 setlocal只能在批处理上下文中使用。

    测试4L,4R:最初启用延迟扩展,但括号禁用它。 CMD /V:ON重新启用延迟扩展,一切按预期工作。 我们仍然有命令行上下文,输出和预期一样。

    测试5L,5R:执行CMD /V:on时,除了延迟扩展已经启用外,几乎与4L,4R相同。 %var2%给出预期的命令行上下文输出。 但是!var2! 输出为空白,这在批处理环境中是预期的。 这是另一个非常奇怪和意外的行为。 编辑 – 实际上这是有道理的,我知道!var2! 在父批处理中进行扩展

    测试6L,6R,7L,7R:这些与测试4L / R,5L / R类似,除了现在延迟扩展开始禁用。 这次所有4个场景给出了预期的!var2! 批量上下文输出。

    如果有人能为2L,2R和5L,5R的结果提供一个合理的解释,那么我将选择它作为我原来问题的答案。 否则,我可能会接受这个帖子作为答案(真正更多的是观察发生了什么比答案) 编辑 – 刺戳钉!


    附录:为了回应jeb的评论 – 这里有更多的证据表明批处理中的管道命令在命令行上下文中执行,而不是在批处理上下文中执行。

    这个批处理脚本:

     @echo on call echo batch context %%%% call echo cmd line context %%%% | more 

    给出这个输出:

     C:\test>call echo batch context %% batch context % C:\test>call echo cmd line context %% | more cmd line context %% 


    最终附录

    我已经添加了一些额外的测试和结果,证明迄今为止的所有发现。 我还演示了FOR变量扩展发生在管道处理之前。 最后,当多行块折叠成一行时,显示管道处理的一些有趣的副作用。

     @echo off cls setlocal disableDelayedExpansion set var1=value1 set "var2=" setlocal enableDelayedExpansion echo on @echo( @echo Delayed expansion is ON echo 1: %%, %%var1%%, %%var2%%, !var1!, ^^^!var1^^^!, !var2!, ^^^!var2^^^!, %%cmdcmdline%% | more (echo 2: %%, %%var1%%, %%var2%%, !var1!, ^^^!var1^^^! !var2!, %%cmdcmdline%%) | more for %%a in (Z) do (echo 3: %%a %%, %%var1%%, %%var2%%, !var1!, ^^^!var1^^^! !var2!, %%cmdcmdline%%) | more ( echo 4: part1 set "var2=var2Value set var2 echo " set var2 ) ( echo 5: part1 set "var2=var2Value set var2 echo " set var2 echo --- begin cmdcmdline --- echo %%cmdcmdline%% echo --- end cmdcmdline --- ) | more ( echo 6: part1 rem Only this line remarked echo part2 ) ( echo 7: part1 rem This kills the entire block because the closing ) is remarked! echo part2 ) | more 

    这是输出

     Delayed expansion is ON C:\test>echo 1: %, %var1%, %var2%, !var1!, ^!var1^!, !var2!, ^!var2^!, %cmdcmdline% | more 1: %, value1, %var2%, value1, !var1!, , !var2!, C:\Windows\system32\cmd.exe /S /D /c" echo 1: %, %var1%, %var2%, value1, !var1!, , !var2!, %cmdcmdline% " C:\test>(echo 2: %, %var1%, %var2%, !var1!, ^!var1^! !var2!, %cmdcmdline% ) | more 2: %, value1, %var2%, !var1!, !var1! !var2!, C:\Windows\system32\cmd.exe /S /D /c" ( echo 2: %, %var1%, %var2%, !var1!, ^!var1^! !var2!, %cmdcmdline% )" C:\test>for %a in (Z) do (echo 3: %a %, %var1%, %var2%, !var1!, ^!var1^! !var2!, %cmdcmdline% ) | more C:\test>(echo 3: Z %, %var1%, %var2%, !var1!, ^!var1^! !var2!, %cmdcmdline% ) | more 3: Z %, value1, %var2%, !var1!, !var1! !var2!, C:\Windows\system32\cmd.exe /S /D /c" ( echo 3: Z %, %var1%, %var2%, !var1!, ^!var1^! !var2!, %cmdcmdline% )" C:\test>( echo 4: part1 set "var2=var2Value set var2 echo " set var2 ) 4: part1 var2=var2Value " var2=var2Value C:\test>( echo 5: part1 set "var2=var2Value set var2 echo " set var2 echo --- begin cmdcmdline --- echo %cmdcmdline% echo --- end cmdcmdline --- ) | more 5: part1 var2=var2Value & set var2 & echo --- begin cmdcmdline --- C:\Windows\system32\cmd.exe /S /D /c" ( echo 5: part1 & set "var2=var2Value var2=var2Value & set var2 & echo " & set var2 & echo --- begin cmdcmdline --- & echo %cmdcmdline% & echo --- end cmdcmdline --- )" --- end cmdcmdline --- C:\test>( echo 6: part1 rem Only this line remarked echo part2 ) 6: part1 part2 C:\test>(echo %cmdcmdline% & ( echo 7: part1 rem This kills the entire block because the closing ) is remarked! echo part2 ) ) | more 

    测试1:和2:总结所有的行为,%% cmdcmdline %%技巧确实有助于展示正在发生的事情。

    测试3:演示FOR可变扩展仍然适用于管道块。

    测试4:/ 5:和6:/ 7:显示管道在多线路模块中的工作方式的有趣副作用。 谨防!

    我必须相信,在复杂的管道情况下计算逃生序列将是一场噩梦。