Вопрос или проблема
Я пытаюсь автоматизировать процесс, который включает выполнение скриптов на различных машинах через ssh. Важно захватить как вывод, так и код возврата (для обнаружения ошибок).
Установка кода выхода явно работает, как и ожидалось:
~$ ssh host exit 5 && echo OK || echo FAIL
FAIL
Однако, если есть оболочка, сигнализирующая о некорректном выходе, ssh всегда возвращает 0 (скрипт имитируется выполнением строки):
~$ ssh host sh -c 'exit 5' && echo OK || echo FAIL
OK
Выполнение того же скрипта на хосте в интерактивной оболочке работает прекрасно:
~$ sh -c 'exit 5' && echo OK || echo FAIL
FAIL
Я запутался, почему это происходит. Как я могу заставить ssh передать код возврата bash? Я не могу изменить удаленные скрипты.
Я использую аутентификацию по открытым ключам, закрытый ключ разблокирован – нет необходимости в взаимодействии с пользователем. Все системы работают на Ubuntu 18.04. Версии приложений:
OpenSSH_7.6p1 Ubuntu-4ubuntu0.1, OpenSSL 1.0.2n 7 Dec 2017
GNU bash, Version 4.4.19(1)-release (x86_64-pc-linux-gnu)
Примечание: Этот вопрос отличается от этих, казалось бы, похожих вопросов:
- bash shell – захват вывода и кода выхода удаленного скрипта через ssh?
- https://stackoverflow.com/questions/15390978/shell-script-ssh-command-exit-status
- https://stackoverflow.com/questions/36726995/exit-code-from-ssh-command
- https://superuser.com/questions/652729/command-executed-via-ssh-does-not-return-proper-return-code
Как отмечено в ответе, который вы уже имеете, удаленный sh
не выполняет exit 5
. Только exit
:
$ ssh test sh -x -c 'exit 5'; echo $?
+ exit
0
Что происходит здесь, объясняется, например, в этом ответе:
ssh
выполняет удаленную оболочку и передает строку в нее, а не список аргументов.
Когда мы выполняем ssh host sh -c 'exit 5'
:
- Локальная оболочка удаляет одинарные кавычки (удаление кавычек);
- Клиент
ssh
получает аргументыhost
,sh
,-c
иexit 5
. Он объединяет их в строку и отправляет удаленному хосту; - На удаленном хосте
ssh
вызывает оболочку и передает ей строкуsh -c exit 5
; - Удаленная оболочка вызывает
sh
и передает ей опцию-c
,exit
как строку команды, и5
как имя команды.
Обратите внимание, что если мы добавим слова после exit 5
, они просто передаются в sh
как дальнейшие аргументы – никаких ошибок, связанных с тем, что они не распознаются оболочкой:
$ ssh test sh -x -c 'exit 5' a b c; echo $?
+ exit
0
strace
подтверждает, что 5
не является частью строки команды, переданной sh
, здесь; это аргумент:
$ ssh test strace -e execve sh -c 'exit 5'; echo $?
execve("/usr/bin/sh", ["sh", "-c", "exit", "5"], 0x7ffc0d744c38 /* 14 vars */) = 0
+++ вышел с 0 +++
0
Чтобы выполнить sh -c 'command'
на удаленном хосте, как задумано, мы должны убедиться, что правильно отправляем кавычки тоже:
$ ssh test "sh -x -c 'exit 5'"; echo $?
+ exit 5
5
Чтобы было ясно, что кавычение всей удаленной команды не имеет отношения к нашей текущей проблеме, мы могли бы просто написать:
$ ssh test sh -x -c "'exit 5'"; echo $?
+ exit 5
5
Экранирование внутренних кавычек с помощью обратных слэшей вместо двойных кавычек тоже сработает.
Примечание о команде ssh host sh -c ':; exit 5'
(из комментариев к вашему вопросу). Что она делает:
$ ssh test sh -x -c ':; exit 5'; echo $?
+ :
5
То есть exit 5
выполняется внешней оболочкой, а не sh
. Снова, чтобы позволить sh
выйти с нужным кодом:
$ ssh test sh -x -c "':; exit 5'"; echo $?
+ :
+ exit 5
5
Я смог воспроизвести это, используя команду, которую вы использовали, и смог решить это, обернув удаленную команду в кавычки. Вот мои тестовые случаи:
#!/bin/bash -x
echo 'Тест без кавычек:'
ssh evil sh -x -c exit 5 && echo OK || echo FAIL
echo 'Тест с кавычками 1:'
ssh evil sh -x -c 'exit 5' && echo OK || echo FAIL
echo 'Тест с кавычками 2:'
ssh evil 'sh -x -c "exit 5"' && echo OK || echo FAIL
Вот результаты:
bash-[540]$ bash -x test.sh
+ echo 'Тест без кавычек:'
Тест без кавычек:
+ ssh evil sh -x -c exit 5
+ exit
+ echo OK
OK
+ echo 'Тест с кавычками 1:'
Тест с кавычками 1:
+ ssh evil sh -x -c 'exit 5'
+ exit
+ echo OK
OK
+ echo 'Тест с кавычками 2:'
Тест с кавычками 2:
+ ssh evil 'sh -x -c "exit 5"'
+ exit 5
+ echo FAIL
FAIL
В первом и втором тестах, кажется, 5
не передается в exit
, как мы ожидали. Он просто, похоже, исчезает. Он не идет в exit
, sh
не жалуется на 5: команда не найдена
, и ssh
не жалуется на это.
В третьем тесте exit 5
заключено в кавычки внутри более крупной команды, чтобы запустить на удаленном хосте, так же как и во втором тесте. Это гарантирует, что 5
передается в exit
, и оба выполняются как опция -c
для sh
. Разница между вторым и третьим тестами в том, что весь набор команд и аргументов отправляется на удаленный хост в кавычках как один аргумент командной строки для ssh
.
Другие ответы хорошо отвечают на вопрос, опираясь на приведенные примеры. Мое реальное приложение более сложное и включает в себя серию скриптов и подпроцессов. Вот упрощенный пример скрипта, который я хочу выполнить:
#!/bin/bash
sub-process-that-fails
# сохранить и вывести код возврата для целей отладки
rc=$?
echo $rc
exit $rc
Пытаясь убедиться, что удаленно выполняемая оболочка действительно является bash, а не dash (как отметил @JeffSchaller), я попробовал вызвать скрипт вот так:
~$ ssh -t -t host /bin/bash -x /srv/scripts/run.sh ; echo $?
Что привело к этому странному выводу:
+ sub-process-that-fails
+ rc=5
+ echo 5
5
+ exit 5
0
После часов поиска я заметил, что в .bashrc
был установлен trap 'kill 0' EXIT
. Это сделано, чтобы убить все подпроцессы в случае, если bash будет убит. Отладка bash не отображает выполнение этой ловушки. Я переместил ловушку в обёрточный скрипт. Теперь я вижу, что на самом деле выполняется:
+ trap 'kill 0' EXIT
+ sub-process-that-fails
+ rc=5
5
+ echo 5
+ exit 5
+ kill 0
0
Удаленная оболочка выходит с кодом последней выполненной команды. Это kill 0
, и она выходит с 0.
Ответ или решение
Решение проблемы: Неверный код выхода при выполнении bash скрипта через SSH
При автоматизации процессов, связанных с удалённым выполнением скриптов через SSH, важно правильно обработать коды выхода для эффективного обнаружения ошибок. В данном ответе мы рассмотрим, почему команда, выполняемая через SSH, возвращает код выхода 0, даже если в скрипте явно указано «exit 5», а также предложим решение данной проблемы.
1. Понимание проблемы
Когда вы выполняете команду через SSH, возникает ряд нюансов, которые влияют на возвращаемый код выхода. Рассмотрим следующее:
ssh host sh -c 'exit 5' && echo OK || echo FAIL
На первый взгляд, вышеуказанная команда должна вернуть «FAIL», поскольку она должна завершаться с кодом выхода 5. Однако вывод будет «OK». Важно понять, что:
- При выполнении команды
ssh
удалённый процесс получает на вход строку, а не список аргументов, как это происходит в интерактивной оболочке. - Команда
sh -c 'exit 5'
будет интерпретирована какsh -c exit 5
, и5
будет интерпретироваться как аргумент, а не как часть командыexit
.
Эта ошибка также заметна, если вы попытаетесь выполнить команду с дополнительными аргументами:
ssh host sh -c 'exit 5' a b c
В этом случае 5
будет передано как второй аргумент, что снова приведет к неверному коду выхода.
2. Правильное использование кавычек
Чтобы корректно передать код выхода в удалённую оболочку, необходимо правильно использовать кавычки:
ssh host "sh -c 'exit 5'" && echo OK || echo FAIL
Или можно использовать экранирование:
ssh host sh -c "'exit 5'" && echo OK || echo FAIL
Также можно использовать двойные кавычки, чтобы объединить команду в единую строку:
ssh host "sh -c \"exit 5\"" && echo OK || echo FAIL
Теперь удалённый процесс будет корректно получать команду exit 5
и завершаться с необходимым кодом.
3. Усложненный сценарий с подскриптами
В вашем случае, если вы работаете со сложным скриптом, где существуют подскрипты, которые могут завершаться с ошибками, важно подтвердить, что данная структура кода также правильно обрабатывает коды выхода.
Пример скрипта:
#!/bin/bash
sub-process-that-fails
rc=$?
echo $rc
exit $rc
Чтобы правильно запустить этот скрипт и получить корректный код выхода, используйте следующую команду:
ssh -t -t host "/bin/bash -x /srv/scripts/run.sh" ; echo $?
Если вы столкнулись с дополнительными проблемами, проверьте, не установлены ли ловушки (trap), которые могут изменять код выхода. Например, наличие команды trap 'kill 0' EXIT
в вашем .bashrc
может привести к ошибкам, так как она завершает все подпроцессы, что также приводит к возвращению кода 0.
Заключение
Используйте правильные кавычки и экранирование для передачи команд через SSH, чтобы убедиться, что код выхода передается корректно. При работе со скриптами и подскриптами следите за тем, чтобы код выхода не играл первую скрипту, и избегайте ловушек, которые могут повлиять на финальный код завершения.
Внимательность к деталям в ваших командах позволит устранить такие проблемы на ранних стадиях и обеспечит надежное выполнение ваших автоматизированных процессов.