Эффективная проверка состояния выхода Bash для нескольких команд
Есть ли что-то похожее на pipefail для нескольких команд, например оператор try, но внутри bash. Я хотел бы сделать что-то вроде этого:
echo "trying stuff"
try {
command1
command2
command3
}
И в любой момент, если какая-либо команда завершится неудачно, пропустите и отобразите ошибку этой команды. Я не хочу делать что-то вроде:
command1
if [ $? -ne 0 ]; then
echo "command1 borked it"
fi
command2
if [ $? -ne 0 ]; then
echo "command2 borked it"
fi
И так далее... или что-то вроде:
pipefail -o
command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3
Потому что аргументы каждой команды, которой я верю (поправьте меня, если я ошибаюсь), будут мешать друг другу. Эти два метода кажутся мне ужасно скучными и неприятными, поэтому я призываю к более эффективному методу.
16 ответов
Вы можете написать функцию, которая запускает и тестирует команду для вас. Предполагать command1
а также command2
переменные среды, которые были установлены для команды.
function mytest {
"$@"
local status=$?
if [ $status -ne 0 ]; then
echo "error with $1" >&2
fi
return $status
}
mytest $command1
mytest $command2
Что вы имеете в виду под "выкинуть и повторить ошибку"? Если вы имеете в виду, что хотите, чтобы скрипт завершился, как только какая-либо команда не выполнена, просто выполните
set -e
в начале скрипта (но обратите внимание на предупреждение ниже). Не беспокойтесь о том, чтобы повторить сообщение об ошибке: пусть ошибочная команда справится с этим. Другими словами, если вы делаете:
#!/bin/sh
set -e # Use caution. eg, don't do this
command1
command2
command3
и команда2 завершается неудачно, при выводе сообщения об ошибке в stderr, кажется, что вы достигли того, чего хотите. (Если я не понимаю, что вы хотите!)
Как следствие, любая команда, которую вы пишете, должна вести себя хорошо: она должна сообщать об ошибках в stderr вместо stdout (пример кода в вопросе печатает ошибки в stdout) и должна выходить с ненулевым состоянием в случае неудачи.
Однако я больше не считаю это хорошей практикой. set -e
изменил свою семантику с различными версиями bash, и хотя он отлично работает для простого скрипта, существует так много крайних случаев, что он по сути непригоден для использования. (Рассмотрим такие вещи, как: set -e; foo() { false; echo should not print; } ; foo && echo ok
Семантика здесь несколько разумна, но если вы преобразуете код в функцию, которая полагается на настройку параметра для преждевременного завершения, вы можете легко получить укус.) ИМО лучше написать:
#!/bin/sh
command1 || exit
command2 || exit
command3 || exit
или же
#!/bin/sh
command1 && command2 && command3
У меня есть набор функций сценариев, которые я широко использую в своей системе Red Hat. Они используют системные функции из /etc/init.d/functions
печатать зеленый [ OK ]
и красный [FAILED]
индикаторы состояния.
При желании вы можете установить $LOG_STEPS
переменная к имени файла журнала, если вы хотите записать, какие команды не удалось.
использование
step "Installing XFS filesystem tools:"
try rpm -i xfsprogs-*.rpm
next
step "Configuring udev:"
try cp *.rules /etc/udev/rules.d
try udevtrigger
next
step "Adding rc.postsysinit hook:"
try cp rc.postsysinit /etc/rc.d/
try ln -s rc.d/rc.postsysinit /etc/rc.postsysinit
try echo $'\nexec /etc/rc.postsysinit' >> /etc/rc.sysinit
next
Выход
Installing XFS filesystem tools: [ OK ]
Configuring udev: [FAILED]
Adding rc.postsysinit hook: [ OK ]
Код
#!/bin/bash
. /etc/init.d/functions
# Use step(), try(), and next() to perform a series of commands and print
# [ OK ] or [FAILED] at the end. The step as a whole fails if any individual
# command fails.
#
# Example:
# step "Remounting / and /boot as read-write:"
# try mount -o remount,rw /
# try mount -o remount,rw /boot
# next
step() {
echo -n "$@"
STEP_OK=0
[[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$
}
try() {
# Check for `-b' argument to run command in the background.
local BG=
[[ $1 == -b ]] && { BG=1; shift; }
[[ $1 == -- ]] && { shift; }
# Run the command.
if [[ -z $BG ]]; then
"$@"
else
"$@" &
fi
# Check if command failed and update $STEP_OK if so.
local EXIT_CODE=$?
if [[ $EXIT_CODE -ne 0 ]]; then
STEP_OK=$EXIT_CODE
[[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$
if [[ -n $LOG_STEPS ]]; then
local FILE=$(readlink -m "${BASH_SOURCE[1]}")
local LINE=${BASH_LINENO[0]}
echo "$FILE: line $LINE: Command \`$*' failed with exit code $EXIT_CODE." >> "$LOG_STEPS"
fi
fi
return $EXIT_CODE
}
next() {
[[ -f /tmp/step.$$ ]] && { STEP_OK=$(< /tmp/step.$$); rm -f /tmp/step.$$; }
[[ $STEP_OK -eq 0 ]] && echo_success || echo_failure
echo
return $STEP_OK
}
Для краткости написания кода для проверки успешности каждой команды:
command1 || echo "command1 borked it"
command2 || echo "command2 borked it"
Это все еще утомительно, но по крайней мере это читабельно.
Альтернативой является просто объединить команды вместе с &&
так что первый сбой предотвращает выполнение остальных:
command1 &&
command2 &&
command3
Это не тот синтаксис, который вы задали в вопросе, но это общий шаблон описанного вами варианта использования. В общем случае команды должны отвечать за сбои печати, чтобы вам не приходилось делать это вручную (возможно, с -q
флаг, чтобы замолчать ошибки, когда вы не хотите их). Если у вас есть возможность изменить эти команды, я бы отредактировал их так, чтобы они выкрикивали при неудаче, а не заключал их в что-то еще, что делает это.
Обратите внимание, что вам не нужно делать:
command1
if [ $? -ne 0 ]; then
Вы можете просто сказать:
if ! command1; then
Вместо того, чтобы создавать функции бегуна или использовать set -e
, использовать trap
:
trap 'echo "error"; do_cleanup failed; exit' ERR
trap 'echo "received signal to stop"; do_cleanup interrupted; exit' SIGQUIT SIGTERM SIGINT
do_cleanup () { rm tempfile; echo "$1 $(date)" >> script_log; }
command1
command2
command3
Ловушка даже имеет доступ к номеру строки и командной строке команды, которая ее сработала. Переменные $BASH_LINENO
а также $BASH_COMMAND
,
Лично я предпочитаю использовать легкий подход, как показано здесь;
yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
asuser() { sudo su - "$1" -c "${*:2}"; }
Пример использования:
try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"
run() {
$*
if [ $? -ne 0 ]
then
echo "$* failed with exit code $?"
return 1
else
return 0
fi
}
run command1 && run command2 && run command3
Я разработал почти безупречную реализацию try & catch в bash, которая позволяет вам писать такой код:
try
echo 'Hello'
false
echo 'This will not be displayed'
catch
echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
Вы можете даже вложить блоки try-catch внутри себя!
try {
echo 'Hello'
try {
echo 'Nested Hello'
false
echo 'This will not execute'
} catch {
echo "Nested Caught (@ $__EXCEPTION_LINE__)"
}
false
echo 'This will not execute too'
} catch {
echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
}
Код является частью моего bash шаблон / рамки. Это также расширяет идею try & catch с такими вещами, как обработка ошибок с обратным следом и исключениями (плюс некоторые другие приятные функции).
Вот код, который отвечает только за try & catch:
set -o pipefail
shopt -s expand_aliases
declare -ig __oo__insideTryCatch=0
# if try-catch is nested, then set +e before so the parent handler doesn't catch us
alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e;
__oo__insideTryCatch+=1; ( set -e;
trap \"Exception.Capture \${LINENO}; \" ERR;"
alias catch=" ); Exception.Extract \$? || "
Exception.Capture() {
local script="${BASH_SOURCE[1]#./}"
if [[ ! -f /tmp/stored_exception_source ]]; then
echo "$script" > /tmp/stored_exception_source
fi
if [[ ! -f /tmp/stored_exception_line ]]; then
echo "$1" > /tmp/stored_exception_line
fi
return 0
}
Exception.Extract() {
if [[ $__oo__insideTryCatch -gt 1 ]]
then
set -e
fi
__oo__insideTryCatch+=-1
__EXCEPTION_CATCH__=( $(Exception.GetLastException) )
local retVal=$1
if [[ $retVal -gt 0 ]]
then
# BACKWARDS COMPATIBILE WAY:
# export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}"
# export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}"
export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}"
export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}"
export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}"
return 1 # so that we may continue with a "catch"
fi
}
Exception.GetLastException() {
if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]]
then
cat /tmp/stored_exception
cat /tmp/stored_exception_line
cat /tmp/stored_exception_source
else
echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}"
fi
rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source
return 0
}
Не стесняйтесь использовать, раскошелиться и внести свой вклад - это на GitHub.
Извините, что не могу сделать комментарий к первому ответу, но вы должны использовать новый экземпляр для выполнения команды: cmd_output=$($@)
#!/bin/bash
function check_exit {
cmd_output=$($@)
local status=$?
echo $status
if [ $status -ne 0 ]; then
echo "error with $1" >&2
fi
return $status
}
function run_command() {
exit 1
}
check_exit run_command
Вы можете использовать потрясающее решение @john-kugelman, найденное выше для систем, не относящихся к RedHat, закомментировав эту строку в своем коде:
. /etc/init.d/functions
Затем вставьте приведенный ниже код в конце. Полное раскрытие: это просто прямое копирование и вставка соответствующих битов вышеупомянутого файла, взятых из Centos 7.
Протестировано на MacOS и Ubuntu 18.04.
BOOTUP=color
RES_COL=60
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \\033[1;32m"
SETCOLOR_FAILURE="echo -en \\033[1;31m"
SETCOLOR_WARNING="echo -en \\033[1;33m"
SETCOLOR_NORMAL="echo -en \\033[0;39m"
echo_success() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_SUCCESS
echo -n $" OK "
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 0
}
echo_failure() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
echo -n $"FAILED"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}
echo_passed() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
echo -n $"PASSED"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}
echo_warning() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
echo -n $"WARNING"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}
Для пользователей раковин рыбы, которые натыкаются на эту тему.
Позволять foo
быть функцией, которая не "возвращает" (эхо) значение, но устанавливает код выхода как обычно.
Чтобы избежать проверки $status
после вызова функции вы можете сделать:
foo; and echo success; or echo failure
И если это слишком долго, чтобы поместиться на одной строке:
foo; and begin
echo success
end; or begin
echo failure
end
Когда я использую ssh
Мне нужно различать проблемы, вызванные проблемами подключения и кодами ошибок удаленной команды в errexit
(set -e
) Режим. Я использую следующую функцию:
# prepare environment on calling site:
rssh="ssh -o ConnectionTimeout=5 -l root $remote_ip"
function exit255 {
local flags=$-
set +e
"$@"
local status=$?
set -$flags
if [[ $status == 255 ]]
then
exit 255
else
return $status
fi
}
export -f exit255
# callee:
set -e
set -o pipefail
[[ $rssh ]]
[[ $remote_ip ]]
[[ $( type -t exit255 ) == "function" ]]
rjournaldir="/var/log/journal"
if exit255 $rssh "[[ ! -d '$rjournaldir/' ]]"
then
$rssh "mkdir '$rjournaldir/'"
fi
rconf="/etc/systemd/journald.conf"
if [[ $( $rssh "grep '#Storage=auto' '$rconf'" ) ]]
then
$rssh "sed -i 's/#Storage=auto/Storage=persistent/' '$rconf'"
fi
$rssh systemctl reenable systemd-journald.service
$rssh systemctl is-enabled systemd-journald.service
$rssh systemctl restart systemd-journald.service
sleep 1
$rssh systemctl status systemd-journald.service
$rssh systemctl is-active systemd-journald.service
Если вы хотите выйти сerror code 1
сразу после неудачной команды:
Один лайнер:
command1 || { echo "command1 borked it"; exit 1; }
command2 || { echo "command2 borked it"; exit 1; }
Будьте осторожны, добавляя пробел после{
и раньше}
как показано выше.
Дополнительная информация:
Обратите внимание, что вы не можете использовать круглые скобки следующим образом:
command1 || (echo "command1 borked it" && exit 1)
Потому что(exit 1)
будет работать в подоболочке. Таким образом, он выйдет из подоболочки , но НЕ выйдет из родительской оболочки. Для получения дополнительной информации проверьте этот ответ: https://unix.stackexchange.com/a/172543/513474 .
предполагать
alias command1='grep a <<<abc'
alias command2='grep x <<<abc'
alias command3='grep c <<<abc'
либо
{ command1 1>/dev/null || { echo "cmd1 fail"; /bin/false; } } && echo "cmd1 succeed" &&
{ command2 1>/dev/null || { echo "cmd2 fail"; /bin/false; } } && echo "cmd2 succeed" &&
{ command3 1>/dev/null || { echo "cmd3 fail"; /bin/false; } } && echo "cmd3 succeed"
или же
{ { command1 1>/dev/null && echo "cmd1 succeed"; } || { echo "cmd1 fail"; /bin/false; } } &&
{ { command2 1>/dev/null && echo "cmd2 succeed"; } || { echo "cmd2 fail"; /bin/false; } } &&
{ { command3 1>/dev/null && echo "cmd3 succeed"; } || { echo "cmd3 fail"; /bin/false; } }
урожаи
cmd1 succeed
cmd2 fail
Утомительно это. Но читаемость не плохая.
Проверка статуса в функциональном порядке
assert_exit_status() {
lambda() {
local val_fd=$(echo $@ | tr -d ' ' | cut -d':' -f2)
local arg=$1
shift
shift
local cmd=$(echo $@ | xargs -E ':')
local val=$(cat $val_fd)
eval $arg=$val
eval $cmd
}
local lambda=$1
shift
eval $@
local ret=$?
$lambda : <(echo $ret)
}
Использование:
assert_exit_status 'lambda status -> [[ $status -ne 0 ]] && echo Status is $status.' lls
Выход
Status is 127