Intercepter les erreurs dans la substitution de commandes en utilisant «-o errtrace» (c'est-à-dire définir -E)

14

Selon ce manuel de référence :

-E (également -o errtrace)

S'il est défini, tout piège sur ERR est hérité par les fonctions shell, les substitutions de commandes et les commandes exécutées dans un environnement de sous-shell. Le piège ERR n'est normalement pas hérité dans de tels cas.

Cependant, je dois l'interpréter incorrectement, car ce qui suit ne fonctionne pas:

#!/usr/bin/env bash
# -*- bash -*-

set -e -o pipefail -o errtrace -o functrace

function boom {
  echo "err status: $?"
  exit $?
}
trap boom ERR


echo $( made up name )
echo "  ! should not be reached ! "

Je connais déjà une affectation simple ,, my_var=$(made_up_name)quittera le script avec set -e(ie errexit).

Est -E/-o errtracecensé fonctionner comme le code ci-dessus? Ou, très probablement, je l'ai mal lu?

dgo.a
la source
2
C'est une bonne question. Le remplacement echo $( made up name )par $( made up name )produit le comportement souhaité. Je n'ai pas d'explication cependant.
iruvar
Je ne connais pas -E de bash mais je sais que -e n'effectue une sortie de shell que si l'erreur résulte de la dernière commande dans un pipeline. Ainsi , vos var=$( pipe )et $( pipe )exemples seraient représentent tous deux points d' extrémité du tuyau alors pipe > echone serait pas. Ma page de manuel dit: "1. L'échec d'une commande individuelle dans un pipeline multi-commandes ne doit pas provoquer la fermeture du shell. Seule l'échec du pipeline lui-même doit être pris en compte."
mikeserv
Vous pouvez le faire échouer cependant: echo $ ($ {madeupname?}). Mais c'est réglé -e. Encore une fois, -E est en dehors de ma propre expérience.
mikeserv
@mikeserv @ 1_CR Le manuel bash @ echo indique que echorenvoie toujours 0. Cela doit être pris en compte dans l'analyse ...

Réponses:

5

Remarque: zshse plaindra de "mauvais modèles" si vous ne le configurez pas pour accepter les "commentaires en ligne" pour la plupart des exemples ici et ne les exécutez pas via un shell proxy comme je l'ai fait avec sh <<-\CMD.

Ok, donc, comme je l'ai dit dans les commentaires ci-dessus, je ne connais pas spécifiquement les bashset -E , mais je sais que les shells compatibles POSIX fournissent un moyen simple de tester une valeur si vous le souhaitez:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
        echo "echo still works" 
    }
    _test && echo "_test doesnt fail"
    # END
    CMD
sh: line 1: empty: error string
+ echo

+ echo 'echo still works'
echo still works
+ echo '_test doesnt fail'
_test doesnt fail

Au- dessus , vous verrez que si je parameter expansiontester ${empty?} _test()encore returnest un passe - comme il est témoigna dans le dernier echoCela se produit parce que la valeur a échoué tue le $( command substitution )sous - shell qui le contient, mais sa coquille mère - _testà ce moment - garde sur le camionnage. Etecho peu importe - il est très heureux de servir seulement un test \newline; echon'est pas .

Mais considérez ceci:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    _test ||\
            echo "this doesnt even print"
    # END
    CMD
_test+ sh: line 1: empty: function doesnt run

Parce que j'ai alimenté l' _test()'sentrée avec un paramètre pré-évalué dans le INIT here-documentmaintenant, la _test()fonction n'essaie même pas de fonctionner du tout. De plus, le shshell abandonne apparemment le fantôme et echo "this doesnt even print" ne s'imprime même pas.

Ce n'est probablement pas ce que vous voulez.

Cela se produit car l' ${var?}expansion des paramètres de style est conçue pour quitter leshell en cas de paramètre manquant, cela fonctionne comme ceci :

${parameter:?[word]}

Indique une erreur si Nullou Unset.si le paramètre est non défini ou nul, le expansion of word(ou un message indiquant qu'il est non défini si le mot est omis) doit être written to standard erroret le shell exits with a non-zero exit status. Sinon, la valeur deparameter shall be substituted . Un shell interactif n'a pas besoin de quitter.

Je ne vais pas copier / coller le document entier, mais si vous voulez un échec pour un set but null valeur, vous utilisez le formulaire:

${var :? error message }

Avec le :colon comme ci-dessus. Si vous voulez qu'une nullvaleur réussisse, omettez simplement les deux points. Vous pouvez également l'annuler et échouer uniquement pour les valeurs définies, comme je le montrerai dans un instant.

Une autre série de _test():

    sh <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    echo "this runs" |\
        ( _test ; echo "this doesnt" ) ||\
            echo "now it prints"
    # END
    CMD
this runs
sh: line 1: empty: function doesnt run
now it prints

Cela fonctionne avec toutes sortes de tests rapides, mais ci-dessus, vous verrez que _test(), exécuté à partir du milieu des pipelineéchecs, et en fait son command listsous-shell contenant échoue complètement, car aucune des commandes de la fonction ne s'exécute ni la suivante echo, bien qu'il soit également démontré qu'il peut facilement être testé car echo "now it prints" il s'imprime maintenant.

Le diable est dans les détails, je suppose. Dans le cas ci-dessus, le shell qui sort n'est pas celui du script _main | logic | pipelinemais le( subshell in which we ${test?} ) || un petit bac à sable est donc nécessaire.

Et ce n'est peut-être pas évident, mais si vous vouliez passer uniquement pour le cas contraire, ou uniquement pour les set=valeurs, c'est aussi assez simple:

    sh <<-\CMD
    N= #N is NULL
    _test=$N #_test is also NULL and
    v="something you would rather do without"    
    ( #this subshell dies
        echo "v is ${v+set}: and its value is ${v:+not NULL}"
        echo "So this ${_test:-"\$_test:="} will equal ${_test:="$v"}"
        ${_test:+${N:?so you test for it with a little nesting}}
        echo "sure wish we could do some other things"
    )
    ( #this subshell does some other things 
        unset v #to ensure it is definitely unset
        echo "But here v is ${v-unset}: ${v:+you certainly wont see this}"
        echo "So this ${_test:-"\$_test:="} will equal NULL ${_test:="$v"}"
        ${_test:+${N:?is never substituted}}
        echo "so now we can do some other things" 
    )
    #and even though we set _test and unset v in the subshell
    echo "_test is still ${_test:-"NULL"} and ${v:+"v is still $v"}"
    # END
    CMD
v is set: and its value is not NULL
So this $_test:= will equal something you would rather do without
sh: line 7: N: so you test for it with a little nesting
But here v is unset:
So this $_test:= will equal NULL
so now we can do some other things
_test is still NULL and v is still something you would rather do without

L'exemple ci-dessus tire parti des 4 formes de substitution de paramètres POSIX et de leurs diverses :colon nullounot null des tests. Il y a plus d'informations dans le lien ci-dessus, et le voici à nouveau .

Et je suppose que nous devrions aussi montrer notre _testtravail fonctionnel, non? Nous déclarons simplementempty=something comme paramètre de notre fonction (ou à tout moment à l'avance):

    sh <<-\CMD
    _test() { echo $( echo ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?tested as a pass before function runs}
    INIT
    echo "this runs" >&2 |\
        ( empty=not_empty _test ; echo "yay! I print now!" ) ||\
            echo "suspiciously quiet"
    # END
    CMD
this runs
not_empty
echo still works
yay! I print now!

Il convient de noter que cette évaluation est autonome - elle ne nécessite aucun test supplémentaire pour échouer. Quelques exemples supplémentaires:

    sh <<-\CMD
    empty= 
    ${empty?null, no colon, no failure}
    unset empty
    echo "${empty?this is stderr} this is not"
    # END
    CMD
sh: line 3: empty: this is stderr

    sh <<-\CMD
    _input_fn() { set -- "$@" #redundant
            echo ${*?WHERES MY DATA?}
            #echo is not necessary though
            shift #sure hope we have more than $1 parameter
            : ${*?WHERES MY DATA?} #: do nothing, gracefully
    }
    _input_fn heres some stuff
    _input_fn one #here
    # shell dies - third try doesnt run
    _input_fn you there?
    # END
    CMD
heres some stuff
one
sh: line :5 *: WHERES MY DATA?

Et enfin, nous revenons à la question initiale: comment gérer les erreurs dans un $(command substitution)sous-shell? La vérité est - il y a deux façons, mais aucune n'est directe. Le cœur du problème est le processus d'évaluation du shell - les extensions du shell (y compris$(command substitution) ) se produisent plus tôt dans le processus d'évaluation du shell que l'exécution actuelle des commandes du shell - c'est à ce moment que vos erreurs peuvent être détectées et piégées.

Le problème rencontré par l'op est qu'au moment où le shell actuel évalue les erreurs, le $(command substitution) sous shell a déjà été remplacé - aucune erreur ne subsiste.

Alors, quelles sont les deux façons? Soit vous le faites explicitement dans le$(command substitution) sous shell avec des tests comme vous le feriez sans, soit vous absorbez ses résultats dans une variable shell actuelle et testez sa valeur.

Méthode 1:

    echo "$(madeup && echo \: || echo '${fail:?die}')" |\
          . /dev/stdin

sh: command not found: madeup
/dev/stdin:1: fail: die

    echo $?

126

Méthode 2:

    var="$(madeup)" ; echo "${var:?die} still not stderr"

sh: command not found: madeup
sh: var: die

    echo $?

1

Cela échouera quel que soit le nombre de variables déclarées par ligne:

   v1="$(madeup)" v2="$(ls)" ; echo "${v1:?}" "${v2:?}"

sh: command not found: madeup
sh: v1: parameter not set

Et notre valeur de retour reste constante:

    echo $?
1

MAINTENANT LE PIÈGE:

    trap 'printf %s\\n trap resurrects shell!' ERR
    v1="$(madeup)" v2="$(printf %s\\n shown after trap)"
    echo "${v1:?#1 - still stderr}" "${v2:?invisible}"

sh: command not found: madeup
sh: v1: #1 - still stderr
trap
resurrects
shell!
shown
after
trap

    echo $?
0
mikeserv
la source
Par exemple, pratiquement sa spécification exacte: echo $ {v = $ (madeupname)}; echo "$ {v:? trap1st}! n'est pas atteint!"
mikeserv
1
Pour résumer: Ces extensions de paramètres sont un excellent moyen de contrôler si des variables sont définies, etc. En ce qui concerne l'état de sortie de l'écho, j'ai remarqué que echo "abc" "${v1:?}"cela ne semble pas s'exécuter (abc jamais imprimé). Et le shell retourne 1. Cela est vrai avec ou sans commande even ( "${v1:?}"directement sur le cli). Mais pour le script OP, tout ce qui était nécessaire pour déclencher le piège est de placer son affectation de variable contenant une substitution pour une commande inexistante seule sur une seule ligne. Sinon, le comportement de l'écho est de toujours retourner 0, à moins qu'il ne soit interrompu comme avec les tests que vous avez expliqué.
Justement v=$( madeup ). Je ne vois pas ce qui est dangereux avec ça. C'est juste une affectation, la personne a mal orthographié la commande par exemple v="$(lss)". Il fait des erreurs. Oui, vous pouvez vérifier avec le statut d'erreur de la dernière commande $? - parce que c'est la commande sur la ligne (une affectation sans nom de commande) et rien d'autre - pas un argument à faire écho. De plus, ici, il est piégé par la fonction comme! = 0, donc vous obtenez deux fois des commentaires. Sinon, sûrement, comme vous l'expliquez, il existe une meilleure façon de le faire de manière ordonnée dans un cadre, mais OP avait une seule ligne: un écho plus sa substitution échouée. Il s'interrogeait sur l'écho.
Oui, une simple affectation est mentionnée dans la question ainsi que pour acquis, et bien que je ne puisse pas offrir de conseils spécifiques sur la façon d'utiliser -E, j'ai essayé de montrer des résultats similaires au problème démontré étaient plus que possibles sans recourir à bashismes. Dans tous les cas, ce sont précisément les problèmes que vous mentionnez - comme l'affectation unique par ligne - qui rendent ces solutions difficiles à gérer dans un pipeline, que j'ai également montré comment gérer. Bien que ce que vous dites soit vrai - il n'y a rien de dangereux à simplement l'attribuer.
mikeserv
1
Bon point - peut-être que plus d'efforts sont nécessaires. Je vais y penser.
mikeserv
1

S'il est défini, tout piège sur ERR est hérité par les fonctions shell, les substitutions de commandes et les commandes exécutées dans un environnement de sous-shell

Dans votre script, c'est une exécution de commande ( echo $( made up name )). Dans bash, les commandes sont délimitées par l'un ou l'autre ; ou avec une nouvelle ligne . Dans la commande

echo $( made up name )

$( made up name )est considéré comme faisant partie de la commande. Même si cette partie échoue et revient avec une erreur, la commande entière s'exécute avec succès car echoje n'en sais rien. Lorsque la commande revient avec 0, aucune interruption n'est déclenchée.

Vous devez le mettre dans deux commandes, affectation et écho

var=$(made_up_name)
echo $var
totti
la source
echo $varn'échoue pas - $varest étendu pour se vider avant même de pouvoir echoy jeter un œil.
mikeserv
0

Cela est dû à un bogue dans bash. Pendant l'étape de substitution de commande

echo $( made up name )

mades'exécute (ou ne parvient pas à être trouvé) dans un sous-shell, mais le sous-shell est "optimisé" de telle sorte qu'il n'utilise pas certains pièges du shell parent. Cela a été corrigé dans version 4.4.5 :

Dans certaines circonstances, une commande simple est optimisée pour éliminer une fourchette, ce qui entraîne l'exécution d'une interruption EXIT.

Avec bash 4.4.5 ou supérieur, vous devriez voir la sortie suivante:

error.sh: line 13: made: command not found
err status: 127
  ! should not be reached !

Le gestionnaire de trap a été appelé comme prévu, puis le sous-shell se termine. (set -e provoque uniquement la sortie du sous-shell, pas le parent, de sorte que le message "ne doit pas être atteint" doit en fait être atteint.)

Une solution de contournement pour les anciennes versions consiste à forcer la création d'un sous-shell complet et non optimisé:

echo $( ( made up name ) )

Les espaces supplémentaires sont nécessaires pour distinguer de l'expansion arithmétique.

JigglyNaga
la source