Effizientes Überprüfen des Bash-Exit-Status mehrerer Befehle

260

Gibt es etwas Ähnliches wie Pipefail für mehrere Befehle, wie eine 'try'-Anweisung, aber innerhalb von bash. Ich möchte so etwas machen:

echo "trying stuff"
try {
    command1
    command2
    command3
}

Wenn ein Befehl fehlschlägt, können Sie den Fehler dieses Befehls jederzeit beenden und wiederholen. Ich möchte nicht so etwas tun müssen wie:

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

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

Und so weiter ... oder so ähnlich:

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

Weil sich die Argumente jedes Befehls, von dem ich glaube (korrigieren Sie mich, wenn ich falsch liege), gegenseitig stören. Diese beiden Methoden scheinen mir schrecklich langwierig und böse zu sein, deshalb appelliere ich hier an eine effizientere Methode.

jwbensley
quelle
2
Schauen Sie sich den inoffiziellen Bash-Strict-Modus an : set -euo pipefail.
Pablo A
1
@PabloBianchi, set -eist eine schreckliche Idee. In den Übungen in BashFAQ # 105 werden nur einige der unerwarteten Randfälle erläutert , die eingeführt werden, und / oder der Vergleich zeigt Inkompatibilitäten zwischen den Implementierungen verschiedener Shells (und Shell-Versionen) unter inulm.de/~mascheck/various/set -e .
Charles Duffy

Antworten:

274

Sie können eine Funktion schreiben, die den Befehl für Sie startet und testet. Es sei angenommen , command1und command2sind Umgebungsvariablen , die auf einen Befehl gesetzt wurden.

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

mytest "$command1"
mytest "$command2"
krtek
quelle
32
Nicht verwenden $*, es schlägt fehl, wenn Argumente Leerzeichen enthalten. Verwenden Sie "$@"stattdessen. Fügen Sie in ähnlicher Weise $1die Anführungszeichen im echoBefehl ein.
Gordon Davisson
82
Außerdem würde ich den Namen vermeiden, testda dies ein eingebauter Befehl ist.
John Kugelman
1
Dies ist die Methode, mit der ich gegangen bin. Um ehrlich zu sein, ich glaube nicht, dass ich in meinem ursprünglichen Beitrag klar genug war, aber diese Methode ermöglicht es mir, meine eigene 'Test'-Funktion zu schreiben, damit ich dort eine Fehleraktion ausführen kann, die für die in durchgeführten Aktionen relevant ist das Skript. Danke :)
jwbensley
7
Würde der von test () zurückgegebene Exit-Code im Fehlerfall nicht immer 0 zurückgeben, da der zuletzt ausgeführte Befehl 'echo' war? Möglicherweise müssen Sie den Wert von $ speichern? zuerst.
Magiconair
2
Dies ist keine gute Idee und fördert schlechte Praktiken. Betrachten Sie den einfachen Fall von ls. Wenn Sie ls foodas Formular aufrufen und eine Fehlermeldung erhalten ls: foo: No such file or directory\n, verstehen Sie das Problem. Wenn Sie stattdessen bekommen, werden ls: foo: No such file or directory\nerror with ls\nSie von überflüssigen Informationen abgelenkt. In diesem Fall ist es leicht zu argumentieren, dass der Überfluss trivial ist, aber er wächst schnell. Prägnante Fehlermeldungen sind wichtig. Noch wichtiger ist jedoch, dass diese Art von Wrapper auch Autoren dazu ermutigt, gute Fehlermeldungen vollständig wegzulassen.
William Pursell
185

Was meinst du mit "aussteigen und den Fehler wiederholen"? Wenn Sie möchten, dass das Skript beendet wird, sobald ein Befehl fehlschlägt, tun Sie dies einfach

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

zu Beginn des Skripts (beachten Sie jedoch die Warnung unten). Machen Sie sich nicht die Mühe, die Fehlermeldung zu wiederholen: Lassen Sie den fehlerhaften Befehl damit umgehen. Mit anderen Worten, wenn Sie dies tun:

#!/bin/sh

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

und command2 schlägt fehl, während eine Fehlermeldung an stderr gedruckt wird, dann scheint es, dass Sie erreicht haben, was Sie wollen. (Es sei denn, ich interpretiere falsch, was Sie wollen!)

Folglich muss sich jeder Befehl, den Sie schreiben, gut verhalten: Er muss Fehler an stderr anstelle von stdout melden (der Beispielcode in der Frage gibt Fehler an stdout aus) und er muss mit einem Status ungleich Null beendet werden, wenn er fehlschlägt.

Ich halte dies jedoch nicht mehr für eine gute Praxis. set -ehat seine Semantik mit verschiedenen Versionen von bash geändert, und obwohl es für ein einfaches Skript gut funktioniert, gibt es so viele Randfälle, dass es im Wesentlichen unbrauchbar ist. (Betrachten Sie Dinge wie: set -e; foo() { false; echo should not print; } ; foo && echo ok Die Semantik hier ist etwas vernünftig, aber wenn Sie Code in eine Funktion umgestalten, die sich auf die Optionseinstellung stützt, um vorzeitig zu beenden, können Sie leicht gebissen werden.) IMO ist es besser zu schreiben:

 #!/bin/sh

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

oder

#!/bin/sh

command1 && command2 && command3
William Pursell
quelle
1
Beachten Sie, dass diese Lösung zwar die einfachste ist, Sie jedoch bei einem Fehler keine Bereinigung durchführen können.
Josh J
6
Die Reinigung kann mit Fallen durchgeführt werden. (zB trap some_func 0wird some_funcam Ausgang ausgeführt)
William Pursell
3
Beachten Sie auch, dass sich die Semantik von errexit (set -e) in verschiedenen Versionen von bash geändert hat und sich beim Aufrufen von Funktionen und anderen Einstellungen häufig unerwartet verhält. Ich empfehle die Verwendung nicht mehr. IMO ist es besser, || exitnach jedem Befehl explizit zu schreiben .
William Pursell
87

Ich habe eine Reihe von Skriptfunktionen, die ich auf meinem Red Hat-System häufig verwende. Sie verwenden die Systemfunktionen von /etc/init.d/functions, um grüne [ OK ]und rote [FAILED]Statusanzeigen zu drucken .

Sie können die $LOG_STEPSVariable optional auf einen Protokolldateinamen setzen, wenn Sie protokollieren möchten, welche Befehle fehlschlagen.

Verwendung

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

Ausgabe

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

Code

#!/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
quelle
Das ist reines Gold. Obwohl ich verstehe, wie man das Skript verwendet, verstehe ich nicht jeden Schritt vollständig, definitiv außerhalb meines Bash-Scripting-Wissens, aber ich denke, es ist trotzdem ein Kunstwerk.
Kingmilo
2
Hat dieses Tool einen formalen Namen? Ich würde gerne eine Manpage über diese Art von Schritt / Versuch / nächster Protokollierung
lesen
Diese Shell-Funktionen scheinen unter Ubuntu nicht verfügbar zu sein? Ich hatte gehofft, dies zu verwenden, etwas tragbares
ThorSummoner
@ThorSummoner, dies ist wahrscheinlich, weil Ubuntu Upstart anstelle von SysV init verwendet und bald systemd verwenden wird. RedHat neigt dazu, die Abwärtskompatibilität lange aufrechtzuerhalten, weshalb das init.d-Zeug immer noch vorhanden ist.
Dragon788
Ich habe eine Erweiterung für Johns Lösung veröffentlicht und erlaube die Verwendung auf Nicht-RedHat-Systemen wie Ubuntu. Siehe stackoverflow.com/a/54190627/308145
Mark Thomson
51

Für das, was es wert ist, ist eine kürzere Möglichkeit, Code zu schreiben, um jeden Befehl auf Erfolg zu überprüfen:

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

Es ist immer noch langweilig, aber zumindest lesbar.

John Kugelman
quelle
Ich habe nicht daran gedacht, nicht an die Methode, die ich gewählt habe, aber sie ist schnell und einfach zu lesen, danke für die Info :)
jwbensley
3
Um die Befehle still auszuführen und dasselbe zu erreichen:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne
Ich bin ein Fan dieser Methode. Gibt es eine Möglichkeit, mehrere Befehle nach dem OP auszuführen? So etwas wiecommand1 || (echo command1 borked it ; exit)
AndreasKralj
38

Eine Alternative besteht einfach darin, die Befehle zusammenzufügen, &&sodass der erste Fehler die Ausführung des Restes verhindert:

command1 &&
  command2 &&
  command3

Dies ist nicht die Syntax, nach der Sie in der Frage gefragt haben, aber es ist ein allgemeines Muster für den von Ihnen beschriebenen Anwendungsfall. Im Allgemeinen sollten die Befehle für Druckfehler verantwortlich sein, damit Sie dies nicht manuell tun müssen (möglicherweise mit einem -qFlag, um Fehler auszuschalten, wenn Sie sie nicht möchten). Wenn Sie die Möglichkeit haben, diese Befehle zu ändern, würde ich sie so bearbeiten, dass sie bei einem Fehler schreien, anstatt sie in etwas anderes zu verpacken, das dies tut.


Beachten Sie auch, dass Sie nicht tun müssen:

command1
if [ $? -ne 0 ]; then

Sie können einfach sagen:

if ! command1; then

Wenn Sie die Rückkehrcodes überprüfen müssen, verwenden Sie einen arithmetischen Kontext anstelle von [ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then
dimo414
quelle
34

Verwenden Sie Folgendes set -e, anstatt Runner-Funktionen zu erstellen oder zu verwenden 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

Der Trap hat sogar Zugriff auf die Zeilennummer und die Befehlszeile des Befehls, der ihn ausgelöst hat. Die Variablen sind $BASH_LINENOund $BASH_COMMAND.

Bis auf weiteres angehalten.
quelle
4
Wenn Sie einen Try-Block noch genauer nachahmen möchten, trap - ERRschalten Sie die Falle am Ende des "Blocks" aus.
Gordon Davisson
14

Persönlich bevorzuge ich einen leichten Ansatz, wie hier zu sehen ;

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

Anwendungsbeispiel:

try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"
schläfrig
quelle
8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3
Erik
quelle
6
Nicht ausführen $*, es schlägt fehl, wenn Argumente Leerzeichen enthalten. Verwenden Sie "$@"stattdessen. (Obwohl $ * im echoBefehl
Gordon Davisson
6

Ich habe eine nahezu fehlerfreie Try & Catch-Implementierung in Bash entwickelt, mit der Sie Code schreiben können wie:

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

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

Sie können die Try-Catch-Blöcke sogar in sich selbst verschachteln!

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

Der Code ist Teil meines Bash Boilerplate / Frameworks . Es erweitert die Idee des Try & Catch weiter um Dinge wie Fehlerbehandlung mit Backtrace und Ausnahmen (plus einige andere nette Funktionen).

Hier ist der Code, der nur für try & catch verantwortlich ist:

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
}

Fühlen Sie sich frei zu verwenden, zu teilen und beizutragen - es ist auf GitHub .

niieani
quelle
1
Ich habe mir Repo angesehen und werde es nicht selbst verwenden, weil es viel zu magisch für meinen Geschmack ist (IMO ist es besser, Python zu verwenden, wenn man mehr Abstraktionskraft benötigt), aber definitiv groß +1 von mir, weil es einfach fantastisch aussieht.
Alexander Malakhov
Vielen Dank für die freundlichen Worte @AlexanderMalakhov. Ich stimme der Menge an "Magie" zu - das ist einer der Gründe, warum wir eine vereinfachte 3.0-Version des Frameworks erarbeiten, die viel einfacher zu verstehen, zu debuggen usw. ist. Es gibt ein offenes Problem zu 3.0 bei GH, wenn Sie möchten Ihre Gedanken einfließen lassen.
Niieani
3

Es tut mir leid, dass ich zur ersten Antwort keinen Kommentar abgeben kann. Sie sollten jedoch eine neue Instanz verwenden, um den Befehl auszuführen: 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
quelle
2

Für Benutzer von Fischschalen , die über diesen Thread stolpern.

Sei fooeine Funktion, die keinen Wert "zurückgibt" (echo), sondern den Exit-Code wie gewohnt setzt.
Um eine Überprüfung $statusnach dem Aufrufen der Funktion zu vermeiden , haben Sie folgende Möglichkeiten :

foo; and echo success; or echo failure

Und wenn es zu lang ist, um in eine Zeile zu passen:

foo; and begin
  echo success
end; or begin
  echo failure
end
Dennis
quelle
1

Wenn ich benutze, muss sshich zwischen Problemen unterscheiden, die durch Verbindungsprobleme verursacht werden, und Fehlercodes des Remote-Befehls im errexit( set -e) -Modus. Ich benutze die folgende Funktion:

# 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
Tomilov Anatoliy
quelle
1

Sie können die großartige Lösung von @ john-kugelman verwenden, die Sie oben auf Nicht-RedHat-Systemen gefunden haben, indem Sie diese Zeile in seinem Code auskommentieren :

. /etc/init.d/functions

Fügen Sie dann am Ende den folgenden Code ein. Vollständige Offenlegung: Dies ist nur ein direktes Kopieren und Einfügen der relevanten Bits der oben genannten Datei aus Centos 7.

Getestet unter MacOS und 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
quelle
0

Status auf funktionale Weise prüfen

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)

}

Verwendungszweck:

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

Ausgabe

Status is 127
slawik
quelle