Comportamento correto de armadilhas EXIT e ERR ao usar 'set -eu'

23

Estou observando um comportamento estranho ao usar set -e ( errexit ), set -u ( nounset ) junto com ERR e EXIT traps. Eles parecem relacionados, então colocá-los em uma pergunta parece razoável.

1) set -u não aciona as armadilhas ERR

  • Código:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
  • Esperado: ERR armadilha é chamada, RC! = 0
  • Real: a interceptação ERR é não chamada, RC == 1
  • Nota: set -e não altera o resultado

2) Usando set -eu o código de saída em uma interceptação EXIT é 0 em vez de 1

  • Código:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
    
  • Esperado: a interrupção EXIT é chamada, RC == 1
  • Real: EXIT trap é chamado, RC == 0
  • Nota: Ao usar set +e , o RC == 1. A interceptação EXIT retorna o RC apropriado quando qualquer outro comando gera um erro.
  • Editar: Há uma postagem SO sobre esse tópico com um comentário interessante sugerindo que isso possa estar relacionado à versão do Bash sendo usada. Testar este trecho com o Bash 4.3.11 resulta em um RC = 1, então é melhor. Infelizmente, atualizar o Bash (a partir de 3.2.51) em todos os hosts não é possível no momento, por isso temos que encontrar alguma outra solução.

Alguém pode explicar algum desses comportamentos?

A pesquisa nesses tópicos não foi muito bem-sucedida, o que é bastante surpreendente, considerando o número de postagens nas configurações e traps do Bash. Existe um tópico no fórum , embora, mas a conclusão é bastante insatisfatória.

    
por dvdgsng 07.06.2015 / 18:34

2 respostas

14

De man bash :

  • %código%
    • Trate as variáveis não configuradas e os parâmetros diferentes dos parâmetros especiais set -u e "@" como um erro ao executar a expansão de parâmetro. Se a expansão for tentada em uma variável ou parâmetro não definido, o shell imprimirá uma mensagem de erro e, se não for "*" nterativo, sairá com um status diferente de zero.

POSIX afirma que, no caso de um erro de expansão , um shell não interativo deve sair quando a expansão estiver associada a um shell especial embutido (que é uma distinção que -i regularmente ignora de qualquer maneira, e então talvez seja irrelevante) ou qualquer outro utilitário além disso.

  • Consequências dos erros da Shell :
    • Um erro de expansão é aquele que ocorre quando as expansões do shell são definidas em Expansões de palavras são realizadas (por exemplo, bash , porque "${x!y}" não é um operador válido) ; uma implementação pode tratá-los como erros de sintaxe se for capaz de detectá-los durante a tokenização, em vez de durante a expansão.
    • O shell interativo [A] n deve gravar uma mensagem de diagnóstico no erro padrão sem sair.

Também de ! :

  • %código%
    • Se um sigspec for ERR , o comando arg será executado sempre que um pipeline (que pode consistir em um único comando simples) , um lista, ou um comando composto retorna um status de saída diferente de zero, sujeito às seguintes condições:
      • A armadilha ERR não é executada se o comando com falha fizer parte da lista de comandos imediatamente após uma palavra-chave man bash ou trap ... ERR ...
      • ... parte do teste em uma instrução while ...
      • ... parte de um comando executado em uma lista until ou if , exceto o comando após o% final&& ou || ...
      • ... qualquer comando em um pipeline, mas o último ...
      • ... ou se o valor de retorno do comando estiver sendo invertido usando && .
    • Estas são as mesmas condições obedecidas pela opção errexit || .

Note acima que a armadilha ERR é toda sobre a avaliação de algum retorno do comando outro . Mas quando ocorre um erro de expansão , não é executado nenhum comando para retornar nada. No seu exemplo, ! nunca acontece - porque enquanto o shell avalia e expande seus argumentos, ele encontra uma variável -e nset, que foi especificada pela opção shell explícita para causar uma saída imediata do shell atual com script.

E então a armadilha EXIT , se houver, é executada, e o shell sai com uma mensagem de diagnóstico e um status de saída diferente de 0 - exatamente como deveria.

Quanto à coisa rc: 0 , espero que seja algum tipo de bug específico da versão - provavelmente relacionado aos dois gatilhos para o EXIT ocorrendo no mesmo tempo e aquele recebendo o código de saída do outro (o que não deveria ocorrer) . E de qualquer maneira, com um binário echo atualizado como instalado por -u :

bash <<\IN
    printf "shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

Eu adicionei a primeira linha para que você possa ver que as condições do shell são as de um shell com script - ele não é interativo. A saída é:

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

Aqui estão algumas notas relevantes de registros de alterações recentes :

  • Corrigido um erro que fazia com que os comandos assíncronos não definissem bash corretamente.
  • Corrigido um erro que causava mensagens de erro geradas por erros de expansão no pacman comandos para ter o número da linha errado.
  • Corrigido um erro que fazia com que SIGINT e SIGQUIT não fossem $? pable em comandos de subshell assíncrono.
  • Corrigido um problema com o tratamento de interrupções que fazia com que um segundo e um SIGINT subseqüente fosse ignorado por shells interativos.
  • O shell não bloqueia mais o recebimento de sinais durante a execução de for manipuladores para esses sinais e permite que mais trap handlers sejam executados recursivamente (executando trap handlers enquanto um manipulador trap está sendo executado) .

Eu acho que é o último ou o primeiro que é mais relevante - ou possivelmente uma combinação dos dois. Um trap handler é por sua própria natureza assíncrono porque todo seu trabalho é esperar e manipular sinais assíncronos . E você aciona dois simultaneamente com trap e trap .

E talvez você deva apenas atualizar, mas se você gosta de si mesmo, você fará isso com um shell diferente.

    
por 14.06.2015 / 05:50
6

(Estou usando o bash 4.2.53). Para a parte 1, a página man bash diz apenas "Uma mensagem de erro será gravada no erro padrão, e um shell não interativo sairá". Não diz que uma armadilha ERR será chamada, embora eu concorde que seria útil se o fizesse.

Para ser pragmático, se o que você realmente quer é lidar de forma mais limpa com variáveis indefinidas, uma possível solução é colocar a maior parte do seu código dentro de uma função, então executar essa função em um sub-shell e recuperar o código de retorno saída stderr. Aqui está um exemplo em que "cmd ()" é a função:

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

Na minha festa eu recebo

./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
    
por 13.06.2015 / 21:44