Почему отложенное расширение завершается неудачно, когда внутри конвейерного блока кода? - PullRequest
30 голосов
/ 19 ноября 2011

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

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

Ответы [ 3 ]

37 голосов
/ 19 ноября 2011

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

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, обе стороны труб эквивалентны для фаз расширения.
Основные правила, по-видимому, таковы:

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

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

В моем мнении это «ошибка» или неуместное поведение, но это не первое

РЕДАКТИРОВАТЬ: Добавление трюка 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> остановит синтаксический анализ этой строки.

РЕДАКТИРОВАТЬ: асинхронный не полная правда

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

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

Я уже смутно знал, что канал выполняет как левую, так и правую стороны каждый в своей "сессии" CMD.EXE. Но ответы Аасини и Джеба заставили меня по-настоящему задуматься и изучить, что происходит с трубами. (Спасибо, Джеб, за то, что продемонстрировал, что происходит при подключении к 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: скобки отключают отложенное расширение! Очень странно и неожиданно для меня. Редактировать - Джеб считает это ошибкой 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, тогда я выберу это в качестве ответа на мой первоначальный вопрос. В противном случае я, вероятно, приму этот пост в качестве ответа (на самом деле это скорее наблюдение за происходящим, чем ответ) Редактировать - джеб его прибил!

<ч />

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

Этот пакетный скрипт:

@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-последовательностей в сложных сценариях трубы будет кошмаром.

8 голосов
/ 19 ноября 2011

Забавная вещь!Я не знаю ответа. Что я знаю, так это то, что в конвейерной работе возникают постоянные сбои в Windows Batch, которых не должно быть в исходном пакете 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

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

Мы можем использовать эту возможность дляразработать программу, эквивалентную Ожидаемое приложение (работающее аналогично pexpect Phyton module ), которое может управлять любой интерактивной программой следующим образом:

Input | anyprogram | Output

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

...