Каждый процесс в операционной системе завершает работу не бесследно, а с небольшим числом в рукаве. Это число называется кодом возврата, и именно оно говорит родительскому процессу, всё ли прошло гладко или что-то пошло не так. Соглашение старо как сама идея пакетной обработки: ноль означает успех, любое ненулевое значение означает ошибку, причём конкретная величина уточняет, какая именно. Тот, кто пишет bat-файлы, запускает установщики из скриптов или настраивает планировщик задач, сталкивается с этим механизмом ежедневно, даже если не подозревает об этом. Статья разбирает, откуда взялось соглашение, как читать %ERRORLEVEL% в cmd и $LASTEXITCODE в PowerShell, какие значения возвращают установщики MSI и robocopy, почему программы иногда умирают с кодом 255 или минус один, и как всё это проверять внутри скриптов, не наступая на грабли отложенного расширения переменных.
Исторические корни соглашения о нуле как признаке успеха
Привычка сообщать результат работы числом уходит корнями в операционные системы семидесятых годов. В CP/M программа при завершении могла вернуть операционной системе код, и уже тогда ноль стал нейтральным, хорошим ответом. Unix закрепил подход окончательно: там функция exit принимает целое число, и ноль по договорённости означает, что программа сделала всё, что от неё требовалось. Ненулевое значение стало способом сказать об ошибке, причём разные номера позволили различать разные неприятности: один для общей неудачи, два для неправильного вызова и так далее.
Смысл такого выбора глубоко практичен. Успех бывает один, а способов потерпеть неудачу много, поэтому логично отдать успеху единственное значение, а всю остальную числовую ось оставить под диагностику. Когда процесс завершается, его код возврата передаётся родителю, то есть тому, кто его породил: командному интерпретатору, планировщику, системе развёртывания. Родитель может прочитать это число и принять решение: продолжать сценарий, повторить попытку, остановить конвейер. В Windows функция ExitProcess делает ровно то же самое, что exit в Unix, а оболочка получает значение через ожидание дескриптора процесса. Так что %ERRORLEVEL% в cmd и код, который видит планировщик задач, это одно и то же число, просто прочитанное разными инструментами.
Как cmd и PowerShell передают и читают код возврата
В командном процессоре cmd самый прямой способ увидеть код возврата это набрать echo %ERRORLEVEL% сразу после интересующей команды. Интерпретатор автоматически обновляет переменную ERRORLEVEL после завершения каждой внешней программы и многих внутренних команд. Рядом работают условные связки, которые давно стали визитной карточкой bat-файлов: запись команда1 && команда2 означает, что вторая команда выполнится только если первая вернула ноль, а конструкция команда1 || обработка запускает обработку именно в случае ненулевого кода. Эти операторы избавляют от необходимости сравнивать числа вручную в простых сценариях и делают скрипт короче и нагляднее.
PowerShell устроен иначе, и это источник постоянной путаницы. Переменная $LASTEXITCODE появляется только тогда, когда был запущен внешний исполняемый файл, то есть настоящий процесс. Исполнение командлетов и функций на неё не влияет вовсе. Ошибка командлета это исключение или запись об ошибке в поток, и проверять её надо через автоматическую переменную $?, блок try catch или параметр ErrorAction. Смешивать два мира в одной голове приходится часто: скрипт может вызвать ping, проверить $LASTEXITCODE, а затем вызвать Get-Item и проверить уже $?. Тот, кто забывает про различие, долго ищет причину, почему условие не срабатывает, хотя файл успешно не найден. Правило простое: внешняя программа отдаёт число, внутренний командлет сообщает об ошибке иначе, и $LASTEXITCODE после него может остаться старым значением от какого-то прежнего запуска.
Проверку в cmd принято писать через специальную форму if errorlevel. Конструкция if errorlevel 1 означает не равенство единице, а больше или равно единице, то есть любая ошибка. Поэтому по-настоящему ветвиться по точным значениям приходится по цепочке: сначала проверяют if errorlevel 2, потом if errorlevel 1, а внутри уже сравнивают %ERRORLEVEL%==2. Эта идиома выглядит неуклюже, но работает на любой версии cmd и остаётся самым переносимым способом реагировать на коды.
Коды возврата установщиков и robocopy как живая практика
На практике больше всего вопросов вызывают коды, которые возвращают установщики. Инфраструктура Windows Installer описала свои значения вполне чётко: ноль это успех и ничего больше не требуется, 3010 означает, что установка прошла, но нужна перезагрузка, чтобы изменения вступили в силу. Новичок легко принимает 3010 за провал, хотя это успех с оговоркой, и система развёртывания, не знающая этого нюанса, начинает ложно рапортовать об ошибках. Код 1603 это печально известная общая фатальная ошибка установки, за которой может скрываться что угодно, от нехватки прав до занятого файла, и разбираться приходится уже по журналу msiexec. Код 1619 сообщает, что пакет не открывается: файл повреждён, недоступен или не является действительным пакетом установщика.
Утилита robocopy демонстрирует другой подход и ломает простую схему ноль хорошо, ненулевое плохо. Она возвращает не ошибку, а битовую маску того, что произошло: ноль это ничего не копировалось и не понадобилось, один это успешное копирование файлов, два это лишние файлы в приёмнике, четыре это несовпадения, и комбинации складываются. Таким образом значения от нуля до семи это всё варианты успеха или хотя бы нестрашных расхождений, а вот восемь и выше, то есть есть невозможность скопировать хотя бы один файл, это уже настоящий провал. Скрипт, который пишет под robocopy условие вида ненулевое значение значит ошибка, будет объявлять аварию при каждом успешном копировании. Правильная проверка звучит так: если code geq 8, тогда бить тревогу. Это яркий пример того, почему документацию к конкретной программе читать обязательно, а общая договорённость служит лишь отправной точкой.
Почему программы умирают с кодом 255 или минус один и что с этим делать
Код 255 встречается в практике настолько часто, что стал почти мемом среди администраторов. Причина троякая. Во-первых, традиционно минус один используется в исходном коде как универсальный признак ошибки, а при приведении к беззнаковому байту минус один превращается ровно в 255. Во-вторых, низкоуровневые оболочки исторически хранили код возврата в одном байте, поэтому всё, что было передано как отрицательное число, обрезалось или пересчитывалось по модулю 256, и та же величина всплывала как 255. В-третьих, некоторые программы сознательно возвращают 255 как максимально громкий код аварийного выхода.
Переполнение отрицательных значений это отдельная канитель. Разработчик может честно написать return с минус двум или минус тремя, ожидая осмысленной градации ошибок, однако на пути к родителю число пройдёт через сужение до беззнакового диапазона, и вызывающий увидит 254 или 253 вместо ожидаемых значений. В Windows код выхода формально хранится как беззнаковое тридцатидвухбитное число, так что минус один туда записывается как 4294967295, а cmd при этом показывает его в %ERRORLEVEL% со знаком, что ещё сильнее сбивает с толку при сравнениях. Практический вывод состоит из трёх пунктов: никогда не возвращать отрицательные числа из собственных программ, при сравнении в скриптах не полагаться на знак, а для диагностики всегда брать абсолютное значение или рассматривать любое ненулевое значение выше порога как ошибку. Тогда 255, минус один и 4294967295 окажутся одним и тем же фактом, записанным в разных нотациях.
Проверка кодов внутри скриптов и ловушка отложенного расширения
Самая коварная история в bat-файлах связана не с самими кодами, а с тем, когда cmd подставляет значения переменных. Расширение %ERRORLEVEL% происходит в момент разбора строки, а блок в скобках, например всё тело if или for, разбирается целиком один раз до исполнения. Поэтому классический пример, где внутри if выполняют команду и тут же проверяют %ERRORLEVEL% в том же блоке, всегда видит старое значение, подставленное заранее. Человеку кажется, что проверка не работает, а на деле работает подстановка, случившаяся раньше времени.
Лечение известно: в начало скрипта добавляют setlocal ENABLEDELAYEDEXPANSION, а внутри блока пишут !ERRORLEVEL! с восклицательными знаками вместо процентов. Восклицательная форма расширяется уже в момент выполнения конкретной строки и потому показывает свежий код. Альтернатива без отложенного расширения звучит так: использовать операторы && и || либо разбить логику так, чтобы проверка оказалась вне скобок. Отдельно стоит помнить про область видимости. Пара setlocal и endlocal ограждает скрипт от протекания переменных наружу, но у неё есть побочный эффект: всё, что установлено внутри, после endlocal пропадает, включая нарочно вычисленные значения. Если нужно передать результат из локальной области, значение закрепляют в той же строке через запись вида endlocal & set RESULT=%RESULT%, которая выглядит странно, но опирается на тот же самый механизм раннего расширения, на этот раз во благо.
Планировщик задач, явный exit /b и документирование собственных кодов
Планировщик задач Windows хранит результат последнего запуска задания именно как код возврата процесса. В колонке последнего результата он отображается в шестнадцатеричном виде: 0x0 значит успех, 0x1 значит, что программа вышла с кодом один, а 0x41301 сообщает, что задание ещё выполняется. Администратору важно понимать, что планировщик видит только то число, которое вернул процесс, и ничего не знает о том, что произошло внутри. Если bat-файл упал с ошибкой, но не вышел явно, код может остаться нулём, и задание будет гордо рапортовать об успехе при полном провале внутри.
Отсюда главный ремесленный совет: каждый bat-файл должен завершаться явным exit /b с конкретным числом. Форма с ключом /b завершает только текущий скрипт, не убивая окно интерпретатора, что важно при вызове одного файла из другого через call. В точках обработки ошибок следует ставить exit /b 1, 2 и далее с осмысленными номерами, а в конце успешного пути exit /b 0. Тогда и планировщик, и цепочка &&, и любая внешняя система получат честный сигнал. Тот, кто пишет собственную утилиту, выигрывает ещё больше, документируя свои коды заранее: отдать нулю успех, единице общую ошибку, двойке неверные аргументы, тройке недоступный ресурс и так далее, а потом опубликовать таблицу рядом со справкой. Потребитель инструмента сможет строить точную реакцию вместо безликого ненулевое значение, а поддержка превратится из гадания в чтение короткой таблицы. Последовательность действий при анализе непонятного кода выглядит так:
- Выполнить echo %ERRORLEVEL% или посмотреть $LASTEXITCODE сразу после запуска.
- Сверить число с документацией конкретной программы, помня про маски robocopy и коды MSI.
- Проверить, не находится ли проверка внутри скобок, и при необходимости включить отложенное расширение.
- Убедиться, что скрипт сам завершается через exit /b с предсказуемым значением.
Код возврата это маленькое число с большой историей, уходящей к CP/M и раннему Unix, и оно до сих пор остаётся главным способом процесса сказать миру, как всё закончилось. Установщики с их 3010 и 1603, robocopy с маской успеха до семёрки, загадочные 255 от переполненных минус единиц, %ERRORLEVEL% в cmd и $LASTEXITCODE в PowerShell, ловушка скобок и спасительные восклицательные знаки, шестнадцатеричные результаты планировщика и привычка писать exit /b - все эти детали складываются в одно простое правило. Процесс обязан отчитаться числом, а вызывающий обязан это число прочитать правильно. Кто усвоил обе половины правила, тот перестаёт бояться молчаливых сбоев и получает скрипты, которые честно говорят, что у них на уме.