Pourquoi «bash -x» rompt ce script?

13

J'ai un script qui mesure la durée d'exécution d'une commande.

Il a besoin de la "vraie" timecommande, ce qui signifie, un binaire par exemple dans /usr/bin/time(car le bash-built-in n'a pas le -fdrapeau).

Ci-dessous, un script simplifié qui peut être débogué:

#!/bin/bash

TIMESEC=$(echo blah | ( /usr/bin/time -f %e grep blah >/dev/null ) 2>&1 | awk -F. '{print $1}')

echo ABC--$TIMESEC--DEF

if [ "$TIMESEC" -eq 0 ] ; then
   echo "we are here!"
fi

Enregistrez sous "test.sh" et exécutez:

$ bash test.sh
ABC--0--DEF
we are here!

Donc ça a marché.

Maintenant, essayons de déboguer ceci en ajoutant "-x" à la ligne de commande bash:

$ bash -x test.sh
++ echo blah
++ awk -F. '{print $1}'
+ TIMESEC='++ /usr/bin/time -f %e grep blah
0'
+ echo ABC--++ /usr/bin/time -f %e grep blah 0--DEF
ABC--++ /usr/bin/time -f %e grep blah 0--DEF
+ '[' '++ /usr/bin/time -f %e grep blah
0' -eq 0 ']'
test.sh: line 10: [: ++ /usr/bin/time -f %e grep blah
0: integer expression expected

Pourquoi ce script se casse-t-il lorsque nous utilisons "-x" et fonctionne correctement sans lui?

Tomasz Chmielewski
la source
1
Il h. Il semble qu'avec -xon, la $()construction obtient la -xsortie incluse dans le cadre de sa valeur résultante. Je ne sais pas si c'est un comportement "attendu" ou un bogue ... Ou peut-être que c'est le sous-shell à l' ()intérieur qui donne la -xsortie.
Jeff Y
En plus: le réglage BASH_XTRACEFDvous permet de rediriger la set -xsortie vers un endroit où cela pose moins de problèmes.
Charles Duffy

Réponses:

21

Le problème est cette ligne:

TIMESEC=$(echo blah | ( /usr/bin/time -f %e grep blah >/dev/null ) 2>&1 | awk -F. '{print $1}')

où vous redirigez l'erreur standard pour qu'elle corresponde à la sortie standard. bash écrit ses messages de trace dans l'erreur standard, et utilise (par exemple) son intégré echoavec d'autres constructions de shell dans le processus bash.

Si vous le changez en quelque chose comme

TIMESEC=$(echo blah | sh -c "( /usr/bin/time -f %e grep blah >/dev/null )" 2>&1 | awk -F. '{print $1}')

il contournera ce problème et sera peut-être un compromis acceptable entre le traçage et le travail:

++ awk -F. '{print $1}'
++ sh -c '( /usr/bin/time -f %e grep blah >/dev/null )'
++ echo blah
+ TIMESEC=0                 
+ echo ABC--0--DEF
ABC--0--DEF
+ '[' 0 -eq 0 ']'
+ echo 'we are here!'
we are here!
Thomas Dickey
la source
7

vous pouvez également simplement déposer le sous-shell. ce sont apparemment les coquilles imbriquées qui finissent par se bouleverser:

TIMESEC=$(
    echo blah |
    /usr/bin/time -f %e grep blah 2>&1 >/dev/null |
    awk -F. '{print $1}'
)

Si tu fais:


...| ( subshell ) 2>pipe | ...

... vous vous retrouvez avec le sous-shell lancé pour gérer cette section du pipeline hébergeant le sous-shell à l'intérieur. Parce que le shell sans redirige même la sortie de débogage du sous-shell à l'intérieur (comme il le ferait également pour tout autre type de {commande composée que ; } >redirectvous pourriez choisir d'utiliser) vers sa section du pipeline, vous finissez par mélanger les flux. Cela a à voir avec l'ordre de redirection.

Au lieu de cela, si vous redirigez tout d'abord uniquement la sortie d'erreur des commandes que vous essayez d'évaluer, et laissez la sortie du shell hôte arriver à stderr, vous ne vous retrouvez pas avec le même problème.

et donc...


... | command 2>pipe 1>/dev/null | ...

... le shell hôte est libre de continuer à écrire son stderr où il veut tout en ne redirigeant que la sortie des commandes qu'il appelle dans le pipe.


bash -x time.sh
+++ echo blah
+++ /usr/bin/time -f %e grep blah
+++ awk -F. '{print $1}'
++ TIMESEC=0
++ echo ABC--0--DEF
ABC--0--DEF
++ '[' 0 -eq 0 ']'
++ echo 'we are here!'
we are here!

D'ailleurs...


TIMESEC=$(
    echo blah |
    /usr/bin/time -f %e grep blah 2>&1 >/dev/null
)
printf %s\\n "ABC--${TIMESEC%%.*}--DEF"
if [ "${TIMESEC%%.*}" -eq 0 ] ; then
   echo "we are here!"
fi
mikeserv
la source