Ефективна перевірка стану виходу Bash з кількох команд


260

Чи є щось подібне до 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

Тому що аргументи кожної команди, на яку я вірю (виправте мене, якщо я помиляюся) будуть заважати один одному. Ці два способи здаються мені жахливими довгодушними і бридкими, тому я тут закликаю до більш ефективного методу.



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"

32
Не використовуйте $*, це не вдасться, якщо в будь-яких аргументах є пробіли; використовувати "$@"замість цього. Аналогічно вставте $1всередину лапки в echoкоманду.
Гордон Девіссон

82
Також я б уникав імені, testоскільки це вбудована команда.
Джон Кугельман

1
Це метод, з яким я пішов. Якщо чесно, я не думаю, що я був досить чітким у своєму початковому дописі, але цей метод дозволяє мені написати власну функцію "тестування", щоб потім я міг виконувати дії з помилками там, які мені подобаються, що стосуються дій, виконаних у сценарій. Дякую :)
jwbensley

7
Чи не код виходу, повернутий тестом (), завжди повертає 0 у випадку помилки, оскільки остання виконувана команда була "ехо". Можливо, вам потрібно буде зберегти значення $? перший.
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

і command2 не вдається, друкуючи повідомлення про помилку на 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

1
Зауважте, що, хоча це рішення є найпростішим, воно не дозволяє вам проводити будь-яку очистку в разі відмови.
Josh J

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
}

це чисте золото. Хоча я розумію, як використовувати сценарій, я не повністю розумію кожен крок, безумовно, поза моїми знаннями сценарію баш, але все ж я думаю, що це витвір мистецтва.
kingmilo

2
Чи має цей інструмент формальну назву? Я хотів би прочитати сторінку чоловіка про цей стиль кроку / спробу / наступного ведення журналу
ThorSummoner

Ці функції оболонки здаються недоступними на 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

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"

8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3

6
Не запускайте $*, він вийде з ладу, якщо в аргументах є пробіли; використовувати "$@"замість цього. (Хоча в echoкоманді $ * нормально .)
Гордон Девіссон

6

Я розробив майже бездоганну реалізацію спробувати і зловити в bash, що дозволяє писати код на зразок:

try 
    echo 'Hello'
    false
    echo 'This will not be displayed'

catch 
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"

Ви навіть можете вкласти гнучкі блоки всередині себе!

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__!"
}

Код є частиною моєї башти / рамки . Це також розширює ідею пробувати та виловлювати такі речі, як поводження з помилками з відступом та винятками (плюс деякі інші приємні функції).

Ось код, який відповідає лише за спробу і ловлю:

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 .


1
Я подивився на репо і не буду цим користуватися сам, тому що це занадто багато магії на мій смак (ІМО краще використовувати Python, якщо потрібна більша потужність абстракції), але, безумовно, великий +1 від мене, тому що це виглядає просто приголомшливо.
Олександр Малахов

Дякую за добрі слова @AlexanderMalakhov. Я погоджуюсь щодо кількості "магії" - це одна з причин, коли ми проводимо мозковий штурм спрощеної версії 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.