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ę.


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"

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

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
}

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.


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

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.


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"

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

run command1 && run command2 && run command3

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 .


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

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

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

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
} 

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.