Trapping erros na substituição de comandos usando "-o errtrace" (ie set -E)

14

De acordo com este ref manual :

-E (also -o errtrace)

If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment. The ERR trap is normally not inherited in such cases.

No entanto, devo estar interpretando incorretamente, porque o seguinte não funciona:

#!/usr/bin/env bash
# -*- bash -*-

set -e -o pipefail -o errtrace -o functrace

function boom {
  echo "err status: $?"
  exit $?
}
trap boom ERR


echo $( made up name )
echo "  ! should not be reached ! "

Eu já sei que a atribuição simples, my_var=$(made_up_name) , sairá do script com set -e (ou seja, errexit).

O -E/-o errtrace deveria funcionar como o código acima? Ou, mais provavelmente, eu interpretei mal?

    
por dgo.a 02.07.2013 / 12:06

4 respostas

5

Observação: zsh irá reclamar sobre "padrões ruins" se você não configurá-lo para aceitar "comentários em linha" para a maioria dos exemplos aqui e não executá-los por meio de um proxy shell feito com sh <<-\CMD .

Ok, então, como afirmei nos comentários acima, eu não sei especificamente sobre bash set -E , mas eu sei que os shells compatíveis com POSIX fornecem um meio simples de testar um valor se você deseja:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
        echo "echo still works" 
    }
    _test && echo "_test doesnt fail"
    # END
    CMD
sh: line 1: empty: error string
+ echo

+ echo 'echo still works'
echo still works
+ echo '_test doesnt fail'
_test doesnt fail

Acima, você verá que, embora eu tenha usado parameter expansion para testar ${empty?} _test() ainda return s um passe - como é evidenciada no último echo Isso ocorre porque o valor com falha mata o submarino $( command substitution ) que o contém, mas seu shell pai - _test neste momento - continua em caminhões. E echo não se importa - é muito prazer servir apenas um \newline; echo é não um teste.

Mas considere isto:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    _test ||\
            echo "this doesnt even print"
    # END
    CMD
_test+ sh: line 1: empty: function doesnt run

Como eu alimentei a entrada _test()'s com um parâmetro pré-avaliado no INIT here-document agora a função _test() faz ' nem tente correr. Além disso, o shell sh aparentemente desiste totalmente do fantasma e echo "this doesnt even print" nem imprime.

Provavelmente não é o que você quer.

Isso acontece porque a expansão de parâmetro de estilo ${var?} é projetada para sair do shell no caso de um parâmetro ausente , funciona assim :

${parameter:?[word]}

Indicate Error if Null or Unset. If parameter is unset or null, the expansion of word (or a message indicating it is unset if word is omitted) shall be written to standard error and the shell exits with a non-zero exit status. Otherwise, the value of parameter shall be substituted. An interactive shell need not exit.

Não vou copiar / colar o documento inteiro, mas se você quiser uma falha para um valor set but null , use o formulário:

${var :? error message }

Com o :colon como acima. Se você quiser que um valor null seja bem-sucedido, apenas omita os dois pontos. Você também pode negar e falhar apenas por valores definidos, como mostrarei em breve.

Outra execução de _test():

    sh <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    echo "this runs" |\
        ( _test ; echo "this doesnt" ) ||\
            echo "now it prints"
    # END
    CMD
this runs
sh: line 1: empty: function doesnt run
now it prints

Isso funciona com todos os tipos de testes rápidos, mas acima você verá que _test() , executado no meio do pipeline falha, e em de fato, seu subshell contendo command list falha completamente, pois nenhum dos comandos dentro da função é executado nem a seguinte echo é executada, embora também seja mostrado que pode ser facilmente testado porque echo "now it prints" agora é impresso.

O diabo está nos detalhes, eu acho. No caso acima, o shell que sai não é do _main | logic | pipeline do script, mas o ( subshell in which we ${test?} ) || um pouco de sandboxing é necessário.

E isso pode não ser óbvio, mas se você quiser passar apenas pelo caso oposto, ou somente valores set= , também é bastante simples:

    sh <<-\CMD
    N= #N is NULL
    _test=$N #_test is also NULL and
    v="something you would rather do without"    
    ( #this subshell dies
        echo "v is ${v+set}: and its value is ${v:+not NULL}"
        echo "So this ${_test:-"\$_test:="} will equal ${_test:="$v"}"
        ${_test:+${N:?so you test for it with a little nesting}}
        echo "sure wish we could do some other things"
    )
    ( #this subshell does some other things 
        unset v #to ensure it is definitely unset
        echo "But here v is ${v-unset}: ${v:+you certainly wont see this}"
        echo "So this ${_test:-"\$_test:="} will equal NULL ${_test:="$v"}"
        ${_test:+${N:?is never substituted}}
        echo "so now we can do some other things" 
    )
    #and even though we set _test and unset v in the subshell
    echo "_test is still ${_test:-"NULL"} and ${v:+"v is still $v"}"
    # END
    CMD
v is set: and its value is not NULL
So this $_test:= will equal something you would rather do without
sh: line 7: N: so you test for it with a little nesting
But here v is unset:
So this $_test:= will equal NULL
so now we can do some other things
_test is still NULL and v is still something you would rather do without

O exemplo acima aproveita todas as 4 formas de substituição do parâmetro POSIX e seus vários testes :colon null ou not null . Há mais informações no link acima e aqui está novamente .

E eu acho que devemos mostrar o nosso trabalho de função _test também, certo? Nós apenas declaramos empty=something como um parâmetro para a nossa função (ou a qualquer momento antes):

    sh <<-\CMD
    _test() { echo $( echo ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?tested as a pass before function runs}
    INIT
    echo "this runs" >&2 |\
        ( empty=not_empty _test ; echo "yay! I print now!" ) ||\
            echo "suspiciously quiet"
    # END
    CMD
this runs
not_empty
echo still works
yay! I print now!

Deve-se notar que esta avaliação é independente - não requer nenhum teste adicional para falhar. Mais alguns exemplos:

    sh <<-\CMD
    empty= 
    ${empty?null, no colon, no failure}
    unset empty
    echo "${empty?this is stderr} this is not"
    # END
    CMD
sh: line 3: empty: this is stderr

    sh <<-\CMD
    _input_fn() { set -- "$@" #redundant
            echo ${*?WHERES MY DATA?}
            #echo is not necessary though
            shift #sure hope we have more than $1 parameter
            : ${*?WHERES MY DATA?} #: do nothing, gracefully
    }
    _input_fn heres some stuff
    _input_fn one #here
    # shell dies - third try doesnt run
    _input_fn you there?
    # END
    CMD
heres some stuff
one
sh: line :5 *: WHERES MY DATA?

E então, finalmente, voltamos à pergunta original: como lidar com erros em uma subcamada $(command substitution) ? A verdade é que existem duas maneiras, mas nenhuma é direto. O núcleo do problema é o processo de avaliação do shell - expansões de shell (incluindo $(command substitution) ) acontecem mais cedo no processo de avaliação do shell do que a execução do comando shell atual - que é quando seus erros podem ser capturados e capturados .

O problema que o op experimenta é que quando o shell atual avalia os erros, o subshell $(command substitution) já foi substituído - nenhum erro permanece.

Então, quais são as duas maneiras? Ou você faz isto explicitamente dentro do subshell $(command substitution) com testes como você faria sem ele, ou você absorve seus resultados em uma variável shell atual e testa seu valor.

Método 1:

    echo "$(madeup && echo \: || echo '${fail:?die}')" |\
          . /dev/stdin

sh: command not found: madeup
/dev/stdin:1: fail: die

    echo $?

126

Método 2:

    var="$(madeup)" ; echo "${var:?die} still not stderr"

sh: command not found: madeup
sh: var: die

    echo $?

1

falhará independentemente do número de variáveis declaradas por linha:

   v1="$(madeup)" v2="$(ls)" ; echo "${v1:?}" "${v2:?}"

sh: command not found: madeup
sh: v1: parameter not set

E o nosso valor de retorno permanece constante:

    echo $?
1

AGORA A ARMADILHA:

    trap 'printf %s\n trap resurrects shell!' ERR
    v1="$(madeup)" v2="$(printf %s\n shown after trap)"
    echo "${v1:?#1 - still stderr}" "${v2:?invisible}"

sh: command not found: madeup
sh: v1: #1 - still stderr
trap
resurrects
shell!
shown
after
trap

    echo $?
0
    
por 27.03.2014 / 01:10
1

If set, any trap on ERR is inherited by shell functions, command substitutions, and commands executed in a subshell environment

No seu script, é uma execução de comando ( echo $( made up name ) ). Nos comandos bash são delimitados com ; ou com nova linha . No comando

echo $( made up name )

$( made up name ) é considerado como parte do comando. Mesmo se esta parte falhar e retornar com erro, o comando inteiro será executado com sucesso, pois echo não sabe disso. Como o comando retorna com 0 nenhum trap disparado.

Você precisa colocá-lo em dois comandos, atribuição e eco

var=$(made_up_name)
echo $var
    
por 14.03.2014 / 11:36
0

Isso se deve a um bug no bash. Durante a etapa Substituição de Comando em

echo $( made up name )

made é executado (ou não é encontrado) em um subshell, mas o subshell é "otimizado" de tal forma que não usa alguns traps do shell pai. Isso foi corrigido na versão 4.4.5 :

Under certain circumstances, a simple command is optimized to eliminate a fork, resulting in an EXIT trap not being executed.

Com o bash 4.4.5 ou superior, você deve ver a seguinte saída:

error.sh: line 13: made: command not found
err status: 127
  ! should not be reached !

O manipulador de armadilha foi chamado como esperado e, em seguida, a subchave sai. ( set -e apenas faz com que a subshell saia, não o pai, de modo que a mensagem "não deveria ser encontrada" seja, de fato, alcançada.)

Uma solução alternativa para versões mais antigas é forçar a criação de um subshell completo não otimizado:

echo $( ( made up name ) )

Os espaços extras são necessários para diferenciar da Expansão Aritmética.

    
por 07.11.2018 / 15:45
-1

Considere a diferença na saída dos conjuntos de comandos a seguir que usam o parâmetro ? , expandindo para o status de saída do pipeline de primeiro plano executado mais recentemente:

# grep . $(made) $?; echo $? ${PIPESTATUS[@]}
bash: made: command not found
grep: 127: No such file or directory
2 2

# echo $(made) $?; echo $? ${PIPESTATUS[@]}
bash: made: command not found
127
0 0

A echo bash integrada always retorna 0:

echo [-neE] [arg …]

Output the args, separated by spaces, terminated with a newline. The return status is always 0. (...)

A substituição de comandos acontece em um subshell ambiente e além disso , se um comando não for encontrado, o processo filho criado para executá-lo retorna um status de 127 . E assim foi em nossos exemplos. O comando grep sai com um status 2 (ocorreu um erro que não está relacionado a não encontrar linhas). O comportamento do comando echo é previsível conforme documentado no manual bash: ele retorna 0, independentemente do que acontecer com a expansão de seus argumentos. Portanto, não há erros para interceptar seu exemplo.

O comando simples: um lembrete

Você mencionou que a atribuição de variável contendo uma substituição de comando para um comando que não existe acionará a saída do shell com set -e . Esse não é o caso da atribuição de variável opcional precedendo um nome de comando em um comando simples:

# var=$(made) echo $?; echo $? ${PIPESTATUS[@]}
bash: made: command not found
0
0 0

Mas é realmente o caso com um comando simples que não contém um nome de comando, ou seja, apenas atribuição de variável (s):

var=$(made); echo $? ${PIPESTATUS[@]}
bash: made: command not found
127 127

mas não se houver mais de uma substituição de comando nessas atribuições e a última tiver êxito:

var=$(made) var2=$(ls); echo $? ${PIPESTATUS[@]}
bash: made: command not found
0 0

Novamente, esse comportamento é totalmente documentado :

If there is a command name left after expansion, execution proceeds as described below. Otherwise, the command exits. If one of the expansions contained a command substitution, the exit status of the command is the exit status of the last command substitution performed. If there were no command substitutions, the command exits with a status of zero.

No nosso último exemplo, as duas atribuições de variáveis constituem um comando (simples), obviamente, mas não um com um nome de comando após a atribuição.

Conclusão

Manter uma atribuição de variável por linha deve permitir que qualquer substituição de comando que contenha seja capturada em caso de erro. O echo builtin retornará 0 como seu status de saída e o status de saída de seus argumentos nunca poderá manchar isso. O shell pode falhar por outras razões, por exemplo, no caso de um método específico projetado para validar se as variáveis são definidas (conforme demonstrado por outra resposta a este Q) que seriam avaliadas antes na ordem de execução. Mas o fato permanece: o comportamento do shell quando um comando não é encontrado, dentro ou não de uma substituição de comando, é previsível e documentado - o processo filho retorna 127, mas o eco prossegue. É por isso que a substituição deve ser testada em uma única tarefa ou por outros meios. Esse entendimento é um pré-requisito para explorar a diferença que as opções set farão em relação à armadilha em seu script.

    
por 20.03.2014 / 02:14