it-swarm.com.de

Überprüfen Sie den Bash-Exit-Status mehrerer Befehle effizient

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, lassen Sie den Fehler des Befehls jederzeit ausfallen und wiederholen Sie ihn. Ich möchte nicht 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 irgendetwas wie:

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

Ich glaube, die Argumente jedes Befehls (korrigieren Sie mich, wenn ich falsch liege) werden sich gegenseitig stören. Diese beiden Methoden erscheinen mir furchtbar langatmig und unangenehm, daher fordere ich hier eine effizientere Methode an.

239
jwbensley

Sie können eine Funktion schreiben, die den Befehl für Sie startet und testet. Angenommen, command1 und command2 sind Umgebungsvariablen, die auf einen Befehl gesetzt wurden.

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

mytest $command1
mytest $command2
257
krtek

Was meinst du mit "Drop out and echo the error"? Wenn Sie möchten, dass das Skript beendet wird, sobald ein Befehl fehlschlägt, tun Sie es einfach 

set -e

am Anfang des Skripts (beachten Sie jedoch die Warnung unten). Machen Sie sich nicht die Mühe, die Fehlermeldung zu wiederholen: Lassen Sie den fehlgeschlagenen 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, als hätten Sie das erreicht, was Sie wollen. (Es sei denn, ich interpretiere falsch, was du willst!)

Jeder Befehl, den Sie schreiben, muss sich entsprechend verhalten: Er muss Fehler an stderr statt stdout melden (der Beispielcode in der Frage druckt Fehler an stdout) und muss mit einem Nicht-Null-Status beendet werden, wenn er fehlschlägt. 

Ich halte das jedoch nicht länger für eine gute Praxis. set -e hat seine Semantik mit verschiedenen bash-Versionen geändert, und obwohl es für ein einfaches Skript gut funktioniert, gibt es so viele Edge-Fälle, dass es grundsätzlich unbrauchbar ist. (Denken Sie an Dinge wie: set -e; foo() { false; echo should not print; } ; foo && echo ok Die Semantik hier ist einigermaßen vernünftig, aber wenn Sie Code in eine Funktion umwandeln, die auf der Optionseinstellung basiert, um sie vorzeitig zu beenden, können Sie leicht gebissen werden.)

 #!/bin/sh

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

oder 

#!/bin/sh

command1 && command2 && command3
177
William Pursell

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

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

Verwendungszweck

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 "[email protected]"

    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
        "[email protected]"
    else
        "[email protected]" &
    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
}
85
John Kugelman

Eine kürzere Methode zum Schreiben von Code, um jeden Befehl auf Erfolg zu prüfen, ist:

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

Es ist immer noch langweilig, aber zumindest lesbar.

50
John Kugelman

Eine Alternative besteht einfach darin, die Befehle mit && zu verknüpfen, damit 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 das Drucken von Fehlern verantwortlich sein, sodass Sie dies nicht manuell tun müssen (möglicherweise mit einem -q-Flag, um Fehler abzustellen, 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, statt sie in etwas anderes zu packen, 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
35
dimo414

Anstatt Runner-Funktionen zu erstellen oder set -e zu verwenden, verwenden Sie eine 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_LINENO und $BASH_COMMAND.

30

Ich persönlich ziehe es vor, einen leichtgewichtigen Ansatz zu verwenden, wie in here ;

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "[email protected]" || die "cannot $*"; }
asuser() { Sudo su - "$1" -c "${*:2}"; }

Verwendungsbeispiel:

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

run command1 && run command2 && run command3
8
Erik

Ich habe eine fast fehlerlose try & catch-Implementierung in bash entwickelt, mit der Sie Code wie folgt schreiben können:

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

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

Sie können sogar die Try-Catch-Blöcke 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 meiner bash boilerplate/framework . Es erweitert die Idee von Try & Catch um Dinge wie die Fehlerbehandlung mit Backtrace und Ausnahmen (sowie einige andere Nice-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 spalten und beizutragen - es ist auf GitHub .

6
niieani

Entschuldigung, 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=$([email protected])
    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
3
umount

For fish Shell Benutzer, die über diesen Thread stolpern.

Sei foo eine Funktion, die keinen Wert "zurückgibt" (echo), sondern den Exit-Code wie gewohnt setzt.
Um zu vermeiden, $status nach dem Aufruf der Funktion zu prüfen, können Sie Folgendes tun:

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
2
Dennis

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

# prepare environment on calling site:

rssh="ssh -o ConnectionTimeout=5 -l root $remote_ip"

function exit255 {
    local flags=$-
    set +e
    "[email protected]"
    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
Orient

Status auf Funktion prüfen

assert_exit_status() {

  lambda() {
    local val_fd=$(echo [email protected] | tr -d ' ' | cut -d':' -f2)
    local arg=$1
    shift
    shift
    local cmd=$(echo [email protected] | xargs -E ':')
    local val=$(cat $val_fd)
    eval $arg=$val
    eval $cmd
  }

  local lambda=$1
  shift

  eval [email protected]
  local ret=$?
  $lambda : <(echo $ret)

}

Verwendungszweck:

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

Ausgabe

Status is 127
0
slavik

Sie können @ john-kugelmans s awesome solution verwenden, die oben auf Nicht-RedHat-Systemen gefunden wurden, indem Sie diese Zeile in seinem Code auskommentieren:

. /etc/init.d/functions

Fügen Sie dann den unten stehenden Code am Ende 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
} 
0
Mark Thomson