Эффективная проверка состояния выхода Bash для нескольких команд

261

Есть ли что-то похожее на 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

Потому что аргументы каждой команды, которой я верю (поправьте меня, если я ошибаюсь), будут мешать друг другу. Эти два метода кажутся мне ужасно скучными и неприятными, поэтому я обращаюсь к более эффективному методу.

jwbensley
источник
2
Посмотрите неофициального Баш строгого режима : set -euo pipefail.
Пабло А
1
@PabloBianchi, set -eэто ужасная идея. См. Упражнения в BashFAQ # 105, где обсуждаются лишь некоторые из неожиданных крайних случаев, которые он вводит, и / или сравнение, показывающее несовместимость между реализациями различных оболочек (и версий оболочки), в in-ulm.de/~mascheck/various/set -е .
Чарльз Даффи

Ответы:

274

Вы можете написать функцию, которая запускает и тестирует команду для вас. Предположим, command1и command2переменные среды, которые были установлены для команды.

function mytest {
    "$@"
    local status=$?
    if (( status != 0 )); then
        echo "error with $1" >&2
    fi
    return $status
}

mytest "$command1"
mytest "$command2"
Krtek
источник
32
Не используйте $*, он потерпит неудачу, если в аргументах есть пробелы; используйте "$@"вместо этого. Точно так же поместите $1в кавычки в echoкоманде.
Гордон Дэвиссон
82
Также я бы избегал названия, testпоскольку это встроенная команда.
Джон Кугельман
1
Это метод, с которым я пошел. Честно говоря, я не думаю, что я был достаточно ясен в своем первоначальном посте, но этот метод позволяет мне написать свою собственную функцию 'test', чтобы я мог затем выполнять там действия с ошибками, которые мне нравятся, которые относятся к действиям, выполняемым в сценарий. Спасибо :)
jwbensley
7
Не будет ли код завершения, возвращаемый test (), всегда возвращать 0 в случае ошибки, поскольку последняя выполненная команда была 'echo'. Вам может понадобиться сохранить значение $? первый.
magiconair
2
Это не очень хорошая идея, и она поощряет плохую практику. Рассмотрим простой случай ls. Если вы вызываете ls fooи получаете сообщение об ошибке формы, ls: foo: No such file or directory\nвы понимаете проблему. Если вместо этого вы получаете, ls: foo: No such file or directory\nerror with ls\nвы отвлекаетесь от лишней информации. В этом случае достаточно легко утверждать, что избыточность тривиальна, но она быстро растет. Краткие сообщения об ошибках важны. Но что еще более важно, этот тип оболочки поощряет слишком писателей полностью опускать хорошие сообщения об ошибках.
Уильям Перселл
185

Что вы подразумеваете под "выкинуть и повторить ошибку"? Если вы имеете в виду, что хотите, чтобы скрипт завершился, как только какая-либо команда не сработала, просто сделайте

set -e    # DON'T do this.  See commentary below.

в начале сценария (но обратите внимание на предупреждение ниже). Не пытайтесь повторить сообщение об ошибке: пусть ошибочная команда справится с этим. Другими словами, если вы делаете:

#!/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 семантика здесь несколько разумна, но если вы преобразуете код в функцию, которая полагается на настройку параметра для преждевременного завершения, вы можете легко получить укус.) IMO лучше написать:

 #!/bin/sh

 command1 || exit
 command2 || exit
 command3 || exit

или

#!/bin/sh

command1 && command2 && command3
Уильям Перселл
источник
1
Имейте в виду, что хотя это простейшее решение, оно не позволяет выполнять очистку при сбое.
Джош Дж
6
Очистка может быть выполнена с ловушками. (например trap some_func 0, выполнит some_funcна выходе)
Уильям Перселл
3
Также обратите внимание, что семантика errexit (set -e) изменилась в разных версиях bash и часто будет вести себя неожиданно во время вызова функции и других настроек. Я больше не рекомендую его использовать. ИМО, лучше писать || exitявно после каждой команды.
Уильям Перселл
87

У меня есть набор функций сценариев, которые я широко использую в своей системе 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
}
Джон Кугельман
источник
это чистое золото. Хотя я понимаю, как использовать сценарий, я не полностью понимаю каждый шаг, определенно за пределами моих знаний сценариев bash, но, тем не менее, я думаю, что это произведение искусства.
kingmilo
2
У этого инструмента есть формальное имя? Я хотел бы прочитать
справочную
Эти функции оболочки, кажется, недоступны в Ubuntu? Я надеялся использовать это, что-то портативное, хотя
ThorSummoner
@ThorSummoner, это скорее всего потому, что Ubuntu использует Upstart вместо SysV init и скоро будет использовать systemd. RedHat имеет тенденцию поддерживать обратную совместимость в течение длительного времени, поэтому материал init.d все еще там.
dragon788
Я опубликовал расширение решения Джона и разрешил его использование в системах, отличных от RedHat, таких как Ubuntu. См. Stackoverflow.com/a/54190627/308145
Марк Томсон
51

Для краткости написания кода для проверки успешности каждой команды:

command1 || echo "command1 borked it"
command2 || echo "command2 borked it"

Это все еще утомительно, но по крайней мере это читается.

Джон Кугельман
источник
Не думал об этом, не о методе, который я использовал, но его легко и быстро прочитать, спасибо за информацию :)
jwbensley
3
Выполнять команды молча и достичь того же:command1 &> /dev/null || echo "command1 borked it"
Мэтт Бирн
Я фанат этого метода, есть ли способ выполнить несколько команд после ИЛИ? Нечто подобноеcommand1 || (echo command1 borked it ; exit)
AndreasKralj
38

Альтернатива состоит в том, чтобы просто объединить команды вместе с &&тем, чтобы первая, которая потерпит неудачу, препятствовала выполнению остальных:

command1 &&
  command2 &&
  command3

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


Обратите внимание, что вам не нужно делать:

command1
if [ $? -ne 0 ]; then

Вы можете просто сказать:

if ! command1; then

И когда вы делаете необходимость проверки кодов возврата использовать арифметическую контекст вместо [ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then
dimo414
источник
34

Вместо того, чтобы создавать функции бегуна или использовать 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.

Приостановлено до дальнейшего уведомления.
источник
4
Если вы хотите подражать блоку еще более близко, используйте, trap - ERRчтобы выключить ловушку в конце «блока».
Гордон Дэвиссон
14

Лично я предпочитаю использовать легкий подход, как показано здесь ;

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"
sleepycal
источник
8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3
Erik
источник
6
Не запускайте $*, он потерпит неудачу, если в аргументах есть пробелы; используйте "$@"вместо этого. (Хотя $ * в echoкоманде хорошо .)
Гордон Дэвиссон
6

Я разработал почти безупречную реализацию 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 .

niieani
источник
1
Я смотрел на репо и не собираюсь использовать это сам, потому что это слишком много волшебства на мой вкус (IMO лучше использовать Python, если нужно больше мощи абстракции), но определенно большое +1 от меня, потому что это выглядит просто потрясающе.
Александр Малахов
Спасибо за добрые слова @ Александр Малахов. Я согласен с количеством «магии» - это одна из причин, по которой мы проводим мозговой штурм в упрощенной версии 3.0 фреймворка, который будет намного легче понять, отладить и т. Д. Есть открытый вопрос о 3.0 на GH, если Вы хотели бы включить ваши мысли.
niieani
3

Извините, что я не могу прокомментировать первый ответ, но вы должны использовать новый экземпляр для выполнения команды: 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
размонтировать
источник
2

Для пользователей раковин рыбы, которые натыкаются на эту тему.

Позвольте fooбыть функцией, которая не "возвращает" (эхо) значение, но она устанавливает код выхода, как обычно.
Чтобы избежать проверки $statusпосле вызова функции, вы можете сделать:

foo; and echo success; or echo failure

И если это слишком долго, чтобы поместиться в одну строку:

foo; and begin
  echo success
end; or begin
  echo failure
end
Деннис
источник
1

Когда я использую, 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
Томилов Анатолий
источник
1

Вы можете использовать удивительное решение @ 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
} 
Марк Томсон
источник
0

Проверка статуса в функциональном порядке

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
Славик
источник