Comportement correct des interruptions EXIT et ERR lors de l'utilisation de `set -eu`

27

J'observe un comportement étrange lors de l'utilisation de set -e( errexit), set -u( nounset) avec les pièges ERR et EXIT. Ils semblent liés, donc les poser dans une question semble raisonnable.

1) set -une déclenche pas de pièges ERR

  • Code:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
  • Attendu: le piège ERR est appelé, RC! = 0
  • Réel: l'interruption ERR n'est pas appelée, RC == 1
  • Remarque: set -ene modifie pas le résultat

2) L'utilisation set -eudu code de sortie dans une interruption EXIT vaut 0 au lieu de 1

  • Code:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
  • Attendu: le piège EXIT est appelé, RC == 1
  • Réel: le trap EXIT est appelé, RC == 0
  • Remarque: Lors de l'utilisation set +e, le RC == 1. Le piège EXIT renvoie le RC approprié lorsqu'une autre commande génère une erreur.
  • Edit: Il y a un article SO sur ce sujet avec un commentaire intéressant suggérant que cela pourrait être lié à la version Bash utilisée. Le test de cet extrait avec Bash 4.3.11 donne un RC = 1, donc 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 donc trouver une autre solution.

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

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

dvdgsng
la source
3
À partir de 4 heures, je pense avoir bashrompu avec la norme et commencé à mettre des pièges en sous-coquilles. Le piège est censé être exécuté dans le même environnement d'où le retour, mais bashil ne l'a pas fait depuis un certain temps.
mikeserv
1
Attendez une minute - voulez-vous une solution ou une explication? Et si vous voulez une solution, alors une solution à quoi exactement? Que voulez-vous faire? set -eet set -usont tous deux conçus spécifiquement pour tuer un shell scripté. Les utiliser dans des conditions susceptibles de déclencher leur application tuera un shell scripté. Il n'y a pas moyen de contourner cela, sauf pour ne pas les utiliser, et plutôt pour tester ces conditions lorsqu'elles s'appliquent dans une séquence de code. Donc, fondamentalement, vous pouvez écrire un bon code shell, ou vous pouvez utiliser set -eu.
mikeserv
2
En fait, je recherche les deux, car je n'ai pas trouvé d'informations suffisantes sur la raison pour laquelle -une déclencherait pas le piège ERR (c'est une erreur, alors ne devrait-il pas déclencher le piège) ou le code d'erreur est 0 au lieu de 1. Le ce dernier semble être un bug qui a déjà été corrigé dans une version ultérieure, c'est tout. Mais la première partie est assez difficile à comprendre si vous n'avez pas réalisé que les erreurs dans l'évaluation du shell (expansion des paramètres) et les erreurs réelles dans les commandes semblent être deux choses différentes. Pour la solution, eh bien, comme vous l'avez suggéré, j'essaie maintenant d'éviter -euet de vérifier manuellement quand c'est nécessaire.
dvdgsng
1
@dvdsng - Bien. C'est la voie à suivre - vous devez publier votre script lorsque vous le faites comme réponse et vous attribuer la prime. Je n'aime vraiment pas ces options - elles ne permettent pas la gestion des exceptions de manière sûre.
mikeserv
1
@dvdsng - où l'une de ces options peut être utile, cependant, se trouve dans un contexte sous-shell. Et il est donc concevable que tout ce que vous utilisiez auparavant puisse être localisé dans un contexte de sous-shell comme: (set -u; : $UNSET_VAR)et similaire. Ce genre de trucs peut aussi être bon - vous pouvez en déposer beaucoup de &&temps en temps: (set -e; mkdir dir; cd dir; touch dirfile)si vous obtenez ma dérive. C'est juste que ce sont des contextes contrôlés - lorsque vous les définissez comme des options globales, vous perdez le contrôle et devenez contrôlé. Cependant, il existe généralement des solutions plus efficaces.
mikeserv

Réponses:

15

De man bash:

  • set -u
    • Traitez les variables et paramètres non définis autres que les paramètres spéciaux "@"et "*"comme une erreur lors de l'expansion des paramètres. Si une expansion est tentée sur une variable ou un paramètre non -idéfini, le shell affiche un message d'erreur et, s'il n'est pas interactif, se termine avec un état différent de zéro.

POSIX indique que, en cas d' erreur d'expansion , un shell non interactif doit quitter lorsque l'expansion est associée soit à un shell spécial intégré (ce qui est une distinction bashrégulièrement ignoré de toute façon, et donc peut-être pas pertinent) ou à tout autre utilitaire en plus .

  • Conséquences des erreurs shell :
    • Une erreur d'extension est une erreur qui se produit lorsque les extensions shell définies dans Word Expansions sont effectuées (par exemple "${x!y}", car !n'est pas un opérateur valide) ; une implémentation peut les traiter comme des erreurs de syntaxe si elle est capable de les détecter lors de la tokenisation, plutôt que lors de l'expansion.
    • [A] n shell interactif doit écrire un message de diagnostic à l'erreur standard sans quitter.

Aussi de man bash:

  • trap ... ERR
    • Si une sigspec est ERR , la commande arg est exécutée chaque fois qu'un pipeline (qui peut consister en une seule commande simple) , une liste ou une commande composée renvoie un état de sortie non nul, sous réserve des conditions suivantes:
      • L' interruption ERR n'est pas exécutée si la commande ayant échoué fait partie de la liste de commandes suivant immédiatement un mot clé whileor until...
      • ... une partie du test dans une ifdéclaration ...
      • ... partie d'une commande exécutée dans une liste &&ou ||à l'exception de la commande suivant la finale &&ou ||...
      • ... n'importe quelle commande dans un pipeline, mais la dernière ...
      • ... ou si la valeur de retour de la commande est inversée à l'aide de !.
    • Ce sont les mêmes conditions auxquelles obéit l' option errexit -e .

Notez ci-dessus que le piège ERR concerne l'évaluation du retour d'une autre commande. Mais lorsqu'une erreur d'extension se produit, aucune commande n'est exécutée pour renvoyer quoi que ce soit. Dans votre exemple, cela echo ne se produit jamais - car pendant que le shell évalue et développe ses arguments, il rencontre une -uvariable nset, qui a été spécifiée par l'option de shell explicite pour provoquer une sortie immédiate du shell scripté actuel.

Ainsi, le piège EXIT , le cas échéant, est exécuté, et le shell se termine avec un message de diagnostic et un état de sortie autre que 0 - exactement comme il devrait le faire.

Quant au rc: 0 chose, je pense que c'est un bug spécifique version de quelque sorte - sans doute à voir avec les deux éléments déclencheurs de la EXIT se produisant en même temps et celui d' obtenir le code de sortie de l'autre (qui ne devrait pas se produire) . Et de toute façon, avec un bashbinaire à jour 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 pour que vous puissiez voir que les conditions du shell sont celles d'un shell scripté - ce n'est pas interactif. La sortie est:

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

Voici quelques notes pertinentes des changelogs récents :

  • Correction d'un bug qui empêchait les commandes asynchrones de se régler $?correctement.
  • Correction d'un bug qui faisait que les messages d'erreur générés par des erreurs d'extension dans les forcommandes avaient le mauvais numéro de ligne.
  • Correction d'un bug qui empêchait SIGINT et SIGQUIT d'être trappable dans les commandes de sous-shell asynchrones.
  • Correction d'un problème avec la gestion des interruptions qui faisait en sorte qu'un deuxième SIGINT et les suivants étaient ignorés par les shells interactifs.
  • Le shell ne bloque plus la réception de signaux lors de l'exécution de trapgestionnaires pour ces signaux et permet à la plupart des trap gestionnaires d'être exécutés de manière récursive (exécution de trapgestionnaires pendant l' trapexécution d' un gestionnaire) .

Je pense que c'est le dernier ou le premier qui est le plus pertinent - ou peut-être une combinaison des deux. Un trapgestionnaire est par nature asynchrone car son travail consiste à attendre et à gérer les signaux asynchrones . Et vous déclenchez deux simultanément avec -euet $UNSET_VAR.

Et peut-être que vous devriez simplement mettre à jour, mais si vous vous aimez, vous le ferez avec un shell différent.

mikeserv
la source
Merci pour l'explication de la façon dont l'expansion des paramètres est gérée différemment. Cela m'a éclairé beaucoup de choses.
dvdgsng
Je vous accorde la prime parce que votre explication a été très utile.
dvdgsng
@dvdgsng - Gracias. Par curiosité, avez-vous déjà trouvé votre solution?
mikeserv
9

(J'utilise bash 4.2.53). Pour la partie 1, la page de manuel bash indique simplement "Un message d'erreur sera écrit dans l'erreur standard et un shell non interactif se fermera". Cela ne dit pas qu'un piège ERR sera appelé, bien que je convienne que ce serait utile s'il le faisait.

Pour être pragmatique, si ce que vous voulez vraiment, c'est gérer plus proprement les variables non définies, une solution possible est de mettre la plupart de votre code à l'intérieur d'une fonction, puis d'exécuter cette fonction dans un sous-shell et de récupérer le code 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
meuh
la source
sympa, une solution pratique qui ajoute de la valeur!
Florian Heigl