web-dev-qa-db-fra.com

Vérification efficace du statut de sortie de Bash de plusieurs commandes

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

echo "trying stuff"
try {
    command1
    command2
    command3
}

Et à tout moment, si une commande échoue, abandonnez et répétez 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 paraissent terriblement lentes et désagréables, alors je lance un appel à une méthode plus efficace.

239
jwbensley

Vous pouvez écrire une fonction qui lance et teste la commande pour vous. Supposons que command1 et command2 sont des variables d’environnement définies en tant que commande.

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

mytest $command1
mytest $command2
257
krtek

Qu'entendez-vous par "abandonner et faire écho à l'erreur"? Si vous voulez dire que vous voulez que le script se termine dès qu'une commande échoue, alors faites simplement 

set -e

au début du script (mais notez l’avertissement ci-dessous). Ne vous inquiétez pas du message d'erreur: laissez la commande défaillante s'en charger. En d'autres termes, si vous le faites:

#!/bin/sh

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

et commande2 échoue, alors que vous affichez un message d'erreur sur stderr, il semble alors que vous ayez obtenu ce que vous souhaitiez. (À moins que j'interprète mal ce que vous voulez!)

En corollaire, toute commande que vous écrivez doit bien se comporter: elle doit signaler les erreurs à stderr au lieu de stdout (le code exemple de la question affiche les erreurs sur stdout) et doit se terminer avec un statut différent de zéro en cas d'échec. 

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

 #!/bin/sh

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

ou 

#!/bin/sh

command1 && command2 && command3
177
William Pursell

J'ai un ensemble de fonctions de script que j'utilise beaucoup sur mon système Red Hat. Ils utilisent les fonctions système de /etc/init.d/functions pour imprimer les indicateurs d'état [ OK ] vert et [FAILED] rouge.

Vous pouvez éventuellement définir la variable $LOG_STEPS sur un nom de fichier journal si vous souhaitez consigner les commandes qui ont échoué.

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

Sortie

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
}
85
John Kugelman

Pour ce que cela vaut, un moyen plus simple d’écrire du code pour vérifier le succès de chaque commande est le suivant:

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

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

50
John Kugelman

Une alternative consiste simplement à joindre les commandes avec && afin que le premier à échouer empêche le reste de l'exécution:

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 indicateur -q pour supprimer les erreurs lorsque vous ne les souhaitez 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 quelque chose d'autre qui le ferait.


Notez aussi que vous n'avez pas besoin de faire:

command1
if [ $? -ne 0 ]; then

Vous pouvez simplement dire:

if ! command1; then
35
dimo414

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_LINENO et $BASH_COMMAND.

30

Personnellement, je préfère de loin utiliser une approche légère, comme vu 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"
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

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 mon bash boilerplate/framework . Il étend encore l’idée d’essayer et d’attraper des choses telles que la gestion des erreurs avec la trace de retour et les exceptions (plus quelques autres fonctionnalités de Nice). 

Voici le code responsable juste pour 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, fourchette et contribuer - c'est sur GitHub .

6
niieani

Désolé, je ne peux pas 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
3
umount

Pour Fish Shell les utilisateurs qui trébuchent sur ce fil.

Soit foo une fonction qui ne "retourne" pas (echo) une valeur, mais définit le code de sortie comme d'habitude.
Pour éviter de vérifier $status après avoir appelé la fonction, vous pouvez effectuer les opérations suivantes:

foo; and echo success; or echo failure

Et si c'est trop long pour tenir sur une seule ligne:

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

Lorsque j'utilise ssh, je 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
1
Orient

Vérification du statut 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

Sortie

Status is 127
0
slavik

Vous pouvez utiliser la solution awesome de @ john-kugelman trouvée ci-dessus sur des systèmes autres que 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 éléments pertinents du fichier susmentionné, 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
} 
0
Mark Thomson