web-dev-qa-db-fra.com

Comportement correct de la sortie et des pièges ERR lorsque vous utilisez `set -eu`

J'observe un comportement étrange lorsque vous utilisez set -e (errexit), set -u (nounset) ainsi que des pièges à la sortie. Ils semblent liés, alors les mettre dans une seule question semble raisonnable.

1) set -u ne déclenche pas les pièges à mépriser

  • Code:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
  • Attendu: ERR Piège est appelé, rc! = 0
  • ERRE ERR TRAP est non appelé, rc == 1
  • Noter: set -e ne change pas le résultat

2) en utilisant set -eu Le code de sortie dans un piège de sortie est 0 au lieu de 1

  • Code:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
    
  • Attendu: le piège de sortie est appelé, RC == 1
  • Actuelle: EXITER TRAP est appelé, RC == 0
  • Remarque: lorsque vous utilisez set +e, le RC == 1. Le piège de sortie renvoie le bon RC lorsque toute autre commande jette une erreur.
  • Edition: Il y a un SO POST sur ce sujet avec un Commentaire intéressant suggérant que Cela pourrait être lié à la version Bash utilisée. Test de cet extrait avec Bash 4.3.11 Résultats dans une RC = 1, c'est mieux. Malheureusement, la mise à niveau de Bash (à partir de 3.2.51) sur tous les hôtes n'est pas possible pour le moment. Nous devons proposer une autre solution.

Quelqu'un peut-il expliquer l'un de ces comportements?

La recherche de ces sujets n'a pas été très réussie, ce qui est plutôt surprenant compte tenu du nombre de postes sur les paramètres de bash et les pièges. Il y a n fil de forum , mais la conclusion est plutôt insatisfaisante.

29
dvdgsng

De man bash:

  • set -u
    • Traiter les variables non définies et les paramètres autres que les paramètres spéciaux "@" et "*" comme une erreur lors de l'expansion des paramètres. Si l'expansion est tentée sur une variable ou un paramètre non défini, le shell imprime un message d'erreur et, sinon -iTrontactive, sort avec un statut non nul.

POSIX indique que, dans le cas d'une erreur erreur d'expansion, une coque non interactive doit quitter Lorsque l'expansion est associée à une coque spéciale intégrée (qui est une distinction bash ignore régulièrement de toute façon, et peut-être peut-être non négligeant) ou tout autre utilitaire en plus.

  • conséquences des erreurs de shell :
    • An Erreur d'expansion est une partie qui se produit lorsque les extensions de coquille définies sont définies dans expansions de mots sont effectuées (par exemple, "${x!y}", car ! n'est pas un opérateur valide); Une implémentation Peut-être Traitez-les comme des erreurs de syntaxe s'il est capable de les détecter pendant la jonction, plutôt que pendant l'expansion.
    • [A] n shell interactif doit écrire un message de diagnostic à une erreur standard sans sortir.

Aussi de man bash:

  • trap ... ERR
    • Si un SIGSPEC est [~ # ~ ~] ERR [~ # ~ ~], la commande arg est exécuté chaque fois qu'un pipeline qui peut consister d'une seule commande simple), une liste ou une commande composée renvoie un statut de sortie non nul, sous réserve des conditions suivantes: [.____]
      • Le [~ # ~] ERR [~ # ~]] Piège n'est pas exécuté si la commande échouée fait partie de la liste de commande immédiatement après un while ou until mot-clé...
      • ... une partie du test dans une déclaration if ...
      • ... une partie d'une commande exécutée dans un && ou || liste sauf la commande suivant la finale && ou ||...
      • ... n'importe quelle commande dans un pipeline mais le dernier ...
      • ... ou si la valeur de retour de la commande est inversée à l'aide de !.
    • Ce sont les mêmes conditions obéites par le errexit-e option.

Notez ci-dessus que le [~ # ~ ~] ERR [~ # ~] Piège est tout sur l'évaluation de certains Autre Retour de la commande. Mais quand Erreur d'expansion On se produit, il n'y a pas de commande à retourner quoi que ce soit. Dans votre exemple, echo N'arrive jamais - parce que pendant que la coquille évalue et élargit ses arguments, il rencontre un -uSet variable, qui a été spécifié par option de shell explicite pour provoquer une sortie immédiate de la coque actuelle et scriptée.

Et donc le [~ # ~] sortie [~ # ~] Piège, le cas échéant, est exécuté et que la coque sort avec un message de diagnostic et un statut de sortie autre que 0 - exactement comme il le devrait faire.

Quant au RC: 0 chose, je m'attends à ce qu'une version est une version spécifique de quelque sorte - probablement avec les deux déclencheurs pour le [~ # ~] # ~] se produisant en même temps et celui obtenant le code de sortie de l'autre (qui ne devrait pas se produire). Et de toute façon, avec un binaire à jour bash comme installé par pacman:

bash <<\IN
    printf "Shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

J'ai ajouté la première ligne afin que vous puissiez voir que les conditions de la coque sont celles d'une coquille scriptée - c'est pas Interactive. La sortie est:

Shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)

Voici quelques notes pertinentes de Changelogs récents :

  • Correction d'un bug qui a provoqué des commandes asynchrones pour ne pas définir $? correctement.
  • Correction d'un bug qui a provoqué des messages d'erreur générés par erreurs d'expansion Dans for commandes pour avoir le mauvais numéro de ligne.
  • Correction d'un bug qui a causé [~ # ~] sigint [~ # ~ ~] et [~ # ~ # ~] sigquit [~ # ~] Pour ne pas être trap PABABLE DANS LES COMMANDES SUBSHELS ASYNCHRONES.
  • Correction d'un problème avec une interruption d'interruption qui a provoqué une seconde et suivante [~ # ~ ~ ~] SIGINT [~ # ~] Pour être ignoré par des coquilles interactives.
  • La coquille ne bloque plus la réception de signaux lors de l'exécution trap des gestionnaires pour ces signaux et permet la pluparttrap gestionnaires à exécuter récursivement (en cours d'exécution trap Les gestionnaires tandis qu'un gestionnaire trap est en cours d'exécution).

Je pense que c'est le dernier ou le premier qui est le plus pertinent - ou éventuellement une combinaison des deux. A trap Handler est par sa nature asynchrone car tout son travail consiste à attendre et à gérer signaux asynchrones. Et vous déclenchez deux simultanément avec -eu et $UNSET_VAR.

Et alors peut-être que vous devriez simplement mettre à jour, mais si vous aimez vous-même, vous le ferez avec une coque différente.

16
mikeserv

(J'utilise Bash 4.2.53). Pour la partie 1, la page Bash Man dit simplement "Un message d'erreur sera écrit à l'erreur standard et une coque non interactive va sortir". Cela ne dit pas un piège à ERR sera appelé, même si je suis d'accord, il serait utile s'il le faisait.

Pour être pragmatique, si ce que vous voulez vraiment, c'est de faire facener plus proprement avec des variables non définies, une solution possible consiste à placer la majeure partie de votre code dans une fonction, puis à exécuter cette fonction dans une sous-coque et à récupérer le code de retour et la sortie STDERR. Voici un exemple où "cmd ()" est la fonction:

#!/bin/bash
trap 'rc=$?; echo "ERR at line ${LINENO} (rc: $rc)"; exit $rc' ERR
trap 'rc=$?; echo "EXIT (rc: $rc)"; exit $rc' EXIT
set -u
set -E # export trap to functions

cmd(){
 echo "args=$*"
 echo ${UNSET_VAR}
 echo hello
}
oops(){
 rc=$?
 echo "$@"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "$@" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

Sur mon bash je reçois

./script my stuff; echo "exit was $?"
args=my stuff
fail: ./script: line 9: UNSET_VAR: unbound variable
ERR at line 15 (rc: 1)
EXIT (rc: 1)
exit was 1
9
meuh