Why does delayed expansion fail when inside a piped block of code?

前端 未结 3 1171
粉色の甜心
粉色の甜心 2020-11-22 07:23

Here is a simple batch file that demonstrates how delayed expansion fails if it is within a block that is being piped. (The failure is toward the end of the script) Can anyo

3条回答
  •  南旧
    南旧 (楼主)
    2020-11-22 07:52

    As Aacini shows, it seems that many things fail within a pipe.

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

    But in reality it's only a problem to understand how the pipe works.

    Each side of a pipe starts its own cmd.exe in its own ascynchronous thread.
    That is the cause why so many things seem to be broken.

    But with this knowledge you can avoid this and create new effects

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

    Update 2019-08-15:
    As discovered at Why does `findstr` with variable expansion in its search string return unexpected results when involved in a pipe?, cmd.exe is only used if the command is internal to cmd.exe, if the command is a batch file, or if the command is enclosed in a parenthesized block. External commands not enclosed within parentheses are launched in a new process without the aid of cmd.exe.

    EDIT: In depth analysis

    As dbenham shows, both sides of the pipes are equivalent for the expansion phases.
    The main rules seems to be:

    The normal batch parser phases are done
    .. percent expansion
    .. special character phase/block begin detection
    .. delayed expansion (but only if delayed expansion is enabled AND it isn't a command block)

    Start the cmd.exe with C:\Windows\system32\cmd.exe /S /D /c""
    These expansions follows the rules of the cmd-line parser not the the batch-line parser.

    .. percent expansion
    .. delayed expansion (but only if delayed expansion is enabled)

    The will be modified if it's inside a parenthesis block.

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

    Called as C:\Windows\system32\cmd.exe /S /D /c" ( echo one %cmdcmdline% & echo two )", all newlines are changed to & operator.

    Why the delayed expansion phase is affected by parenthesis?
    I suppose, it can't expand in the batch-parser-phase, as a block can consist of many commands and the delayed expansion take effect when a line is executed.

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

    Obviously the !var! can't be evaluated in the batch context, as the lines are executed only in the cmd-line context.

    But why it can be evaluated in this case in the batch context?

    echo !var! | more
    

    In my opionion this is a "bug" or inconsitent behaviour, but it's not the first one

    EDIT: Adding the LF trick

    As dbenham shows, there seems to be some limitation through the cmd-behaviour that changes all line feeds into &.

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

    This results into
    C:\Windows\system32\cmd.exe /S /D /c" ( echo 7: part1 & rem This ...& echo part2 ) "
    The rem will remark the complete line tail, so even the closing bracket is missing then.

    But you can solve this with embedding your own line feeds!

    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
    

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

    And as the %lf% is expanded while parsing the parenthises by the parser, the resulting code looks like

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

    This %LF% behaviour works always inside of parenthesis, also in a batch file.
    But not on "normal" lines, there a single will stop the parsing for this line.

    EDIT: Asynchronously is not the full truth

    I said that the both threads are asynchronous, normally this is true.
    But in reality the left thread can lock itself when the piped data isn't consumed by the right thread.
    There seems to be a limit of ~1000 characters in the "pipe" buffer, then the thread is blocked until the data is consumed.

    @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=
        )
    )
    

提交回复
热议问题