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

27

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

1) set -unão aciona interceptações de ERR

  • Código:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
  • Esperado: o trap de erro é chamado, RC! = 0
  • Real: o trap de ERR não é chamado, RC == 1
  • Nota: set -enão altera o resultado

2) O uso set -eudo 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. O trap EXIT retorna o RC apropriado quando qualquer outro comando gera um erro.
  • Edit: Existe uma publicação do SO sobre este 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, a atualização do Bash (de 3.2.51) em todos os hosts não é possível no momento, portanto, temos que apresentar 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. Porém, existe um tópico no fórum , mas a conclusão é bastante insatisfatória.

dvdgsng
fonte
3
A partir de 4, acho que bashrompeu com o padrão e comecei a colocar armadilhas nas subcascas. A armadilha deve ser executada no mesmo ambiente de onde veio o retorno, mas bashnão faz isso há um bom tempo.
mikeserv
11
Espere um minuto - você quer uma solução ou uma explicação? E se você quer uma solução, então uma solução para o que exatamente? O que você quer que ocorra? set -ee set -usão projetados especificamente para matar um shell com script. Usá-los em condições que podem acionar seu aplicativo matará um shell com script. Não há como contornar isso, exceto para não usá-los e, em vez disso, testar essas condições quando elas se aplicam em uma sequência de código. Então, basicamente, você pode escrever um bom código de shell ou usar set -eu.
mikeserv
2
Na verdade, estou procurando por ambos, pois não consegui encontrar informações suficientes sobre por -uque não acionaria a interceptação de ERR (é um erro, portanto, não deveria acionar a interceptação) ou o código de erro é 0 em vez de 1. último parece ser um bug que já foi corrigido na versão posterior, então é isso. Mas a primeira parte é bastante difícil de entender se você não percebeu que erros na avaliação do shell (expansão de parâmetros) e erros reais nos comandos parecem ser duas coisas diferentes. Para a solução, bem, como você sugeriu, agora estou tentando evitar -eue verificar manualmente quando for necessário.
precisa saber é o seguinte
11
@dvdsng - Bom. Esse é o caminho a seguir - você deve postar seu script quando responder como resposta e conceder a si mesmo a recompensa. Eu realmente não gosto dessas opções - elas não permitem o tratamento de exceções de maneira segura.
mikeserv
11
@dvdsng - onde qualquer uma dessas opções pode ser útil, porém, está em um contexto com subcasca. E, portanto, é concebível que, seja lá o que você os usasse antes, pudesse ser localizado em um contexto de subcama como: (set -u; : $UNSET_VAR)e similar. Esse tipo de coisa também pode ser bom - você pode largar muitas de &&vez em (set -e; mkdir dir; cd dir; touch dirfile)quando:, se você entender minha tendência. Só que esses são contextos controlados - quando você os define como opções globais, você perde o controle e se torna controlado. Geralmente, existem soluções mais eficientes.
mikeserv

Respostas:

15

De man bash:

  • set -u
    • Trate variáveis ​​não definidas e parâmetros diferentes dos 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 -idefinido, o shell imprimirá uma mensagem de erro e, se não for inativo, sairá com um status diferente de zero.

O POSIX declara 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 que bashignora regularmente de qualquer maneira e talvez seja irrelevante) ou qualquer outro utilitário além .

  • Consequências dos erros do shell :
    • Um erro de expansão é aquele que ocorre quando as expansões de shell definidas nas expansões do Word são executadas (por exemplo "${x!y}", porque !não é um operador válido) ; uma implementação pode tratá-los como erros de sintaxe se puder detectá-los durante a tokenização, e não durante a expansão.
    • [A] um shell interativo deve escrever uma mensagem de diagnóstico no 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 um único comando simples) , uma lista ou um comando composto retornar um status de saída diferente de zero, sujeito às seguintes condições:
      • A interceptação de ERR não será executada se o comando com falha fizer parte da lista de comandos imediatamente após uma palavra-chave whileou until...
      • ... parte do teste em uma ifdeclaração ...
      • ... parte de um comando executado em uma lista &&ou, ||exceto o comando após a final &&ou ||...
      • ... qualquer comando em um pipeline, mas o último ...
      • ... ou se o valor de retorno do comando estiver sendo invertido usando !.
    • Essas são as mesmas condições obedecidas pela opção errexit -e .

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

Portanto, 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.

Quanto à coisa rc: 0 , espero que seja algum tipo de bug específico da versão - provavelmente relacionado aos dois gatilhos para a EXIT que ocorrem ao mesmo tempo e ao que obtém o código de saída do outro (o que não deve ocorrer) . E de qualquer maneira, com um bashbinário 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 de modo que você pode ver que as condições do shell são as de um shell script - é não interativa. A saída é:

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

Aqui estão algumas notas relevantes dos changelogs recentes :

  • Corrigido um erro que fazia com que os comandos assíncronos não $?fossem definidos corretamente.
  • Corrigido um erro que fazia com que as mensagens de erro geradas pelos erros de expansão nos forcomandos tivessem o número da linha incorreta.
  • Corrigido um erro que fazia com que SIGINT e SIGQUIT não pudessem ser trapexecutados em comandos de subshell assíncronos.
  • Corrigido um problema com o tratamento de interrupções que fazia com que um segundo SIGINT e o subsequente fossem ignorados por shells interativos.
  • O shell não bloqueia mais o recebimento de sinais ao executar trapmanipuladores para esses sinais e permite que a maioria dos trap manipuladores seja executada recursivamente (executando trapmanipuladores enquanto um trapmanipulador está executando) .

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

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

mikeserv
fonte
Obrigado pela explicação de como a expansão de parâmetros é tratada de maneira diferente. Isso me esclareceu muitas coisas.
dvdgsng
Estou concedendo a você a recompensa porque sua explicação foi muito útil.
dvdgsng
@dvdgsng - Gracias. Por curiosidade, você já apareceu com sua solução?
mikeserv
9

(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 forma 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 "$@"
 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 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
meuh
fonte
agradável, uma solução prática que realmente agrega valor!
Florian Heigl