Por que "bash -x" quebra esse script?

13

Eu tenho um script que mede quanto tempo algum comando é executado.

Ele precisa do timecomando "real" , ou seja, um binário, por exemplo, em /usr/bin/time(como o bash-built-in não possui o -fsinalizador).

Abaixo, um script simplificado que pode ser depurado:

#!/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

Salve como "test.sh" e execute:

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

Então funcionou.

Agora, vamos tentar depurar isso adicionando "-x" à linha de comando do 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

Por que esse script é interrompido quando estamos usando "-x" e funciona bem sem ele?

Tomasz Chmielewski
fonte
1
Heh. Parece que com -xon, a $()construção está -xincluindo a saída como parte do valor resultante. Não sei se esse é o comportamento "esperado" ou um bug ... Ou talvez seja o subshell ()dentro que está realmente fornecendo a -xsaída.
Jeff Y
Lado: A configuração BASH_XTRACEFDpermite redirecionar a set -xsaída para um local com menos problemas.
Charles Duffy

Respostas:

21

O problema é esta linha:

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

onde você está redirecionando o erro padrão para corresponder à saída padrão. O bash está gravando suas mensagens de rastreio no erro padrão e está (por exemplo) usando seus recursos internos echojunto com outras construções de shell, tudo no processo do bash.

Se você mudar para algo como

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

contornará esse problema e talvez seja um compromisso aceitável entre rastreamento e trabalho:

++ 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
fonte
7

você também pode simplesmente soltar o subshell. aparentemente, são as conchas aninhadas que acabam perturbando uma à outra:

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

Se você fizer:


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

... você termina com o subshell lançado para lidar com a seção do pipeline que hospeda o subshell dentro. Como o shell sem redireciona até a saída de depuração do subshell (como também faria com qualquer outro tipo de {comando composto que ; } >redirectvocê possa optar por usar) para sua seção do pipeline, você acaba misturando fluxos. Tem a ver com a ordem do redirecionamento.

Em vez disso, se você simplesmente redirecionar apenas a saída de erro dos comandos que está tentando avaliar, e deixar que a saída do shell do host atinja o stderr, você não acabará com o mesmo problema.

e entao...


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

... o shell do host é livre para continuar escrevendo seu stderr onde quiser, enquanto redireciona apenas a saída dos comandos que chama no canal.


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!

Por falar nisso ...


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
fonte