Vérification efficace de l'état de sortie de Bash de plusieurs commandes

261

Existe-t-il quelque chose de similaire à pipefail pour plusieurs commandes, comme une instruction 'try' mais dans bash. Je voudrais faire quelque chose comme ça:

echo "trying stuff"
try {
    command1
    command2
    command3
}

Et à tout moment, si une commande échoue, abandonnez et répercutez l'erreur de cette commande. Je ne veux pas avoir à faire quelque chose comme:

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

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

Et ainsi de suite ... ou quelque chose comme:

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

Parce que les arguments de chaque commande je crois (corrigez-moi si je me trompe) vont interférer les uns avec les autres. Ces deux méthodes me semblent horriblement longues et désagréables, je suis donc ici pour une méthode plus efficace.

jwbensley
la source
2
Jetez un oeil à l'officieux mode strict bash : set -euo pipefail.
Pablo A
1
@PabloBianchi, set -eest une horrible idée. Voir les exercices dans BashFAQ # 105 traitant de quelques-uns des cas marginaux inattendus qu'il introduit, et / ou la comparaison montrant les incompatibilités entre les implémentations de différents shells (et versions de shell) sur in-ulm.de/~mascheck/various/set -e .
Charles Duffy

Réponses:

274

Vous pouvez écrire une fonction qui lance et teste la commande pour vous. Supposons command1et command2sont des variables d'environnement qui ont été définies sur une commande.

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

mytest "$command1"
mytest "$command2"
krtek
la source
32
Ne l'utilisez pas $*, il échouera si des arguments contiennent des espaces; utiliser à la "$@"place. De même, insérez $1les guillemets dans la echocommande.
Gordon Davisson
82
Aussi j'éviterais le nom testcar c'est une commande intégrée.
John Kugelman
1
C'est la méthode que j'ai choisie. Pour être honnête, je ne pense pas avoir été assez clair dans mon message d'origine, mais cette méthode me permet d'écrire ma propre fonction de test afin que je puisse ensuite effectuer une erreur sur les actions que j'aime et qui sont pertinentes pour les actions effectuées dans le scénario. Merci :)
jwbensley
7
Le code de sortie renvoyé par test () ne retournerait-il pas toujours 0 en cas d'erreur puisque la dernière commande exécutée était 'echo'. Vous devrez peut-être enregistrer la valeur de $? première.
magiconair
2
Ce n'est pas une bonne idée et cela encourage les mauvaises pratiques. Prenons le cas simple de ls. Si vous invoquez ls fooet obtenez un message d'erreur du formulaire, ls: foo: No such file or directory\nvous comprenez le problème. Si au lieu de cela, vous vous ls: foo: No such file or directory\nerror with ls\nlaissez distraire par des informations superflues. Dans ce cas, il est assez facile de faire valoir que le superflu est trivial, mais il se développe rapidement. Des messages d'erreur concis sont importants. Mais plus important encore, ce type de wrapper encourage également les rédacteurs à omettre complètement les bons messages d'erreur.
William Pursell
185

Qu'entendez-vous par «abandonner et répéter l'erreur»? Si vous voulez dire que vous voulez que le script se termine dès qu'une commande échoue, faites simplement

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

au début du script (mais notez l'avertissement ci-dessous). Ne vous embêtez pas à faire écho au message d'erreur: laissez la commande défaillante gérer cela. En d'autres termes, si vous le faites:

#!/bin/sh

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

et command2 échoue, lors de l'impression d'un message d'erreur sur stderr, il semble que vous ayez atteint ce que vous vouliez. (À moins que j'interprète mal ce que vous voulez!)

En corollaire, toute commande que vous écrivez doit se comporter correctement: elle doit signaler les erreurs à stderr au lieu de stdout (l'exemple de code dans la question imprime les erreurs à stdout) et elle doit quitter avec un état différent de zéro lorsqu'elle échoue.

Cependant, je ne considère plus cela comme une bonne pratique. set -ea changé sa sémantique avec différentes versions de bash, et bien que cela fonctionne bien pour un script simple, il y a tellement de cas marginaux qu'il est essentiellement inutilisable. (Considérez des choses comme: set -e; foo() { false; echo should not print; } ; foo && echo ok la sémantique ici est quelque peu raisonnable, mais si vous refactorisez le code en une fonction qui reposait sur le paramètre d'option pour se terminer tôt, vous pouvez facilement être mordu.) IMO, il est préférable d'écrire:

 #!/bin/sh

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

ou

#!/bin/sh

command1 && command2 && command3
William Pursell
la source
1
Sachez que bien que cette solution soit la plus simple, elle ne vous permet pas d'effectuer un nettoyage en cas d'échec.
Josh J
6
Le nettoyage peut être accompli avec des pièges. (par exemple trap some_func 0, s'exécutera some_funcà la sortie)
William Pursell
3
Notez également que la sémantique d'errexit (set -e) a changé dans différentes versions de bash, et se comportera souvent de manière inattendue lors de l'invocation de fonctions et d'autres paramètres. Je ne recommande plus son utilisation. OMI, il vaut mieux écrire || exitexplicitement après chaque commande.
William Pursell
87

J'ai un ensemble de fonctions de script que j'utilise intensivement sur mon système Red Hat. Ils utilisent les fonctions système de /etc/init.d/functionspour imprimer des indicateurs d'état verts [ OK ]et rouges [FAILED].

Vous pouvez éventuellement définir la $LOG_STEPSvariable sur un nom de fichier journal si vous souhaitez enregistrer les commandes qui échouent.

Usage

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

Production

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
la source
c'est de l'or pur. Bien que je comprenne comment utiliser le script, je ne saisis pas complètement chaque étape, certainement en dehors de mes connaissances en script bash mais je pense que c'est quand même une œuvre d'art.
kingmilo
2
Cet outil a-t-il un nom officiel? J'adorerais lire une page de manuel sur ce style d'étape / essai / prochaine journalisation
ThorSummoner
Ces fonctions shell semblent indisponibles sur Ubuntu? J'espérais utiliser cela, quelque chose de portable, bien que
ThorSummoner
@ThorSummoner, cela est probablement dû au fait qu'Ubuntu utilise Upstart au lieu de SysV init, et utilisera bientôt systemd. RedHat a tendance à maintenir la compatibilité descendante pendant longtemps, c'est pourquoi le truc init.d est toujours là.
dragon788
J'ai publié une extension de la solution de John et je peux l'utiliser sur des systèmes non RedHat comme Ubuntu. Voir stackoverflow.com/a/54190627/308145
Mark Thomson
51

Pour ce que ça vaut, une façon plus courte d'écrire du code pour vérifier le succès de chaque commande est:

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

C'est toujours fastidieux mais au moins c'est lisible.

John Kugelman
la source
Je n'y ai pas pensé, pas la méthode avec laquelle je suis allé mais c'est rapide et facile à lire, merci pour l'info :)
jwbensley
3
Pour exécuter les commandes en silence et réaliser la même chose:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne
Je suis fan de cette méthode, existe-t-il un moyen d'exécuter plusieurs commandes après le OU? Quelque chose commecommand1 || (echo command1 borked it ; exit)
AndreasKralj
38

Une alternative consiste simplement à joindre les commandes avec &&afin que la première à échouer empêche le reste de s'exécuter:

command1 &&
  command2 &&
  command3

Ce n'est pas la syntaxe que vous avez demandée dans la question, mais c'est un modèle courant pour le cas d'utilisation que vous décrivez. En général, les commandes doivent être responsables des échecs d'impression afin que vous n'ayez pas à le faire manuellement (peut-être avec un -qindicateur pour réduire les erreurs lorsque vous ne les voulez pas). Si vous avez la possibilité de modifier ces commandes, je les éditerais pour crier en cas d'échec, plutôt que de les envelopper dans autre chose qui le fait.


Notez également que vous n'avez pas besoin de faire:

command1
if [ $? -ne 0 ]; then

Vous pouvez simplement dire:

if ! command1; then

Et quand vous avez besoin de vérifier les codes de retour utilisent un contexte arithmétique au lieu de [ ... -ne:

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

Au lieu de créer des fonctions d'exécution ou d'utiliser set -e, utilisez 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

Le piège a même accès au numéro de ligne et à la ligne de commande de la commande qui l'a déclenché. Les variables sont $BASH_LINENOet $BASH_COMMAND.

En pause jusqu'à nouvel ordre.
la source
4
Si vous souhaitez imiter un bloc d'essai de plus près, utilisez trap - ERRpour désactiver le piège à la fin du "bloc".
Gordon Davisson
14

Personnellement, je préfère de loin utiliser une approche légère, comme on le voit ici ;

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

Exemple d'utilisation:

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

run command1 && run command2 && run command3
Erik
la source
6
Ne courez pas $*, cela échouera si des arguments contiennent des espaces; utiliser à la "$@"place. (Bien que $ * soit correct dans la echocommande.)
Gordon Davisson
6

J'ai développé une implémentation try & catch presque sans faille dans bash, qui vous permet d'écrire du code comme:

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

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

Vous pouvez même imbriquer les blocs try-catch à l'intérieur d'eux-mêmes!

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

Le code fait partie de ma plateforme / framework bash . Il étend encore l'idée de try & catch avec des choses comme la gestion des erreurs avec backtrace et exceptions (ainsi que d'autres fonctionnalités intéressantes).

Voici le code qui est juste responsable de 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
}

N'hésitez pas à utiliser, à fourche et à contribuer - c'est sur GitHub .

niieani
la source
1
J'ai regardé le repo et je ne vais pas l'utiliser moi-même, parce que c'est beaucoup trop magique à mon goût (IMO, il vaut mieux utiliser Python si on a besoin de plus de puissance d'abstraction), mais définitivement un grand +1 de ma part car il a l'air tout simplement génial.
Alexander Malakhov
Merci pour les aimables paroles @AlexanderMalakhov. Je suis d'accord sur la quantité de "magie" - c'est l'une des raisons pour lesquelles nous réfléchissons à une version 3.0 simplifiée du cadre, qui sera beaucoup plus facile à comprendre, à déboguer, etc. Il y a un problème ouvert concernant 3.0 sur GH, si vous voudriez puiser dans vos pensées.
niieani
3

Désolé de ne pas pouvoir commenter la première réponse Mais vous devez utiliser une nouvelle instance pour exécuter la commande: 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
la source
2

Pour les utilisateurs de coquille de poisson qui tombent sur ce fil.

Soit fooune fonction qui ne "renvoie" pas (écho) une valeur, mais elle définit le code de sortie comme d'habitude.
Pour éviter de vérifier $statusaprès avoir appelé la fonction, vous pouvez faire:

foo; and echo success; or echo failure

Et s'il est trop long pour tenir sur une seule ligne:

foo; and begin
  echo success
end; or begin
  echo failure
end
Dennis
la source
1

Lorsque j'utilise, sshje dois distinguer les problèmes causés par des problèmes de connexion et les codes d'erreur de la commande à distance en mode errexit( set -e). J'utilise la fonction suivante:

# 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
la source
1

Vous pouvez utiliser la solution géniale de @ john-kugelman trouvée ci-dessus sur les systèmes non RedHat en commentant cette ligne dans son code:

. /etc/init.d/functions

Ensuite, collez le code ci-dessous à la fin. Divulgation complète: il s'agit simplement d'un copier-coller direct des bits pertinents du fichier mentionné ci-dessus provenant de Centos 7.

Testé sur MacOS et 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
la source
0

Vérifier l'état de manière fonctionnelle

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)

}

Usage:

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

Production

Status is 127
slavik
la source