为啥在管道代码块中延迟扩展会失败?

Posted

技术标签:

【中文标题】为啥在管道代码块中延迟扩展会失败?【英文标题】:Why does delayed expansion fail when inside a piped block of code?为什么在管道代码块中延迟扩展会失败? 【发布时间】:2012-01-01 19:13:50 【问题描述】:

这是一个简单的批处理文件,它演示了如果延迟扩展位于正在传输的块中,它是如何失败的。 (失败接近脚本的结尾)谁能解释这是为什么?

我有一个解决方法,但它需要创建一个临时文件。我最初在处理 Find files and sort by size in a Windows batch file

时遇到了这个问题
@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

【问题讨论】:

【参考方案1】:

正如 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!)

2019 年 8 月 15 日更新:正如在 Why does `findstr` with variable expansion in its search string return unexpected results when involved in a pipe? 发现的那样,仅当命令是 cmd 内部的时才使用 cmd.exe .exe,如果命令是批处理文件,或者命令包含在括号中的块中。未包含在括号中的外部命令将在没有 cmd.exe 帮助的情况下在新进程中启动。

编辑:深入分析

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

正常的批处理解析器阶段已完成.. 百分比扩展.. 特殊字符阶段/块开始检测。 . 延迟扩展(但仅在启用延迟扩展且它不是命令方块的情况下)

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

.. 百分比扩展.. 延迟扩展(但仅在启用延迟扩展的情况下)

如果<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! 无法在批处理上下文中进行评估,因为这些行仅在命令行上下文中执行。

但是为什么在这种情况下可以在批处理上下文中对其进行评估?

echo !var! | more

在我看来,这是一个“错误”或不一致的行为,但它不是第一个

编辑:添加 LF 技巧

正如 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 /p x=
    )
)

【讨论】:

+1 感谢 jeb 终于让我看到 SET /P 正在发生的事情。我曾经认为这是它读取输入的方式的一个特点。现在我意识到它确实在工作,但是我们当然不能将结果传递回父环境,所以它有点没用。 这个答案仍然没有解释为什么延迟扩展在 CMD 上下文中被禁用。请参阅我自己对这个问题的“答案”。 +1 非常有趣!此行为修改了一些基本的批处理规则:cd . | BatSub 在 BatSub.bat 结束后返回到当前批处理文件,即使它不是通过CALLCMD /C 调用的(我们现在知道这里有一个隐含的CMD /C)。此外,我们现在知道进行两次重定向 com1 > file & com2 < file 而不是管道更快:com1 | com2;从现在开始,我将避免使用管道来支持两个重定向。所有这些东西对我来说听起来很奇怪! @jeb:只是一个细节,管道右侧的执行不是异步的...... @jeb:你是对的!管道中双方的执行都是异步的!请参阅我的答案中的附录(每次都会变得陌生......) 好东西杰布。现在一切都变得有意义了。%%cmcmdline%% 技巧确实有助于解释事情。还有一件事没有提到:CMD.EXE 不继承父延迟扩展状态;它根据注册表设置默认此状态。大概命令扩展状态也是如此。【参考方案2】:

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

这里有一些例子:

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 Batch 功能有关: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

第二个.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 application 等效的程序(以与pexpect Phyton module 类似的方式工作),以这种方式控制任何交互式程序:

Input | anyprogram | Output

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

【讨论】:

jeb 的回答解释了为什么管道不能调用批处理 :function - CALL :FUNCTION 命令不在批处理上下文中执行,因此它不可能工作。 您对异步行为的编辑很好,但并不完全正确。即使管道首先在 cmd 上下文中启动,但如果您从那里开始批处理,您将再次处于批处理上下文中。 @jeb:你又是对的!当我开发我的示例时,发生了一个奇怪的错误,我对此感到困惑:(我更正了我的示例,删除了有关上下文的错误文本并添加了一个可能的应用程序:一个类似于 Expect 的程序。【参考方案3】:

我不确定是否应该编辑我的问题,或者将其发布为答案。

我已经模糊地知道管道在其自己的 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!输出表明这些命令是在命令行上下文中执行的,而不是在批处理上下文中。换句话说,使用命令行解析规则而不是批量解析。 (见How does the Windows Command Interpreter (CMD.EXE) parse scripts?)编辑-!VAR2!在父批处理上下文中展开

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

测试 3L、3R: setlocal EnableDelayedExpansion 不起作用。但这是意料之中的,因为我们处于命令行上下文中。 setlocal 仅适用于批处理上下文。

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

测试 5L,5R: 几乎与 4L,4R 相同,只是在执行 CMD /V:on 时已启用延迟扩展。 %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:展示了管道与多行块一起工作的方式的有趣副作用。谨防!

我必须相信,在复杂的管道场景中找出转义序列将是一场噩梦。

【讨论】:

+1,我喜欢详尽的测试,但您的某些结论似乎是错误的。恕我直言,您对 1LR 和 5LR 的解释是错误的 @jeb ???我的分析哪里出错了?特别是关于 1LR,因为 3LR 的结果似乎支持我的结论。 5LR 对我来说仍然是个谜。 我编辑了我的答案,希望它能解释现在的完整行为:-) @jeb - 绝对!现在一切都说得通了。看看我在最后的附录中展示的一些副作用。它们处理如何处理多行块。真恶心!但这一切都说得通。 我在我的回答中添加了一个解决方案,使用 REM 或引号来解决 &amp; 行为

以上是关于为啥在管道代码块中延迟扩展会失败?的主要内容,如果未能解决你的问题,请参考以下文章

为啥内部类的扩展会得到重复的外部类引用?

使用 !ERRORLEVEL!在通过管道传输到另一个进程的代码块中

为啥我的代码在完成块中运行两次

我如何知道从延迟块中释放了哪个代理?

有没有一种简单的方法可以从 AFHTTPClient 获取失败块中的 http 状态代码?

Java 线程中调用wait为啥一定要在同步代码块中?