Почему ErrorLevel устанавливается только после || оператора при неудачном перенаправлении?

После неудачного перенаправления (из-за несуществующего файла или недостаточного доступа к файлу) значение ErrorLevel, похоже, не установлено (в следующих примерах файл test.tmp защищен от записи и файл test.nil не существует):

>>> (call ) & rem // (reset `ErrorLevel`)

>>> > "test.tmp" echo Text
Access is denied.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

>>> (call ) & rem // (reset `ErrorLevel`)

>>> < "test.nil" set /P DUMMY=""
The system cannot find the file specified.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

Однако, как только неудачное перенаправление сопровождается оператором условной конкатенации ||, который запрашивает код выхода, ErrorLevel устанавливается на 1, неожиданно:

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) || echo Fail
Access is denied.
Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") || echo Fail
The system cannot find the file specified.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

Интересно, что ErrorLevel остается 0, когда используется оператор &&:

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) && echo Pass
Access is denied.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") && echo Pass
The system cannot find the file specified.

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

ErrorLevel остается также 0 с помощью оператора &:

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) & echo Pass or Fail
Access is denied.
Pass or Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") & echo Pass or Fail
The system cannot find the file specified.
Pass or Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=0

В случае появления операторов условной конкатенации && и ||, ErrorLevel также устанавливается на 1 (если || встречается до &&, обе ветки выполняются как в последнем примере, но Я думаю, это просто потому, что && оценивает код выхода предыдущей команды echo):

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (> "test.tmp" echo Text) && echo Pass || echo Fail
Access is denied.
Fail

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

>>> (call ) & rem // (reset `ErrorLevel`)

>>> (< "test.nil" set /P DUMMY="") || echo Fail && echo Pass
The system cannot find the file specified.
Fail
Pass

>>> echo ErrorLevel=%ErrorLevel%
ErrorLevel=1

Итак, какова связь между значением ErrorLevel и оператором ||, почему ErrorLevel влияет на ||? Является ли || копирование кода выхода на ErrorLevel? Возможно ли это только при перенаправлении (неудачных), поскольку они обрабатываются до того, как будут выполнены какие-либо команды?

Еще более странно, я не мог наблюдать обратное поведение - ErrorLevel 0 && - при правильном возврате тестовой настройки (то есть замене (call ) на (call) (сначала установить ErrorLevel в 1), очищая атрибут только для чтения файла test.tmp, создавая файл test.nil (первая строка не пуста, чтобы избежать set /P, чтобы установить ErrorLevel в 1) и используя расширение файла .bat, а не .cmd для тестирования (чтобы избежать set /P до reset ErrorLevel до 0)).

Я наблюдал описанное поведение в Windows 7 и Windows 10.

Ответ 1

Я впервые обнаружил это нелогичное поведение почти 5 лет назад в Перенаправление файлов в Windows и% errorlevel%. Через два месяца я обнаружил ту же проблему с командой RD (RMDIR) в batch: Exit code for "rd" равно 0 при ошибке. Заголовок этого последнего вопроса фактически вводит в заблуждение, потому что код возврата неудавшегося RD не равен нулю, но ERRORLEVEL не изменяется от любого значения, существовавшего до выполнения команды. Если код возврата был действительно 0, тогда оператор || не срабатывал.

Все это возможно только при перенаправлении (неудачных), поскольку такие обрабатывается до выполнения каких-либо команд?

Вы правы, что перенаправление не выполняется до выполнения команды. И || отвечает на ненулевой код возврата операции перенаправления. Команда (ECHO в вашем случае) никогда не выполняется, если перенаправление не работает.

Итак, какова связь между значением ErrorLevel и || оператор, почему ErrorLevel влияет на ||? Является ли || копирование выхода код в ErrorLevel?

Есть два разных значения, связанные с ошибкой, которые необходимо отслеживать - 1) любой код команды (или операции) возврата (код выхода) и 2) ERRORLEVEL. Коды возврата являются переходными - их необходимо проверять после каждой операции. ERRORLEVEL - это способ cmd.exe для сохранения "важных" состояний ошибки с течением времени. Цель состоит в том, чтобы все ошибки были обнаружены, и ERRORLEVEL должен быть установлен соответствующим образом. Но ERRORLEVEL был бы бесполезным для пакетных разработчиков, если бы он всегда очищался до 0 после каждой успешной операции. Поэтому разработчики cmd.exe пытались сделать логический выбор, когда успешная команда очищает ERRORLEVEL и сохраняет прежнее значение. Я не уверен, насколько мудрым они были в их выборе, но я попытался документировать правила в Какие внутренние команды cmd.exe очищают ERRORLEVEL до 0 при успешном завершении?.

В остальной части этого раздела есть обоснованная гипотеза. Я не думаю, что окончательный ответ возможен без общения с оригинальными разработчиками cmd.exe. Но это то, что дает мне ментальные рамки для успешного преодоления болота поведения ошибки cmd.exe.

Я считаю, что везде, где может произойти ошибка в cmd.exe, разработчики должны были обнаружить код возврата, установите ERRORLEVEL в ненулевое значение при ошибке, а затем запустите любой код ||, если он находится в игре. Но в некоторых случаях разработчик ввел ошибку, не играя по правилам. После неудачного перенаправления или неудачного RD разработчик успешно вызвал код ||, но не смог правильно установить ERRORLEVEL.

Я также считаю, что разработчик (s) || сделал некоторое защитное программирование. ERRORLEVEL уже должен быть установлен на ненулевое значение до того, как будет выполнен код ||. Но я думаю, что разработчик || мудро не доверял своим сверстникам и решил установить ERRORLEVEL в обработчике ||.

Что касается того, что используется ненулевое значение, представляется логичным, что || пересылает исходное значение кода возврата в ERRORLEVEL. Это означало бы, что исходный код возврата должен храниться в некоторой временной области хранения, отличной от ERRORLEVEL. У меня есть два доказательства, подтверждающих эту теорию:

1) Оператор || устанавливает не менее 4 различных значений ERRORLEVEL при сбое RD в зависимости от типа ошибки.

2) ERRORLEVEL, установленный ||, является тем же значением, что и CMD/C, и CMD/C просто пересылает код возврата последней команды/операции.

C:\test>(call )&rd .
The process cannot access the file because it is being used by another process.

C:\test>echo %errorlevel%
0

C:\test>(call )&rd . || rem
The process cannot access the file because it is being used by another process.

C:\test>echo %errorlevel%
32

C:\test>(call )&cmd /c rd .
The process cannot access the file because it is being used by another process.

C:\test>echo %errorlevel%
32

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

C:\test>invalidCommand
'invalidCommand' is not recognized as an internal or external command,
operable program or batch file.

C:\test>echo %errorlevel%
9009

Но если вы используете оператор || или CMD/C, то ERRORLEVEL равен 1: -/

C:\test>invalidCommand || rem
'invalidCommand' is not recognized as an internal or external command,
operable program or batch file.

C:\test>echo %errorlevel%
1

C:\test>(call )

C:\test>cmd /c invalidCommand
'invalidCommand' is not recognized as an internal or external command,
operable program or batch file.

C:\test>echo %errorlevel%
1

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

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

Еще более странно, я не мог наблюдать противоположное поведение - ErrorLevel reset to 0 by && & - при правильном возврате (т.е. замена (вызов) на (вызов) (для установки параметра ErrorLevel на 1 изначально), очищая атрибут только для чтения файла test.tmp, создание файла test.nil(первая строка не пуста, чтобы избежать установки /P для установки ErrorLevel до 1) и использование расширения файла .bat, а не .cmd для тестирование (чтобы избежать установки /P до reset ErrorLevel до 0)).

Как только вы признаете, что не все команды очищают ERRORLEVEL после успеха, это поведение имеет смысл. Было бы неплохо сохранить предыдущие ошибки, если && должен был стереть сохраненную ошибку.

Ответ 2

note. Все содержание в этом ответе - это просто личная интерпретация символов кода ассемблера/отладки cmd.exe, вывод исходного кода, который генерирует выход ассемблера. Весь код в этом ответе - это всего лишь псевдокод, который примерно отражает то, что происходит внутри cmd.exe, показывая только те части, которые относятся к вопросу.

Первое, что нам нужно знать, это то, что значение errorlevel извлекается из внутренней переменной _LastRetCode (по крайней мере, это имя в информационных символах отладки) из функции GetEnvVar.

Второе, что нужно знать, состоит в том, что большинство команд cmd связаны с набором функций. Эти функции изменяют (или не) значение в _LastRetCode, но также возвращают код sucess/failure (a 0/1), который используется внутри, чтобы определить, есть ли ошибка.

В случае команды echo функция eEcho обрабатывает выходные функции, закодированные как-то вроде

eEcho( x ){
    ....
    // Code to echo the required value
    ....
    return 0
}

То есть команда echo имеет только одну точку выхода и не устанавливает/не очищает переменную _LastRetCode (она не изменит значение errorlevel) и всегда будет возвращать код sucess. echo не сбой (с точки зрения партии, он может выйти из строя и записать на stderr, но он всегда будет возвращать 0 и никогда не изменится _LastRetCode).

Но как называется эта функция?, как создается перенаправление?

Существует функция Dispatch, которая определяет команду/функцию для вызова и которая ранее вызывает функцию SetDir (если необходимо) для создания необходимых перенаправлений. Как только перенаправление установлено, функция GetFuncPtr получает адрес исполняемой функции (функция, связанная с командой cmd) вызывает ее и возвращает ее результат вызывающему.

Dispatch( x, x ){
    ....

    if (redirectionNeeded){
        ret = SetRedir(...)
        if (ret != 0) return 1
    }

    ....

    func = GetFuncPtr(...)
    ret = func(...)
    return ret
}

В то время как возвращаемые значения этих функций отражают наличие ошибки (они возвращают 1 при сбое, 0 при успешном завершении), а Dispatch а не SetDir изменяют переменную _LastRetCode (while не показаны здесь, ни одна из них не ссылается на переменную), поэтому при переходе переназначения нет errorlevel.

Что меняется при использовании оператора ||?

Оператор || обрабатывается внутри функции eOr, которая закодирована, да, снова более или менее

eOr( x ){
    ret = Dispatch( leftCommand )
    if (ret == 0) return 0

    _LastRetCode = ret

    ret = Dispatch( rightCommand )
    return ret 
}

Сначала выполняется команда слева. Если он не возвращает ошибку, ничего не остается делать и выходит из значения sucess. Но если левая команда не работает, она сохраняет возвращаемое значение внутри переменной _LastRetCode перед вызовом команды с правой стороны. В случае в вопросе

(> "test.tmp" echo Text) || echo Fail

Выполнение

  • Dispatch (который вызывается из функций, обрабатывающих пакетное выполнение и принимающих в качестве параметра выполнение) вызывает eOr
  • eOr вызывает Dispatch для выполнения eEcho (левая часть оператора)
  • Dispatch вызывает SetDir, чтобы создать перенаправление
  • SetDir сбой и возвращает 1 (никаких изменений в _LastRetCode)
  • Dispatch возвращает 1, поскольку SetDir потерпел неудачу
  • eOr проверяет возвращаемое значение и, поскольку оно не 0, возвращаемое значение сохраняется в _LastRetCode. Теперь _LastRetCode есть 1
  • eOr вызывает Dispatch для выполнения eEcho (справа от оператора)
  • Dispatch вызывает GetFuncPtr для получения адреса функции eEcho.
  • Dispatch вызывает возвращаемый указатель функции и возвращает возвращаемое значение (eEcho всегда возвращает 0)
  • eOr возвращает возвращаемое значение из Dispatch (0)

Таким образом, возвращаемое значение ошибки (return 1) при неудачной операции перенаправления сохраняется в _LastRetCode при использовании оператора ||.

Что происходит с функцией eAnd, то есть с оператором &&?

eAnd( x ){
    ret = Dispatch( leftCommand )
    if (ret != 0) return ret 

    ret = Dispatch( rightCommand )
    return ret
}

Никаких изменений в _LastRetCode нет, поэтому, если вызываемые команды не меняют эту переменную, никаких изменений в errorlevel нет.

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