Эффективная проверка состояния выхода Bash для нескольких команд - PullRequest
247 голосов
/ 04 марта 2011

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

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

Ответы [ 14 ]

264 голосов
/ 04 марта 2011

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

function mytest {
    "$@"
    local status=$?
    if [ $status -ne 0 ]; then
        echo "error with $1" >&2
    fi
    return $status
}

mytest $command1
mytest $command2
180 голосов
/ 04 марта 2011

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

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

 #!/bin/sh

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

или

#!/bin/sh

command1 && command2 && command3
86 голосов
/ 04 марта 2011

У меня есть набор функций сценариев, которые я активно использую в своей системе 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
}
50 голосов
/ 04 марта 2011

Для этого стоит написать более короткий способ проверки кода для каждой команды:

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

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

35 голосов
/ 10 июля 2015

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

command1 &&
  command2 &&
  command3

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


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

command1
if [ $? -ne 0 ]; then

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

if ! command1; then
30 голосов
/ 04 марта 2011

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

14 голосов
/ 24 октября 2014

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

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"
8 голосов
/ 04 марта 2011
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3
6 голосов
/ 04 мая 2015

Я разработал почти безупречную реализацию 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 с такими вещами, как обработка ошибок с помощью backtrace и исключений (плюс некоторые другие приятные функции).

Вот код, который отвечает только за 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 .

3 голосов
/ 03 октября 2014

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