Sprawne sprawdzanie statusu wyjścia Bash kilku poleceń

260

Czy istnieje coś podobnego do pipefail dla wielu poleceń, na przykład instrukcja „try”, ale w bash. Chciałbym zrobić coś takiego:

echo "trying stuff"
try {
    command1
    command2
    command3
}

I w dowolnym momencie, jeśli jakieś polecenie zawiedzie, porzuć i powtórz błąd tego polecenia. Nie chcę robić czegoś takiego:

command1
if [ $? -ne 0 ]; then
    echo "command1 borked it"
fi

command2
if [ $? -ne 0 ]; then
    echo "command2 borked it"
fi

I tak dalej ... lub coś w stylu:

pipefail -o
command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3

Ponieważ uważam, że argumenty każdego polecenia (popraw mnie, jeśli się mylę) będą się wzajemnie zakłócały. Te dwie metody wydają mi się strasznie długie i nieprzyjemne, dlatego apeluję o bardziej wydajną metodę.

jwbensley
źródło
2
Spójrz na nieoficjalnej bash trybie ścisłym : set -euo pipefail.
Pablo A
1
@PabloBianchi, set -eto okropny pomysł. Zobacz ćwiczenia w BashFAQ # 105 omawiające tylko kilka nieoczekiwanych przypadków brzegowych, które wprowadza, i / lub porównanie pokazujące niezgodności między implementacjami różnych powłok (i wersji powłok) na stronie inulm.de/~mascheck/various/set -e .
Charles Duffy

Odpowiedzi:

274

Możesz napisać funkcję, która uruchomi i przetestuje polecenie. Załóż command1i command2są zmiennymi środowiskowymi, które zostały ustawione na polecenie.

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

mytest "$command1"
mytest "$command2"
krtek
źródło
32
Nie używaj $*, zakończy się niepowodzeniem, jeśli w argumentach będą spacje; użyj "$@"zamiast tego. Podobnie umieść $1w cudzysłowie w echopoleceniu.
Gordon Davisson
82
Również unikałbym tej nazwy, testponieważ jest to wbudowane polecenie.
John Kugelman,
1
To jest metoda, którą poszedłem. Szczerze mówiąc, nie sądzę, że byłem wystarczająco jasny w moim oryginalnym poście, ale ta metoda pozwala mi napisać własną funkcję „testową”, dzięki czemu mogę wtedy wykonać działania błędu, które podobają się do działań wykonanych w scenariusz. Dzięki :)
jwbensley
7
Czy kod wyjścia zwrócony przez test () nie zawsze zwraca 0 w przypadku błędu, ponieważ ostatnim wykonanym poleceniem było „echo”. Może być konieczne zapisanie wartości $? pierwszy.
magiconair,
2
To nie jest dobry pomysł i zachęca do złych praktyk. Rozważ prosty przypadek ls. Jeśli wywołasz ls fooi otrzymasz komunikat o błędzie formularza ls: foo: No such file or directory\n, rozumiesz problem. Jeśli zamiast tego ls: foo: No such file or directory\nerror with ls\nzostaniesz rozproszony przez zbędne informacje. W takim przypadku łatwo jest argumentować, że nadmiar jest trywialny, ale szybko rośnie. Ważne są zwięzłe komunikaty o błędach. Ale co ważniejsze, ten rodzaj opakowania zachęca zbyt pisarzy, aby całkowicie pomijali dobre komunikaty o błędach.
William Pursell,
185

Co rozumiesz przez „porzuć i powtórz błąd”? Jeśli masz na myśli, że chcesz, aby skrypt zakończył się natychmiast po niepowodzeniu dowolnego polecenia, po prostu zrób to

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

na początku skryptu (ale uwaga poniżej). Nie zawracaj sobie głowy wyświetlaniem komunikatu o błędzie: pozwól temu błędnemu poleceniu sobie z tym poradzić. Innymi słowy, jeśli to zrobisz:

#!/bin/sh

set -e    # Use caution.  eg, don't do this
command1
command2
command3

i polecenie 2 nie powiedzie się, podczas drukowania komunikatu o błędzie do stderr, wtedy wydaje się, że osiągnąłeś to, co chcesz. (Chyba że źle interpretuję to, czego chcesz!)

Jako następstwo każde napisane polecenie musi zachowywać się dobrze: musi zgłaszać błędy do stderr zamiast do standardowego wyjścia (przykładowy kod w pytaniu drukuje błędy do standardowego wyjścia) i musi zakończyć działanie z niezerowym statusem, gdy się nie powiedzie.

Nie uważam jednak tego za dobrą praktykę. set -ezmienił swoją semantykę z różnymi wersjami bash i chociaż działa dobrze w przypadku prostego skryptu, jest tak wiele przypadków krawędzi, że w zasadzie jest on bezużyteczny. (Rozważ takie rzeczy jak: set -e; foo() { false; echo should not print; } ; foo && echo ok semantyka tutaj jest nieco rozsądna, ale jeśli przekodujesz kod na funkcję, która polegała na ustawieniu opcji wcześniejszego zakończenia, możesz łatwo zostać ugryziony.) IMO lepiej napisać:

 #!/bin/sh

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

lub

#!/bin/sh

command1 && command2 && command3
William Pursell
źródło
1
Pamiętaj, że chociaż to rozwiązanie jest najprostsze, nie pozwala na wykonanie żadnego czyszczenia po awarii.
Josh J
6
Oczyszczanie można przeprowadzić za pomocą pułapek. (np. trap some_func 0wykona się some_funcprzy wyjściu)
William Pursell
3
Zauważ też, że semantyka errexit (zestaw -e) zmieniła się w różnych wersjach bash i często zachowuje się nieoczekiwanie podczas wywoływania funkcji i innych ustawień. Nie polecam już jego użycia. IMO lepiej jest pisać || exitwprost po każdym poleceniu.
William Pursell
87

Mam zestaw funkcji skryptowych, z których często korzystam w systemie Red Hat. Używają funkcji systemowych od /etc/init.d/functionsdo drukowania zielonego [ OK ]i czerwonego[FAILED] wskaźników stanu.

Możesz opcjonalnie ustawić $LOG_STEPS zmienną na nazwę pliku dziennika, jeśli chcesz rejestrować, które polecenia się nie powiodły.

Stosowanie

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

Wynik

Installing XFS filesystem tools:        [  OK  ]
Configuring udev:                       [FAILED]
Adding rc.postsysinit hook:             [  OK  ]

Kod

#!/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
}
John Kugelman
źródło
to jest czyste złoto. Rozumiem, jak korzystać ze skryptu, ale nie do końca rozumiem każdy krok, zdecydowanie poza moją wiedzą na temat skryptów bash, ale myślę, że to jednak dzieło sztuki.
kingmilo
2
Czy to narzędzie ma formalną nazwę? Chciałbym przeczytać stronę
podręcznika
Te funkcje powłoki wydają się niedostępne na Ubuntu? Chciałem jednak użyć tego, czegoś przenośnego
ThorSummoner,
@ThorSummoner, jest to prawdopodobne, ponieważ Ubuntu używa Upstart zamiast SysV init i wkrótce będzie używać systemd. RedHat ma tendencję do utrzymywania zgodności wstecznej przez długi czas, dlatego też pliki init.d wciąż tam są.
dragon788
Opublikowałem rozszerzenie o rozwiązaniu Johna i pozwala ono na stosowanie go w systemach innych niż RedHat, takich jak Ubuntu. Patrz stackoverflow.com/a/54190627/308145
Mark Thomson
51

Co jest warte, krótszy sposób na napisanie kodu, aby sprawdzić, czy każde polecenie się powiodło, to:

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

Nadal jest nudny, ale przynajmniej czytelny.

John Kugelman
źródło
Nie myślałem o tym, nie o metodzie, z którą poszedłem, ale jest szybki i łatwy do odczytania, dzięki za informacje :)
jwbensley
3
Aby wykonać polecenia w ciszy i osiągnąć to samo:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne
Jestem fanem tej metody, czy jest sposób na wykonanie wielu poleceń po OR? Coś w stylucommand1 || (echo command1 borked it ; exit)
AndreasKralj
38

Alternatywą jest po prostu łączenie poleceń razem, &&aby pierwsza, która uległa awarii, uniemożliwiła wykonanie pozostałej części:

command1 &&
  command2 &&
  command3

To nie jest składnia, o którą prosiłeś w pytaniu, ale jest to powszechny wzorzec opisywanego przypadku użycia. Ogólnie polecenia powinny być odpowiedzialne za awarie drukowania, abyś nie musiał tego robić ręcznie (być może z -qflagą, aby wyciszyć błędy, gdy ich nie chcesz). Jeśli masz możliwość modyfikowania tych poleceń, edytowałbym je, aby krzyczeć na niepowodzenie, zamiast zawijać je w coś, co to robi.


Zauważ też, że nie musisz robić:

command1
if [ $? -ne 0 ]; then

Możesz po prostu powiedzieć:

if ! command1; then

I kiedy zrobić potrzebę sprawdzenia kody powrotne użyć arytmetyczną kontekst zamiast [ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then
dimo414
źródło
34

Zamiast tworzyć funkcje runnera lub używać set -e, użyj 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

Pułapka ma nawet dostęp do numeru linii i wiersza polecenia, które ją uruchomiło. Zmienne to $BASH_LINENOi $BASH_COMMAND.

Wstrzymano do odwołania.
źródło
4
Jeśli chcesz jeszcze bardziej naśladować blok prób, użyj, trap - ERRaby wyłączyć pułapkę na końcu „bloku”.
Gordon Davisson,
14

Osobiście wolę stosować lekkie podejście, jak widać tutaj ;

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
asuser() { sudo su - "$1" -c "${*:2}"; }

Przykładowe użycie:

try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"
senny
źródło
8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3
Erik
źródło
6
Nie uruchamiaj $*, zakończy się niepowodzeniem, jeśli w argumentach będą spacje; użyj "$@"zamiast tego. (Chociaż polecenie $ * jest w porządku echo.)
Gordon Davisson
6

Opracowałem prawie bezbłędną implementację try & catch w bash, która umożliwia pisanie kodu takiego jak:

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

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

Możesz nawet zagnieżdżać w sobie bloki 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__!"
}

Kod jest częścią mojej płyty bazowej / frameworku bash . To dodatkowo rozszerza pomysł try & catch na takie rzeczy jak obsługa błędów z funkcją śledzenia wstecznego i wyjątkami (plus kilka innych fajnych funkcji).

Oto kod odpowiedzialny tylko za 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
}

Nie krępuj się używać, rozwidlać i dodawać - jest na GitHub .

niieani
źródło
1
Spojrzałem na repo i sam go nie użyję, ponieważ to według mnie za dużo magii (IMO lepiej jest używać Pythona, jeśli potrzebujemy więcej mocy abstrakcji), ale zdecydowanie duża +1 ode mnie, ponieważ wygląda po prostu niesamowicie.
Alexander Malakhov
Dzięki za miłe słowa @AlexanderMalakhov. Zgadzam się co do ilości „magii” - to jeden z powodów, dla których przeprowadzamy burzę mózgów uproszczonej wersji 3.0 frameworka, który będzie znacznie łatwiejszy do zrozumienia, debugowania itp. Istnieje otwarty problem dotyczący 3.0 w GH, jeśli chcesz zagłębić się w swoje myśli.
niieani
3

Przepraszam, że nie mogę skomentować pierwszej odpowiedzi. Powinieneś jednak użyć nowej instancji, aby wykonać polecenie: 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
umount
źródło
2

Dla użytkowników skorupy ryb, którzy natkną się na ten wątek.

Niech foobędzie funkcją, która nie „zwraca” (echo) wartości, ale ustawia kod wyjścia jak zwykle.
Aby uniknąć sprawdzania $statuspo wywołaniu funkcji, możesz:

foo; and echo success; or echo failure

A jeśli jest za długi, aby zmieścić się w jednej linii:

foo; and begin
  echo success
end; or begin
  echo failure
end
Dennis
źródło
1

Kiedy używam ssh, muszę rozróżnić problemy spowodowane przez problemy z połączeniem i kody błędów polecenia zdalnego w trybie errexit( set -e). Korzystam z następującej funkcji:

# 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
Tomiłow Anatolij
źródło
1

Możesz użyć niesamowitego rozwiązania @ john-kugelman znalezionego powyżej w systemach innych niż RedHat, komentując ten wiersz w jego kodzie:

. /etc/init.d/functions

Następnie wklej poniższy kod na końcu. Pełne ujawnienie: Jest to tylko bezpośrednia kopia i wklejenie odpowiednich fragmentów wyżej wspomnianego pliku pobranego z Centos 7.

Testowane na MacOS i 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
} 
Mark Thomson
źródło
0

Sprawdzanie statusu w sposób funkcjonalny

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)

}

Stosowanie:

assert_exit_status 'lambda status -> [[ $status -ne 0 ]] && echo Status is $status.' lls

Wynik

Status is 127
slavik
źródło