Получить %ERRORLEVEL% со сложным вызовом в цикле FOR

В пакетном файле Windows мне нужно было получить результат выполнения программы в переменную, когда вызов программы был усложнен пробелами и несколькими опциями. После долгих обсуждений был найден обходной путь с использованием CALL:

FOR /F "delims=" %%G IN ('CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah"') do set foo=%%G

См. следующий вопрос для получения более подробной информации и понимания контекста:

Получить вывод команды в переменную, если в команде есть пробелы

На самом деле пакетный файл вызывает PostgreSQL 9.3, например:

SET PSQL_EXE=C:\Program Files\Foo\Bar\PostgreSQL\9.3\bin\psql.exe
SET FOO=1
REM psql query should result in a 0 or 1 based on the mydbproperty table value
FOR /F %%G IN ('call "%PSQL_EXE%" -U pguser -d MyDB -p %PG_PORT% -c "select string_value from mydb.uri as uri, mydb.property as prop where uri.id = prop.property_uriid and uri.namespace_uri='http://example.com/foo/bar/' and uri.simplename = 'fooBar'" -q -t -w') DO SET FOO=%%G
REM next line doesn't have ERRORLEVEL set
IF !ERRORLEVEL! NEQ 0 EXIT !ERRORLEVEL!

К сожалению, похоже, что этот формат приводит к отдельному экземпляру cmd, поэтому любая ошибка, возникшая при вызове pgsql (например, отсутствие файла паролей), не передается обратно, и %ERRORLEVEL% не устанавливается. В частности, pgsql выводит:

psql: fe_sendauth: no password supplied

Но %ERRORLEVEL%!ERRORLEVEL!) по-прежнему 0.

См. следующий вопрос для получения дополнительной информации:

Уровень ошибки PSQL в пакетном цикле

Итак, теперь вопрос в том, как узнать %ERRORLEVEL%, теперь, когда мне удалось получить ответ psql. Я бы предпочел не писать во временный файл --- я хочу делать все в памяти.

(Обратите внимание, что да, значение, которое я пытаюсь запросить из базы данных и сохранить в FOO, будет либо 0, либо 1; не то, чтобы это имело значение, но это делает вещи более запутанными.)

Я попытался протестировать предложенное Аачини решение, просто посмотрев, что будет возвращено в %%G:

FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "%PSQL_EXE%" -U user -d MyDB -p %PG_PORT% -c "select string_value from mydb.uri as uri, mydb.database_property as prop where uri.id = prop.property_uriid and uri.namespace_uri='http://example.com/foo/bar/' and uri.simplename = 'fooBar'" -q -t -w ^& ECHO !ERRORLEVEL!"') DO (
  ECHO %%G
)

Поскольку psql не смог найти файл паролей, он выводит ошибку в stderr (как и ожидалось), а ECHO выводит 0 --- поэтому ERRORLEVEL не отправляется обратно в предложение DO. Но если я выделю команду из цикла FOR и просто запущу ее напрямую, она отлично покажет ERRORLEVEL (2)!

"%PSQL_EXE%" -U user -d MyDB -p %PG_PORT% -c "select string_value from mydb.uri as uri, mydb.database_property as prop where uri.id = prop.property_uriid and uri.namespace_uri='http://example.com/foo/bar/' and uri.simplename = 'fooBar'" -q -t -w
ECHO !ERRORLEVEL!

Обновление: после того, как мне сообщили, что мне нужно избегать побегов, если у меня уже включено отложенное расширение, я взял данный ответ и обновил его, чтобы учесть, что программа, которую я вызываю, не будет возвращать значение, если она генерирует ошибка. Итак, вот что я делаю; сначала я убеждаюсь, что две переменные не определены:

SET "var1="
SET "var2="

Затем я использую цикл, который дали @Aacini и @jeb, но внутри цикла я делаю следующее:

if NOT DEFINED var1 (
  SET "var1=%%G"
) else (
  SET "var2=%%G"
)

Затем вне цикла после его завершения:

if DEFINED var2 (
  ECHO received value: !var1!
  ECHO ERRORLEVEL: !var2!
) ELSE (
  ECHO ERRORLEVEL: !var1!
)

Кажется, это работает. (Невероятно --- какая гимнастика!)


person Garret Wilson    schedule 14.12.2015    source источник
comment
В вашем исходном вопросе я опубликовал ответ на использование короткого пути. Вы пробовали это вместо команды вызова? Кроме того, вы можете увидеть этот вопрос: stackoverflow.com/questions/34226971/   -  person Jason Faulkner    schedule 14.12.2015
comment
Пожалуйста, опубликуйте также код foo.bat.   -  person Squashman    schedule 14.12.2015
comment
@Squashman, я улучшил пример, чтобы использовать фактический исполняемый файл, который я использую, и почти идентичные аргументы. Вероятно, вы могли бы воспроизвести это, изменив %PSQL_EXE% так, чтобы он указывал на какой-либо пакетный файл по вашему выбору, хотя обратите внимание, что путь должен содержать пробелы, чтобы точно отразить проблему.   -  person Garret Wilson    schedule 15.12.2015
comment
Я предполагаю, что вы включили отложенное расширение ДО вашего FOR/F, тогда вам нужно изменить строку на FOR /F ... ('"cmd /V:on .... ^^^& echo ^!ERRORLEVEL^!"') DO (   -  person jeb    schedule 17.12.2015
comment
Да, отложенное расширение уже было включено. Я буду экспериментировать с дополнительной информацией @jeb.   -  person Garret Wilson    schedule 17.12.2015
comment
Кажется, что мои запятые удаляются из строк в кавычках --- их тоже нужно экранировать?   -  person Garret Wilson    schedule 17.12.2015
comment
do those need to be escaped, too? да.   -  person Stephan    schedule 17.12.2015
comment
Кажется, ему нужно = сбежать, а также ...   -  person Garret Wilson    schedule 18.12.2015


Ответы (1)


Для этого нужно совместить несколько трюков. Пакетный файл, помещенный в набор for /F, выполняется в новом контексте cmd.exe, поэтому нам нужен метод, сообщающий об уровне ошибок, когда такая команда завершается. Единственный способ сделать это — сначала выполнить нужный пакетный файл с помощью явно размещенного cmd.exe /C, а после него вставить команду, сообщающую об уровне ошибки. Однако команды, помещенные в набор for /F, выполняются в контексте командной строки, а не в контексте пакетного файла, поэтому необходимо включить отложенное развертывание (переключатель /V:ON) в явном cmd.exe. . После этого простой echo !errorlevel! показывает уровень ошибок, возвращаемый пакетным файлом.

@echo off
setlocal

set "foo="
FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "path with spaces\foo.bat" "blah blah='foobar' blah" ^& echo !errorlevel!"') do (
   if not defined foo (
      set "foo=%%G"
   ) else (
      set "errlevel=%%G"
   )
)

echo String output from foo.bat: "%foo%"
echo Errorlevel returned by foo.bat: %errlevel%

Это "путь с пробелами\foo.bat":

@echo off
echo String from foo.bat
exit /B 12345

И это вывод предыдущего кода:

String output from foo.bat: "String from foo.bat"
Errorlevel returned by foo.bat: 12345

Если отложенное развертывание уже включено ранее в основном пакетном файле, необходимо использовать совершенно другой синтаксис.
^!ERRORLEVEL^! Это необходимо для того, чтобы избежать ошибки !ERRORLEVEL! оценивается в контексте пакетного файла до того, как будет выполнена строка FOR.
^^^& необходимо, чтобы привести один ^& к внутреннему выражению cmd /V:ON

@echo off
setlocal EnableDelayedExpansion

set "foo="
REM *** More carets must be used in the next line ****
FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "path with spaces\foo.bat" "blah blah='foobar' blah" ^^^& echo ^!errorlevel^!"') do (
   if not defined foo (
      set "foo=%%G"
   ) else (
      set "errlevel=%%G"
   )
)

EDIT: Ответить на комментарии OP

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

@echo off
setlocal EnableDelayedExpansion

set "foo="
FOR /F "delims=" %%G IN ('"CMD /V:ON /C CALL "path with spaces\foo.bat" "blah blah='foobar' blah" ^^^& echo errlevel=^!errorlevel^!"') do (
   set "str=%%G"
   if "!str:~0,8!" equ "errlevel" (
      set "errlevel=!str:~9!"
   ) else (
      set "foo=%%G"
   )
)

echo String output from foo.bat: "%foo%"
echo Errorlevel returned by foo.bat: %errlevel%

Однако исходная спецификация указывает, что исполняемая программа представляет собой BATCH-файл с именем "C:\path with spaces\foo.bat". Этот код правильно работает, когда исполняемая программа представляет собой пакетный файл .BAT, как запрошено, но не работает, если исполняемая программа представляет собой файл .exe, как я ясно указал в своем первом комментарии: «Обратите внимание, что C:\path с пробелами \foo.bat должен быть файлом .bat! Этот метод не работает, если такой файл является .exe". Таким образом, вам просто нужно создать файл «foo.bat» с выполнением .exe и команды exit /B %errorlevel% в следующей строке. Конечно, если вы напрямую протестируете этот метод, напрямую запустив программу .exe, он никогда не будет работать...

ВТОРОЕ ИЗМЕНЕНИЕ: добавлены некоторые пояснения

Цель этого решения состоит в том, чтобы предоставить пакетный файл "C:\path with spaces\foo.bat", который показывает некоторые выходные данные и возвращает значение уровня ошибки, например:

@echo off
echo String from foo.bat
exit /B 12345

... он может быть вызван другим пакетным файлом, который получает как вывод, так и уровень ошибки. Это можно сделать очень просто с помощью вспомогательного файла:

@echo off

call "C:\path with spaces\foo.bat" > foo.txt
set errlevel=%errorlevel%
set /P "foo=" < foo.txt

echo String output from foo.bat: "%foo%"
echo Errorlevel returned by foo.bat: %errlevel%

Однако в запросе указано: "не записывать в какой-то временный файл --- я хочу все делать в памяти".

Получить вывод другой команды можно через FOR /F, например:

FOR /F "delims=" %%G in ('CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah"') do set foo=%%G

Однако пакетный файл, помещенный в набор FOR/F, выполняется в новом контексте cmd.exe. Точнее, выполнение foo.bat в предыдущей команде FOR полностью эквивалентно следующему коду:

CMD /C CALL "C:\path with spaces\foo.bat" "blah blah='foobar' blah" > tempFile & TYPE tempFile & DEL tempFile

Каждой строке в tempFile назначается заменяемый параметр %%G и выполняется команда set foo=%%G. Обратите внимание, что предыдущая строка выполняется так, как если бы она была введена из командной строки (контекст командной строки), поэтому некоторые команды не работают в этом контексте (например, goto/call to a label, setlocal/ endlocal и т. д.), а для отложенного расширения установлено начальное значение cmd.exe, обычно отключенное.

Чтобы упростить следующее описание, мы используем более короткую, но похожую команду FOR /F, которая показана с эквивалентным внутренним кодом выполнения foo.bat под ней:

FOR /F %%G in ('CALL "foo.bat"') do set foo=%%G
-> CMD /C CALL "foo.bat"                <- we also omit the "tempFile" parts

Таким образом, нам нужен метод, чтобы сообщить об уровне ошибки foo.bat, когда такая команда завершается. Единственный способ сделать это — сначала выполнить нужный пакетный файл с помощью явно размещенного cmd.exe /C, а после него вставить команду, сообщающую об уровне ошибки. То есть:

FOR /F %%G IN ('"CMD /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
-> CMD /C "CMD /C CALL foo.bat ^& echo !errorlevel!"

Обратите внимание, что знак амперсанда необходимо экранировать следующим образом: ^&; в противном случае он будет неправильно разделять команды, помещенные в набор команды FOR /F. В предыдущей строке обе команды foo.bat и echo !errorlevel! выполняются через вложенный CMD /C.

Однако команды, помещенные в набор FOR /F, выполняются в контексте командной строки, как объяснялось ранее, поэтому необходимо включить отложенное развертывание (переключатель /V:ON) в явном cmd.exe; иначе echo !errorlevel! просто не работает:

FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
-> CMD /C "CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"

В порядке. Обратите внимание, что в предыдущем описании предполагалось, что отложенное расширение было ОТКЛЮЧЕНО при выполнении команды FOR /F. Какие изменения, если он был включен? 1. !errorlevel! заменяется текущим значением уровня ошибки, и 2. удаляется любой знак вставки, используемый для экранирования символа. Эти изменения вносятся при разборе строки, перед выполнением команды FOR /F. Давайте рассмотрим этот момент подробнее:

SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
-> After parsed:
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat & echo 0"') do set foo=%%G

Предыдущая строка выдает ошибку из-за неэкранированного &, как обычно. Нам нужно сохранить как восклицательные знаки, так и знак вставки амперсанда. Сохранить восклицательные знаки несложно: ^!errorlevel^!, но как сохранить каретку в ^&? Если мы используем ^^&, первая каретка сохранит вторую, но следующим символом для разбора будет только &, и появится обычная ошибка! Итак, правильный путь: ^^^&; первая каретка сохраняет вторую и дает ^, а ^& сохраняет амперсанд:

SETLOCAL ENABLEDELAYEDEXPANSION
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^^^& echo ^!errorlevel^!"') do set foo=%%G
-> After parsed:
FOR /F %%G IN ('"CMD /V:ON /C CALL foo.bat ^& echo !errorlevel!"') do set foo=%%G
person Aacini    schedule 14.12.2015
comment
Аачини, боюсь, я не смогу заставить это работать. Все тело FOR пропускалось. Я обновил пример в вопросе, чтобы лучше отразить то, что происходит в реальной жизни. Вероятно, вы могли бы изменить %PSQL_EXE%, чтобы указать на какой-либо пакетный файл по вашему выбору, хотя обратите внимание, что путь должен содержать пробелы, чтобы точно отразить проблему. - person Garret Wilson; 15.12.2015
comment
Я предлагаю вам скопировать мой точно такой же код и запустить его, чтобы проверить, работает ли он. После этого вставляйте модификации одну за другой, пока не определите шаг, вызывающий проблему. Обратите внимание, что C:\path with spaces\foo.bat должен быть файлом .bat! Этот метод не работает, если такой файл является .exe - person Aacini; 15.12.2015
comment
Почему !errorlevel! расширяется внутри экземпляра cmd, созданного for /F, хотя ! не экранированы? - person aschipfl; 16.12.2015
comment
@aschipfl: обратите внимание, что в пакетном файле не включено отложенное расширение, поэтому ! просто передаются в виде текста до тех пор, пока внутренний CMD /V:ON /C ... не выполнит их. - person Aacini; 16.12.2015
comment
Ах да, конечно, извините; Я просто заметил setlocal в начале и сразу же прокомментировал - очевидно, слишком поспешно... - person aschipfl; 17.12.2015
comment
@Aacini, мне трудно понять, что должен делать твой код. Я думаю, что при нормальных обстоятельствах вы ожидаете, что будут сгенерированы две строки: String from foo.bat из foo.bat и 12345 из оператора ECHO. Но вы предполагаете, что foo.bat всегда выдает строку. На самом деле в моем случае foo.bat не выдает значения, если завершается с кодом ошибки. (Код ошибки указывает на то, что это не удалось). В этом случае, как я узнаю, содержит ли foo вывод или код ошибки, поскольку это будет первая строка, возвращаемая в любом случае? - person Garret Wilson; 17.12.2015
comment
@Aacini, ваше решение не работает, потому что ERRORLEVEL не возвращается в %%G. Пожалуйста, смотрите обновленный вопрос. - person Garret Wilson; 17.12.2015
comment
С дополнительными побегами я теперь получаю ERRORLEVEL обратно, но мне нужно изменить вашу логику в цикле, потому что foo.bat не будет выдавать значение, если обнаружит ошибку. - person Garret Wilson; 17.12.2015
comment
Хорошо, я пришел к решению; см. мой обновленный вопрос. Кто-нибудь обновите ответ, чтобы отразить это, и я назначу награду, как только смогу. - person Garret Wilson; 17.12.2015
comment
Гаррет: Смотрите правку в моем коде. @jeb: Большое спасибо! :) - person Aacini; 17.12.2015
comment
Не могли бы вы объяснить, что делает это новое решение? Это очень загадочно. Даже первый пришлось разобрать. - person Garret Wilson; 18.12.2015
comment
Правильно, @Aacini ... но технические детали, которые вы объяснили, не самые загадочные (хотя они, конечно, не очевидны). Особенно сложной является ваша техника. Разработчик должен понимать, что выполняемые команды выдают несколько строк (разных в зависимости от условий ошибки или нет и т.д.). Ваше последнее решение затем добавляет специальные строки, которые должны быть проанализированы (например, str:~0,8), предполагая, что foo.bat не выдает те же строки. В любом случае, я заставил его работать, но я выбрал более длинный, но более понятный подход, который я упомянул в обновленном вопросе. Благодарю вас! - person Garret Wilson; 18.12.2015