desenv-web-rp.com

Comportamento correto das armadilhas EXIT e ERR ao usar `set -eu`

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

1) set -u não aciona traps de ERR

  • Código:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
  • Esperado: a captura de erro é chamada, RC! = 0
  • Real: a interceptação de 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: EXIT trap é chamado, RC == 1
  • Real: A armadilha EXIT é chamada, RC == 0
  • Nota: Ao usar set +e, o RC == 1. A armadilha EXIT retorna o RC apropriado quando qualquer outro comando gera um erro.
  • Edit: Existe uma publicação de SO neste tópico com um comentário interessante sugerindo que isso pode estar relacionado à versão do Bash que está sendo usada. Testar esse snippet com o Bash 4.3.11 resulta em RC = 1, então é melhor. Infelizmente, não é possível atualizar o Bash (de 3.2.51) em todos os hosts no momento. temos que encontrar outra solução.

Alguém pode explicar um desses comportamentos?

A pesquisa nesses tópicos não teve muito êxito, o que é bastante surpreendente, dado o número de postagens nas configurações e armadilhas do Bash. Há m tópico no fórum , porém, mas a conclusão é bastante insatisfatória.

29
dvdgsng

De man bash:

  • set -u
    • Trate variáveis ​​e parâmetros não definidos que não sejam os parâmetros especiais "@" e "*" como um erro ao executar a expansão de parâmetros. Se tentar uma expansão em uma variável ou parâmetro não definido, o Shell imprimirá uma mensagem de erro e, se não -interactive, sai com um status diferente de zero.

O 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 builtin especial do Shell (que é uma distinção bash ignora regularmente de qualquer maneira, e talvez seja irrelevante) ou qualquer outro utilitário além disso.

  • Consequências dos erros do shell :
    • Um erro de expansão é aquele que ocorre quando as expansões do Shell definidas em Expansões do Word são realizadas (por exemplo, "${x!y}", Porque ! não é um operador válido); uma implementação pode trate-os como erros de sintaxe, se puder detectá-los durante a tokenização, e não durante a expansão.
    • [Um] Shell interativo deve escrever uma mensagem de diagnóstico para erro padrão sem sair.

Também de man bash:

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

Observe acima que a armadilha [~ # ~] err [~ # ~] trata da avaliação de alguns outros retorno do comando. Mas quando um erro de expansão ocorre, não há comando executado para retornar nada. No seu exemplo, echo nunca acontece - porque enquanto o Shell avalia e expande seus argumentos, encontra um -unset variável, que foi especificada pela opção explícita do Shell para causar uma saída imediata do atual shell com script.

E assim a armadilha [~ # ~] exit [~ # ~], se houver, é executada e o Shell sai com uma mensagem de diagnóstico e status de saída diferente de 0 - exatamente como deveria Faz.

Quanto à coisa rc: 0, espero que seja algum tipo de bug específico da versão - provavelmente relacionado aos dois gatilhos para a saída [~ # ~] [~ # ~] ocorrendo ao mesmo tempo e recebendo o código de saída do outro (o que não deve ocorrer). E de qualquer maneira, com um binário bash atualizado, instalado por pacman:

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 - é 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 changelogs recentes :

  • Corrigido um erro que fazia com que comandos assíncronos não configurassem $? corretamente.
  • Corrigido um bug que fazia com que as mensagens de erro geradas pelos erros de expansão nos comandos for tivessem o número da linha incorreta.
  • Corrigido um bug que fazia com que [~ # ~] sigint [~ # ~] e [~ # ~] sigquit [~ # ~] não fosse trappable em comandos de subshell assíncronos.
  • Corrigido um problema com o tratamento de interrupções que fazia com que um segundo e subsequente [~ # ~] sigint [~ # ~] fosse ignorado por shells interativos.
  • O Shell não bloqueia mais o recebimento de sinais enquanto executa trap manipuladores para esses sinais e permite que mosttrap manipuladores sejam executados recursivamente (executando trap manipuladores enquanto um trap manipulador está executando).

Eu acho que é o último ou o primeiro que é mais relevante - ou possivelmente uma combinação dos dois. A trap manipulador é da sua própria natureza assíncrono porque todo o seu trabalho é aguardar e manipular sinais assíncronos. E você aciona dois simultaneamente com -eu e $UNSET_VAR.

E então, talvez você deva apenas atualizar, mas se você gosta, você o fará com um Shell completamente diferente.

16
mikeserv

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

Para ser pragmático, se o que você realmente deseja é lidar de maneira mais limpa com variáveis ​​indefinidas, uma solução possível é colocar a maior parte do seu código dentro de uma função, depois executar essa função em um sub-Shell e recuperar o código de retorno e a 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 "[email protected]"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "[email protected]" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

Na minha festança 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
9
meuh