Пакетный переход теряет уровень ошибки

Рассмотрим следующую bat, test.bat (PC01 выключен):

mkdir \\PC01\\c$\Test || goto :eof

Если я запускаю эту летучую мышь из командной оболочки:

> test.bat || echo 99
> if ERRORLEVEL 1 echo 55

Выход только 55. Нет 99. Есть уровень ошибки, но || Оператор этого не видел.

Если я запускаю эту летучую мышь, используя cmd /c -

> cmd /c test.bat || echo 99
> if ERRORLEVEL 1 echo 55

Выход пуст. Уровень ошибки равен 0.

Если я удалю || goto :eofвсе работает так, как можно было бы предсказать - т.е.

99 55

Кто-нибудь знает, почему происходит это полусухое полусуществующее поведение ERRORLEVEL?

3 ответа

Решение

В большинстве случаев || это самый надежный способ обнаружения ошибки. Но вы наткнулись на один из редких случаев, когда ERRORLEVEL работает, но || не.

Проблема связана с тем, что ваша ошибка возникает в пакетном скрипте, и || отвечает на код возврата последней выполненной команды. Вы думаете о test.bat как о единой "команде", но на самом деле это последовательность команд. Последняя команда, выполненная в скрипте GOTO :EOF и это успешно выполнено. Так что ваши test.bat||echo 99 отвечает на успех GOTO :EOF,

Когда вы удаляете ||GOTO :EOF из сценария, то ваш test.bat||echo99 видит результат неудачного mkdir, Но если бы вы должны были добавить REM команда до конца test.bat, затем test.bat||echo 99 будет отвечать на успех REM и ошибка будет замаскирована снова.

ERRORLEVEL по-прежнему ненулевой после test.bat||echo 99 потому что команды как GOTO а также REM не удаляйте любые предыдущие ненулевые ОШИБКИ при успехе. Это одно из многих доказательств того, что ERRORLEVEL и код возврата не совсем одно и то же. Это определенно сбивает с толку.

Вы можете рассматривать test.bat как команду модуля и получить желаемое поведение с помощью CALL.

C:\test>call test.bat && echo OK || echo FAIL
FAIL

C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
FAIL2

Это работает, потому что CALL Команда временно передает управление вызываемому скрипту. Когда сценарий завершается, управление возвращается CALL команда, и он возвращает текущий ERRORLEVEL. Так ||echo 99 отвечает на ошибку, возвращаемую самой командой CALL, а не последней командой в скрипте.

Теперь для CMD /C вопрос.

Код возврата, возвращаемый CMD /C код возврата последней выполненной команды.

Это работает:

C:\test>cmd /c call test.bat && echo OK || echo FAIL
FAIL

C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
FAIL2

так как CMD /C возвращает ОШИБКУ, возвращенную CALL заявление

Но это не удается полностью:

C:\test>cmd /c test.bat && echo OK || echo FAIL
OK

C:\test>if ERRORLEVEL 1 (echo FAIL2) else echo OK2
OK2

Без CALL, CMD /C возвращает код возврата последней выполненной команды, которая является GOTO :EOF, CMD /C также устанавливает ERRORLEVEL в тот же код возврата, так что теперь нет никаких свидетельств того, что когда-либо была ошибка в скрипте.

И по кроличьей норе мы идем

RLH в своем ответе и в своих комментариях к моему ответу обеспокоен тем, что || иногда очищает ОШИБКУ. Он предоставляет доказательства, которые, кажется, подтверждают его заключение. Но ситуация не так проста, и оказывается, что || это самый надежный (но все еще не идеальный) способ обнаружения ошибок.

Как я уже говорил ранее, код возврата, который все внешние команды возвращают при выходе, - это не то же самое, что cmd.exe ERRORLEVEL.

ERRORLEVEL - это состояние, поддерживаемое в самом сеансе cmd.exe, полностью отличное от кодов возврата.

Это даже задокументировано в определении exitCode в справке EXIT.
(help exit или же exit /?)

EXIT [/B] [exitCode]

  /B          specifies to exit the current batch script instead of
              CMD.EXE.  If executed from outside a batch script, it
              will quit CMD.EXE

  exitCode    specifies a numeric number.  if /B is specified, sets
              ERRORLEVEL that number.  If quitting CMD.EXE, sets the process
              exit code with that number.

Когда CMD.EXE выполняет внешнюю команду, она обнаруживает код возврата исполняемого файла и устанавливает соответствие ERRORLEVEL. Обратите внимание, что это всего лишь соглашение, что 0 означает успех, а ненулевое значение означает ошибку. Некоторые внешние команды могут не следовать этому соглашению. Например, команда HELP (help.exe) не соответствует соглашению - она ​​возвращает 0, если указана неверная команда, как в help bogus, но возвращает 1, если вы запрашиваете помощь в действительной команде, как в help rem,

|| оператор никогда не очищает ERRORLEVEL при выполнении внешней команды. Код завершения процесса обнаружен и срабатывает || если он не равен нулю, и ERRORLEVEL все равно будет соответствовать коду выхода. При этом команды, которые появляются после && и / или || может изменить ERRORLEVEL, поэтому нужно быть осторожным.

Но есть много других ситуаций, кроме внешних команд, где мы, разработчики, заботимся об успехах / неудачах и кодах возврата / ОШИБКАХ.

  • выполнение внутренних команд
  • операторы перенаправления <, >, а также >>
  • выполнение пакетных скриптов
  • не удалось выполнить недопустимые команды

К сожалению, CMD.EXE совсем не соответствует тому, как он обрабатывает ошибки в этих ситуациях. CMD.EXE имеет несколько внутренних точек, где он должен обнаруживать ошибки, предположительно с помощью некоторой формы внутреннего кода возврата, который не обязательно является ERRORLEVEL, и в каждой из этих точек CMD.EXE может устанавливать ERRORLEVEL в зависимости от того, что он находит,

Для моих тестовых случаев ниже, обратите внимание, что (call ) , с пробелом, является тайным синтаксисом, который очищает ERRORLEVEL до 0 перед каждым тестом. Позже я также буду использовать (call) без пробела, чтобы установить ERRORLEVEL в 1

Также обратите внимание, что отложенное расширение было включено в моей командной сессии с помощью
cmd /v: on до запуска моих тестов

Подавляющее большинство внутренних команд устанавливает ERRORLEVEL в ненулевое значение при сбое, и условие ошибки также срабатывает ||, || никогда не очищает и не изменяет ОШИБКУ в этих случаях. Вот пара примеров:

C:\test>(call ) & set /a 1/0
Divide by zero error.

C:\test>echo !errorlevel!
1073750993

C:\test>(call ) & type notExists
The system cannot find the file specified.

C:\test>echo !errorlevel!
1

C:\test>(call ) & set /a 1/0 && echo OK || echo ERROR !errorlevel!
Divide by zero error.
ERROR 1073750993

C:\test>(call ) & type notExists.txt && echo OK || echo ERROR !errorlevel!
The system cannot find the file specified.
ERROR 1

Тогда есть по крайней мере одна команда, RD, (возможно, больше), а также операторы перенаправления, которые запускают || при ошибке, но не устанавливайте ERRORLEVEL, если только || используется.

C:\test>(call ) & rd notExists
The system cannot find the file specified.

C:\test>echo !errorlevel!
0

C:\test>(call ) & echo x >\badPath\out.txt
The system cannot find the path specified.

C:\test>echo !errorlevel!
0

C:\test>(call ) & rd notExists && echo OK || echo ERROR !errorlevel!
The system cannot find the file specified.
ERROR 2

C:\test>(call ) & echo x >\badPath\out.txt && echo OK || echo ERROR !errorlevel!
The system cannot find the path specified.
ERROR 1

См. "Rd" для выходов с уровнем ошибки, установленным на 0, при ошибке при удалении, и т. Д., И Перенаправление файлов в Windows и% errorlevel% для получения дополнительной информации.

Мне известна одна внутренняя команда (может быть и другая) плюс базовые неудачные операции ввода-вывода, которые могут выдавать сообщения об ошибках в stderr, но они не запускаются || и при этом они не устанавливают ненулевой ОШИБКА.

Команда DEL может вывести ошибку, если файл доступен только для чтения или не существует, но не запускается || или установите ERRORLEVEL в ненулевое значение

C:\test>(call ) & del readOnlyFile
C:\test\readOnlyFile
Access is denied.

C:\test>echo !errorlevel!
0

C:\test>(call ) & del readOnlyFile & echo OK || echo ERROR !errorlevel!
C:\test\readOnlyFile
Access is denied.
OK

См. /questions/40487918/ne-udalos-udalit-otchet-v-fajl-v-paketnyih-fajlah-del/40487930#40487930 для получения дополнительной информации, связанной с ошибками DEL.

Во многом таким же образом, когда стандартный вывод был успешно перенаправлен в файл на устройстве USB, но затем устройство удаляется до того, как команда, такая как ECHO, пытается выполнить запись на устройство, тогда ECHO завершится ошибкой с сообщением об ошибке stderr., еще || не срабатывает, и ERRORLEVEL не установлен в ненулевое значение. См. http://www.dostips.com/forum/viewtopic.php?f=3&t=6881 для получения дополнительной информации.

Тогда у нас есть случай, когда выполняется пакетный скрипт - фактическая тема вопроса ОП. Без CALL, || Оператор отвечает на последнюю команду, выполненную в скрипте. С CALL, || Оператор отвечает на значение, возвращаемое CALL команда, которая является окончательной ОШИБКОЙ, существующей после завершения пакета.

Наконец, у нас есть случай, когда RLH сообщает, что о неверной команде обычно сообщают как ERRORLEVEL 9009, но как ERRORLEVEL 1, если || используется.

C:\test>(call ) & InvalidCommand
'InvalidCommand' is not recognized as an internal or external command,
operable program or batch file.

C:\test>echo !errorlevel!
9009

C:\test>(call ) & InvalidCommand && echo OK || echo ERROR !errorlevel!
'InvalidCommand' is not recognized as an internal or external command,
operable program or batch file.
ERROR 1

Я не могу доказать это, но я подозреваю, что обнаружение сбоя команды и установка ERRORLEVEL в 9009 происходит очень поздно в процессе выполнения команды. Я предполагаю, что || перехватывает обнаружение ошибок до того, как будет установлен 9009, и в этот момент вместо него устанавливается 1. Так что я не думаю || очищает ошибку 9009, а скорее это альтернативный путь, по которому ошибка обрабатывается и устанавливается.

Альтернативный механизм для этого поведения состоит в том, что недопустимая команда всегда может установить для ERRORLEVEL значение 9009, но иметь другой код возврата, равный 1. || мог впоследствии обнаружить код возврата 1 и установить соответствие ERRORLEVEL, перезаписав таким образом 9009.

Несмотря на это, я не знаю ни о какой другой ситуации, когда ненулевой результат ERRORLEVEL отличается в зависимости от того, || был использован или нет.

Так что это заботится о том, что происходит, когда команда не выполняется. Но что делать, когда внутренняя команда преуспевает? К сожалению, CMD.EXE еще менее согласован, чем с ошибками. Он зависит от команды и может также зависеть от того, выполняется ли он из командной строки, из пакетного сценария с .bat расширение или из пакетного сценария с .cmd расширение.

Я основываю все обсуждение ниже на поведении Windows 10. Я сомневаюсь, что есть различия с более ранними версиями Windows, которые используют cmd.exe, но это возможно.

Следующие команды всегда сбрасывают ERRORLEVEL в 0 в случае успеха, независимо от контекста:

  • CALL: Сбрасывает ERRORLEVEL, если команда CALLed не устанавливает его иначе.
    Пример: call echo OK
  • CD
  • CHDIR
  • ЦВЕТ
  • COPY
  • ДАТА
  • DEL: всегда очищает ERRORLEVEL, даже если DEL не удается
  • DIR
  • ERASE: всегда очищает ERRORLEVEL, даже если ERASE завершается неудачей
  • Мэриленд
  • MKDIR
  • MKLINK
  • ПЕРЕЕХАТЬ
  • PUSHD
  • REN
  • ПЕРЕИМЕНОВАТЬ
  • SETLOCAL
  • ВРЕМЯ
  • ТИП
  • VER
  • ПРОВЕРКИ
  • VOL

Следующий набор команд никогда не сбрасывает ERRORLEVEL в 0 при успешном завершении, независимо от контекста, но вместо этого сохраняет любое существующее ненулевое значение ERRORLEVEL:

  • ПЕРЕРЫВ
  • ЦБС
  • ECHO
  • ENDLOCAL
  • ВЫХОД: Очевидно EXIT /B 0 очищает ОШИБКУ, но EXIT /B без значения сохраняет прежнюю ОШИБКУ.
  • ЗА
  • ИДТИ К
  • ЕСЛИ
  • КЛЮЧИ
  • ПАУЗА
  • POPD
  • RD
  • REM
  • RMDIR
  • СДВИГ
  • НАЧНИТЕ
  • ЗАГЛАВИЕ

И затем есть эти команды, которые не очищают ERRORLEVEL при успешном завершении, если они выполняются из командной строки или внутри сценария с .bat расширение, но очистите ERRORLEVEL до 0, если он выпущен из сценария с .cmd расширение. См. /questions/37874146/paketnyie-fajlyi-windows-bat-protivcmd/37874154#37874154 и https://groups.google.com/forum/#!msg/microsoft.public.win2000.cmdprompt.admin/XHeUq8oe2wk/LIEViGNmkK0J для получения дополнительной информации.

  • ASSOC
  • DPATH
  • FTYPE
  • ДОРОЖКА
  • НЕЗАМЕДЛИТЕЛЬНЫЙ
  • ЗАДАВАТЬ

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

Вот два примера, которые показывают, что && всегда срабатывает, если предыдущая команда была успешной, даже если значение ERRORLEVEL не равно нулю. Команда CD является примером, где команда очищает любой предыдущий ERRORLEVEL, а команда ECHO является примером, где команда не очищает предыдущий ERRORLEVEL. Обратите внимание, я использую (call) принудительно установить ERRORLEVEL в 1, прежде чем выполнить команду, которая будет выполнена успешно.

C:\TEST>(call)

C:\TEST>echo !errorlevel!
1

C:\test>(call) & cd \test

C:\test>echo !errorlevel!
0

C:\test>(call) & cd \test && echo OK !errorlevel! || echo ERROR !errorlevel!
OK 0

C:\test>(call) & echo Successful command
Successful command

C:\test>echo !errorlevel!
1

C:\test>(call) & echo Successful command && echo OK !errorlevel! || echo ERROR !errorlevel!
Successful command
OK 1

Во всех моих примерах кода для обнаружения ошибок я полагался на тот факт, что ECHO никогда не очищает ранее существующий ненулевой ERRORLEVEL. Но приведенный ниже скрипт является примером того, что может произойти, когда другие команды используются после && или же ||,

@echo off
setlocal enableDelayedExpansion
(call)
echo ERRORLEVEL = !errorlevel!
(call) && echo OK !errorlevel! || echo ERROR !errorlevel!
(call) && (echo OK !errorlevel! & set "err=0") || (echo ERROR !errorlevel! & set "err=1" & echo ERROR !errorlevel!)
echo ERRORLEVEL = !errorlevel!
echo ERR = !ERR!

Вот вывод, когда скрипт имеет .bat расширение:

C:\test>test.bat
ERRORLEVEL = 1
ERROR 1
ERROR 1
ERROR 1
ERRORLEVEL = 1
ERR = 1

И вот вывод, когда скрипт имеет .cmd расширение:

C:\test>test.cmd
ERRORLEVEL = 1
ERROR 1
ERROR 1
ERROR 0
ERRORLEVEL = 0
ERR = 1

Помните, что каждая выполненная команда может изменить ERRORLEVEL. Так что даже если && а также || являются наиболее надежными способами обнаружения успеха или неудачи команд, нужно быть осторожным с тем, какие команды используются после этих операторов, если вам важно значение ERRORLEVEL.

И теперь пришло время вылезти из этой вонючей кроличьей норы и подышать свежим воздухом!

Итак, что мы узнали?

Не существует единого идеального способа определить, была ли какая-либо произвольная команда успешной или неудачной. Тем не мение, && а также || являются наиболее надежными методами выявления успеха и неудачи.

В общем, ни && ни || изменить ERRORLEVEL напрямую. Но есть несколько редких исключений.

  • || правильно устанавливает ERRORLEVEL, который в противном случае был бы пропущен при сбое RD или перенаправления
  • || устанавливает другой ERRORLEVEL при неудачном выполнении недопустимой команды, тогда произойдет, если || не использовались (1 против 9009).

В заключение, || не обнаруживает ненулевой ERRORLEVEL, возвращенный пакетным сценарием, как ошибку, если не была использована команда CALL.

Если вы полагаетесь строго на if errorlevel 1 ... или же if %errorlevel% neq 0 ... чтобы обнаружить ошибки, вы рискуете пропустить ошибки, которые могут вызвать RD и перенаправление (и другие?), и вы также рискуете ошибочно думать, что некоторые внутренние команды потерпели неудачу, хотя в действительности это может быть удержание от предыдущей команды, которая не удалось.

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

mkdir \\\failure || goto :EXIT
echo Only on Success
exit /b

:exit
(call)

Теперь вы можете использовать

test.bat || echo Failed

Единственным недостатком здесь является потеря уровня ошибки.
В этом случае код возврата устанавливается на false но уровень ошибки всегда установлен на 1, недействительным (call)
В настоящее время я не могу найти какой-либо возможный способ установить оба, уровень ошибки и код возврата для пользовательских значений

Истинный уровень ошибок не выдерживает оператор двойной трубы (при ошибке).

Вместо этого используйте логику. Например, ниже приведен один из способов сделать это:

mkdir \\PC01\c$\Test
if "%errorlevel%" == "0" (
echo success
) else (
echo failure
)

редактировать:

Если команда, которая предшествует этому, не является единственной командой, то для отслеживания уровня ошибки необходимо сделать больше. Например, если вы вызываете скрипт, то в этом скрипте вы должны управлять передачей уровня ошибки обратно в этот код.

редактировать 2:

Вот тестовые сценарии, где ERRORLEVEL должен быть '9009' и их вывод.

1) Нет неисправных трубопроводов, используется логика if.

setlocal enableDelayedExpansion
mybad.exe 
if "!errorlevel!" == "0" (
echo success !errorlevel!
) else (
echo Errorlevel is now !errorlevel!
echo Errorlevel is now !errorlevel!
)

"mybad.exe" не распознается как внутренняя или внешняя команда, работающая программа или командный файл.

Уровень ошибок теперь 9009

Уровень ошибок теперь 9009

2) Отказ трубопровода, эхо

setlocal enableDelayedExpansion
mybad.exe || echo Errorlevel is now !errorlevel!
echo Errorlevel is now !errorlevel!

"mybad.exe" не распознается как внутренняя или внешняя команда, работающая программа или командный файл.

Errorlevel теперь 1

Errorlevel теперь 1

3) Отказ трубопроводов, переход

setlocal enableDelayedExpansion
mybad.exe || goto :fail
exit
:fail
echo Errorlevel is now !errorlevel!

"mybad.exe" не распознается как внутренняя или внешняя команда, работающая программа или командный файл.

Errorlevel теперь 1

Так что, если все, что вас волнует, это "Что-то" не удалось, тогда ОК. Код 1 так же хорош, как и все остальное. Но если вам нужно знать "что" не удалось, то вы не можете просто отказать трубе и позволить ей уничтожить фактический результат.

Другие вопросы по тегам