Verifica efficiente dello stato di uscita Bash di diversi comandi


261

C'è qualcosa di simile a pipefail per più comandi, come un'istruzione 'try' ma all'interno di bash. Vorrei fare qualcosa del genere:

echo "trying stuff"
try {
    command1
    command2
    command3
}

E in qualsiasi momento, se un comando non riesce, abbandonare e fare eco all'errore di quel comando. Non voglio fare qualcosa del tipo:

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

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

E così via ... o qualcosa del genere:

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

Perché gli argomenti di ciascun comando in cui credo (correggimi se sbaglio) interferiranno l'uno con l'altro. Questi due metodi mi sembrano orribilmente prolissi e cattivi per me, quindi sono qui attraente per un metodo più efficiente.


2
Date un'occhiata a The Unofficial modalità rigorosa bash : set -euo pipefail.
Pablo A

1
@PabloBianchi, set -eè un'idea orribile . Vedi gli esercizi in BashFAQ # 105 che discutono solo alcuni dei casi limite inaspettati che introduce e / o il confronto che mostra incompatibilità tra le diverse implementazioni di shell (e versioni di shell) su in-ulm.de/~mascheck/various/set -e .
Charles Duffy,

Risposte:


274

Puoi scrivere una funzione che avvia e verifica il comando per te. Supponiamo command1che command2siano variabili d'ambiente che sono state impostate su un comando.

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

mytest "$command1"
mytest "$command2"

32
Non usare $*, fallirà se qualche argomento contiene degli spazi; usa "$@"invece. Allo stesso modo, inserisci $1le virgolette nel echocomando.
Gordon Davisson

82
Inoltre eviterei il nome in testquanto è un comando integrato.
John Kugelman,

1
Questo è il metodo con cui sono andato. Ad essere sincero, non credo di essere stato abbastanza chiaro nel mio post originale, ma questo metodo mi consente di scrivere la mia funzione 'test' in modo da poter quindi eseguire un errore in azioni che mi piacciono che sono rilevanti per le azioni eseguite in il copione. Grazie :)
jwbensley il

7
Il codice di uscita restituito da test () non restituirebbe sempre 0 in caso di errore poiché l'ultimo comando eseguito era 'echo'. Potrebbe essere necessario salvare il valore di $? primo.
magiconair,

2
Questa non è una buona idea e incoraggia le cattive pratiche. Considera il semplice caso di ls. Se invochi ls fooe ricevi un messaggio di errore del modulo ls: foo: No such file or directory\n, capisci il problema. Se invece ti ls: foo: No such file or directory\nerror with ls\ncapita di essere distratto da informazioni superflue. In questo caso, è abbastanza facile sostenere che la superfluità è banale, ma cresce rapidamente. I messaggi di errore concisi sono importanti. Ma soprattutto, questo tipo di wrapper incoraggia anche gli scrittori a omettere completamente i buoni messaggi di errore.
William Pursell,

185

Cosa intendi con "abbandona e fai eco all'errore"? Se vuoi dire che vuoi che lo script termini non appena un comando fallisce, allora fallo

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

all'inizio dello script (ma nota l'avvertenza di seguito). Non preoccuparti di fare eco al messaggio di errore: lascia che sia il comando in errore a gestirlo. In altre parole, se lo fai:

#!/bin/sh

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

e command2 ha esito negativo, durante la stampa di un messaggio di errore su stderr, sembra che tu abbia ottenuto ciò che desideri. (A meno che non interpreti male quello che vuoi!)

Come corollario, qualsiasi comando che scrivi deve comportarsi bene: deve riportare gli errori a stderr invece di stdout (il codice di esempio nella domanda stampa gli errori su stdout) e deve uscire con uno stato diverso da zero quando fallisce.

Tuttavia, non considero più questa una buona pratica. set -eha cambiato la sua semantica con diverse versioni di bash e sebbene funzioni bene per un semplice script, ci sono così tanti casi limite che è essenzialmente inutilizzabile. (Considera cose come: set -e; foo() { false; echo should not print; } ; foo && echo ok la semantica qui è in qualche modo ragionevole, ma se rifletti il ​​codice in una funzione che si basava sull'impostazione dell'opzione per terminare in anticipo, puoi facilmente essere morso.) IMO è meglio scrivere:

 #!/bin/sh

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

o

#!/bin/sh

command1 && command2 && command3

1
Tieni presente che, sebbene questa soluzione sia la più semplice, non ti consente di eseguire alcuna pulizia in caso di errore.
Josh J,

6
La pulizia può essere eseguita con trappole. (ad es. trap some_func 0verrà eseguito some_funcall'uscita)
William Pursell,

3
Si noti inoltre che la semantica di errexit (set -e) è cambiata in diverse versioni di bash e spesso si comporterà inaspettatamente durante l'invocazione della funzione e altre impostazioni. Non ne consiglio più l'uso. IMO, è meglio scrivere || exitesplicitamente dopo ogni comando.
William Pursell,

87

Ho un set di funzioni di scripting che uso ampiamente sul mio sistema Red Hat. Usano le funzioni di sistema /etc/init.d/functionsper stampare gli indicatori di stato verde [ OK ]e rosso [FAILED].

Se lo si desidera, è possibile impostare la $LOG_STEPSvariabile su un nome file di registro se si desidera che i comandi non riescano.

uso

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

Produzione

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

Codice

#!/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
}

questo è oro puro. Mentre capisco come usare la sceneggiatura non afferro completamente ogni passo, sicuramente al di fuori delle mie conoscenze di scripting bash ma penso che sia comunque un'opera d'arte.
Kingmilo,

2
Questo strumento ha un nome formale? Mi piacerebbe leggere una pagina man su questo stile di step / try / next logging
ThorSummoner

Queste funzioni della shell sembrano non essere disponibili su Ubuntu? Speravo di usare questo, qualcosa di portatile, però
ThorSummoner,

@ThorSummoner, questo è probabilmente perché Ubuntu utilizza Upstart invece di SysV init e presto utilizzerà systemd. RedHat tende a mantenere a lungo la compatibilità con le versioni precedenti, motivo per cui le cose init.d sono ancora lì.
dragon788,

Ho pubblicato un'espansione sulla soluzione di John e ne consente l'utilizzo su sistemi non RedHat come Ubuntu. Vedi stackoverflow.com/a/54190627/308145
Mark Thomson

51

Per quello che vale, un modo più breve per scrivere il codice per controllare ogni comando per avere successo è:

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

È ancora noioso ma almeno è leggibile.


Non ci ho pensato, non il metodo con cui sono andato ma è facile e veloce da leggere, grazie per le informazioni :)
jwbensley,

3
Per eseguire i comandi in silenzio e ottenere la stessa cosa:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne,

Sono un fan di questo metodo, c'è un modo per eseguire più comandi dopo l'OR? Qualcosa del generecommand1 || (echo command1 borked it ; exit)
AndreasKralj

38

Un'alternativa è semplicemente quella di unire i comandi in &&modo che il primo fallisca impedisca l'esecuzione del resto:

command1 &&
  command2 &&
  command3

Questa non è la sintassi che hai chiesto nella domanda, ma è un modello comune per il caso d'uso che descrivi. In generale i comandi dovrebbero essere responsabili degli errori di stampa in modo da non doverlo fare manualmente (magari con una -qbandiera per silenziare gli errori quando non li si desidera). Se hai la possibilità di modificare questi comandi, li modificherei per urlare in caso di fallimento, piuttosto che avvolgerli in qualcos'altro che lo fa.


Si noti inoltre che non è necessario eseguire:

command1
if [ $? -ne 0 ]; then

Puoi semplicemente dire:

if ! command1; then

E quando si fa necessità di controllare i codici di ritorno utilizzano un contesto aritmetica invece di [ ... -ne:

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

34

Invece di creare funzioni runner o utilizzare set -e, utilizzare un 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

La trap ha anche accesso al numero di riga e alla riga di comando del comando che l'ha attivata. Le variabili sono $BASH_LINENOe $BASH_COMMAND.


4
Se vuoi imitare un blocco try ancora più da vicino, usa trap - ERRper disattivare la trap alla fine del "blocco".
Gordon Davisson,

14

Personalmente preferisco di gran lunga usare un approccio leggero, come visto qui ;

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

Esempio di utilizzo:

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
Non eseguire $*, fallirà se qualche argomento contiene degli spazi; usa "$@"invece. (Anche se $ * è ok nel echocomando.)
Gordon Davisson

6

Ho sviluppato un'implementazione try & catch quasi impeccabile in bash, che ti permette di scrivere codice come:

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

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

Puoi persino annidare i blocchi try-catch dentro di loro!

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

Il codice fa parte del mio bash boilerplate / framework . Estende ulteriormente l'idea di provare e catturare con cose come la gestione degli errori con backtrace ed eccezioni (oltre ad alcune altre belle funzioni).

Ecco il codice responsabile solo di 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
}

Sentiti libero di usare, fork e contribuire - è su GitHub .


1
Ho esaminato il repository e non lo userò da solo, perché è troppo magico per i miei gusti (IMO è meglio usare Python se uno ha bisogno di più potere di astrazione), ma sicuramente grande +1 da parte mia perché sembra semplicemente fantastico.
Alexander Malakhov,

Grazie per le belle parole @AlexanderMalakhov. Sono d'accordo sulla quantità di "magia" - questo è uno dei motivi per cui stiamo facendo il brainstorming di una versione 3.0 semplificata del framework, che sarà molto più facile da capire, da debug, ecc. C'è un problema aperto su 3.0 su GH, se vorresti scheggiare nei tuoi pensieri.
niieani,

3

Spiacente, non posso fare un commento alla prima risposta, ma dovresti usare una nuova istanza per eseguire il comando: 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

Per gli utenti di gusci di pesce che inciampano in questo thread.

Sia foouna funzione che non "restituisce" (eco) un valore, ma imposta il codice di uscita come al solito.
Per evitare il controllo $statusdopo aver chiamato la funzione, è possibile effettuare:

foo; and echo success; or echo failure

E se è troppo lungo per stare su una riga:

foo; and begin
  echo success
end; or begin
  echo failure
end

1

Quando uso sshho bisogno di distinguere tra problemi causati da problemi di connessione e codici di errore del comando remoto in modalità errexit( set -e). Uso la seguente funzione:

# 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

Puoi usare la fantastica soluzione di @ john-kugelman trovata sopra sui sistemi non RedHat commentando questa riga nel suo codice:

. /etc/init.d/functions

Quindi, incolla il codice seguente alla fine. Divulgazione completa: questa è solo una copia e incolla diretta dei bit rilevanti del file sopra menzionato presa da Centos 7.

Testato su MacOS e 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

Verifica dello stato in modo funzionale

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)

}

Uso:

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

Produzione

Status is 127
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.