Por que “bash -x” quebra esse script?

13

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

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

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 quebra quando estamos usando "-x" e funciona bem sem ele?

    
por Tomasz Chmielewski 22.12.2015 / 12:35

2 respostas

20

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á escrevendo suas mensagens de rastreio para o erro padrão e está (por exemplo) usando seu echo integrado junto com outras construções de shell no processo 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}')

funcionará em torno desse problema e talvez seja um compromisso aceitável entre o rastreamento e o 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!
    
por 22.12.2015 / 13:14
7

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

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

Se você fizer isso:

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

... você acaba com o subshell lançado para lidar com essa seção do pipeline hospedando o subshell dentro. Porque o shell sem redireciona até mesmo a saída de depuração do subshell dentro de (como também faria para qualquer outro tipo de { composto comando ; } >redirect você pode escolher usar) para sua seção do gasoduto você acaba misturando fluxos. Tem a ver com a ordem de redirecionamento.

Em vez disso, se você simplesmente redirecionar primeiro somente a saída de erro dos comandos que está tentando medir, e deixar a saída do shell do host torná-la stderr, você não terminará com o mesmo problema.

e assim ...

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

... o shell do host está livre para continuar gravando seu stderr onde ele quiser, enquanto apenas redireciona a saída dos comandos que ele chama para o 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!

Para esse assunto ...

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
    
por 22.12.2015 / 14:09