Почему задержка с расширением заканчивается, когда внутри блока кода?

Вот простой командный файл, который демонстрирует, как с задержкой расширения происходит сбой, если он находится в блоке, который передается по трубопроводу. (Ошибка заканчивается в конце script). Может ли кто-нибудь объяснить, почему это?

У меня есть работа, но для этого требуется создание временного файла. Сначала я столкнулся с этой проблемой при работе над Поиск файлов и сортировка по размеру в пакетном файле 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

Ответ 1

Как показывает Аасини, кажется, что многие вещи терпят неудачу в pipeе.

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

Но на самом деле это только проблема, чтобы понять, как работает pipe.

Каждая сторона канала запускает свой собственный 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-08-15:
Как выяснилось в , почему 'findstr' с расширением переменной в строке поиска возвращает неожиданные результаты при подключении к каналу?, cmd.exe используется только в том случае, если команда является внутренней для cmd.exe, если команда пакетный файл, или если команда заключена в блок в скобках. Внешние команды, не заключенные в скобки, запускаются в новом процессе без помощи cmd.exe.

ОБНОВЛЕНИЕ: углубленный анализ

Как показывает Дбенхем, обе стороны pipe эквивалентны фазам расширения.
Основные правила выглядят так:

Обычные фазы парсера выполняются
.. процентное расширение
.. обнаружение фазы специального символа/начала блока
.. отложенное расширение (но только если включено отложенное расширение И это не командный блок)

Запустите cmd.exe с помощью C:\Windows\system32\cmd.exe /S /D /c"<BATCH COMMAND>"
Эти расширения следуют правилам парсера cmd-строки, а не парсера batch-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! нельзя оценить в контексте пакета, поскольку строки выполняются только в контексте строки 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> остановит разбор этой строки.

ОБНОВЛЕНИЕ: асинхронно не полная правда

Я сказал, что оба потока асинхронны, обычно это так.
Но на самом деле левый поток может блокироваться, когда данные по каналу не используются правым потоком.
Кажется, в буфере "pipe" существует ограничение в ~ 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=
    )
)

Ответ 2

Я не был уверен, должен ли я отредактировать свой вопрос или разместить его в качестве ответа.

Я уже смутно знал, что труба выполняет как левую, так и правую сторону каждый в своем собственном сеансе CMD.EXE. Но ответы Аасини и Джеба заставили меня по-настоящему подумать и исследовать, что происходит с трубами. (Спасибо, что продемонстрировали, что происходит при подключении в SET/P!)

Я разработал этот исследовательский script - он помогает объяснить многое, но также демонстрирует какое-то странное и неожиданное поведение. Я выведу текст script, а затем вывод. Наконец, я дам некоторый анализ.

@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 - undefined, поэтому% var2% и! Var2! вывод показывает, что команды выполняются в контексте командной строки, а не в контексте пакета. Другими словами, вместо анализа партитуры используются правила синтаксического анализа командной строки. (см. Как интерпретирует скрипт командной строки Windows (CMD.EXE)?) EDIT -! 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, тогда я выберу это как ответ на свой первоначальный вопрос. В противном случае я, вероятно, соглашусь с этим сообщением в качестве ответа (на самом деле больше наблюдает за тем, что происходит, чем с ответом) Edit - jab прибил его!


Добавление: В ответ на комментарий jeb - это больше доказательств того, что команды в пакете в пакетном исполнении выполняются в контексте командной строки, а не в контексте пакета.

Эта партия script:

@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: показывают интересные побочные эффекты того, как трубы работают с многострочными блоками. Осторожно!

Я должен верить, что выяснение escape-последовательностей в сложных сценариях будет кошмаром.

Ответ 3

Смешная штука! Я не знаю ответа, я знаю, что операция с конвейером имеет последовательные сбои в пакете Windows, которые не должны присутствовать в исходной пачке MS-DOS (если такие функции могут выполняться в старой версии MS-DOS), поэтому я что ошибка была введена, когда были разработаны новые функции 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

Однако этот метод DO работает в определенных случаях; с 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

В каких случаях работает, а какие нет? Только Бог (и Microsoft) может знать, но, похоже, это связано с новыми функциями пакетной обработки Windows: команда SET/P, замедленное расширение, блок кода в круглых скобках и т.д.

EDIT: асинхронные пакетные файлы

ПРИМЕЧАНИЕ. Я изменил этот раздел, чтобы исправить мою ошибку. См. Мой последний комментарий к 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 application (аналогичным образом работает pexpect Phyton module), который мог бы управлять любой интерактивной программой таким образом:

Input | anyprogram | Output

Файл Output.bat достигнет части "Ожидание", проанализировав выход из программы, а Input.bat достигнет части "Sendline", предоставив вход для программы. Обратная связь с выходами на модули ввода будет достигнута посредством файла с требуемой информацией и простой системы семафора, контролируемой наличием/отсутствием одного или двух файлов флагов.