Detectando erros na substituição de comandos usando "-o errtrace" (ou seja, defina -E)

14

De acordo com este manual de referência :

-E (também -o errtrace)

Se definido, qualquer interceptação no ERR é herdada pelas funções do shell, substituições de comandos e comandos executados em um ambiente de subshell. A interceptação de ERR normalmente não é herdada nesses casos.

No entanto, devo interpretá-lo 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 atribuição simples ,, my_var=$(made_up_name)sairá do script com set -e(ou seja, errexit).

É -E/-o errtracesuposto funcionar como o código acima? Ou, provavelmente, eu o interpretei mal?

dgo.a
fonte
2
Essa é uma boa pergunta. Substituir echo $( made up name )por $( made up name )produz o comportamento desejado. Eu não tenho uma explicação embora.
iruvar
Eu não sei sobre -E do bash, mas sei que -e somente afeta uma saída do shell se o erro resultar do último comando em um pipeline. Portanto, seu var=$( pipe )e $( pipe )exemplos representariam pontos finais de canal, ao contrário de pipe > echonão. Minha página de manual diz: "1. A falha de qualquer comando individual em um pipeline de comandos múltiplos não deve causar a saída do shell. Somente a falha do pipeline em si deve ser considerada."
mikeserv 12/14
Você pode fazê-lo falhar: echo $ ($ {madeupname?}). Mas está pronto. Mais uma vez, -E está fora da minha própria experiência.
mikeserv
@mikeserv @ 1_CR O manual do bash @ echo indica que echosempre retorna 0. Isso deve ser levado em consideração na análise ...

Respostas:

5

Nota: zshirá reclamar de "padrões ruins" se você não o configurar para aceitar "comentários embutidos" para a maioria dos exemplos aqui e não executá-los através de um shell proxy como eu fiz anteriormente sh <<-\CMD.

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

    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 utilizado parameter expansionpara testar ${empty?} _test()ainda returné um passe - como é evidenciado na última echoIsso ocorre porque o valor não mata o $( command substitution )subshell que o contém, mas o seu shell pai - _testneste momento - continua a camionagem. E echonão se importa - é muito bom servir apenas um não\newline; echo é um teste.

Mas considere isso:

    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 _test()'sentrada com um parâmetro pré-avaliado no INIT here-documentagora, a _test()função nem sequer tenta executar. Além do mais, a shconcha aparentemente desiste completamente do fantasma e echo "this doesnt even print" nem sequer imprime.

Provavelmente não é isso que você deseja.

Isso acontece porque o ${var?}estilo parameter-expansion foi projetado para sairshell no caso de um parâmetro ausente, funciona assim :

${parameter:?[word]}

Indique Erro se Nullou Unset.Se o parâmetro estiver desativado ou nulo, a expansion of word(ou uma mensagem indicando que está desativado se a palavra for omitida) deve ser written to standard errore shell exits with a non-zero exit status. Caso contrário, o valor de parameter shall be substituted. Um shell interativo não precisa sair.

Não copio / colo o documento inteiro, mas se você quiser uma falha para um set but nullvalor, use o formulário:

${var :? error message }

Com o :coloncomo acima. Se você deseja que um nullvalor seja bem-sucedido, apenas omita os dois pontos. Você também pode negá-lo e falhar apenas em valores definidos, como mostrarei em um momento.

Outra corrida 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 a partir do meio das pipelinefalhas, e de fato seu command listsubshell contendo falha completamente, pois nenhum dos comandos da função é executado nem a echoexecução a seguir , embora também seja mostrado que ele 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 script, _main | logic | pipelinemas ( subshell in which we ${test?} ) ||é necessário um pouco de sandbox.

E pode não ser óbvio, mas se você quiser passar apenas pelo caso oposto, ou apenas set=valores, 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 quatro formas de substituição de parâmetro POSIX e seus vários :colon nullou not nulltestes. Há mais informações no link acima e aqui está novamente .

E acho que deveríamos mostrar nosso _testtrabalho funcional também, certo? Apenas declaramos empty=somethingcomo parâmetro para nossa função (ou a qualquer momento):

    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 essa avaliação é independente - não requer 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, finalmente, voltamos à pergunta original: como lidar com erros em um $(command substitution)subshell? A verdade é - existem duas maneiras, mas nenhuma é direta. O núcleo do problema é o processo de avaliação do shell - as expansões do shell (inclusive $(command substitution)) acontecem mais cedo no processo de avaliação do shell do que a execução atual dos comandos do shell - que é quando seus erros podem ser capturados e capturados.

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

Então, quais são as duas maneiras? Você faz isso explicitamente dentro do $(command substitution)subshell com testes como faria sem ele ou absorve os resultados em uma variável de 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

Isso 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 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
mikeserv
fonte
Por exemplo, praticamente sua especificação exata: echo $ {v = $ (madeupname)}; echo "$ {v:? trap1st}! não foi atingido!"
mikeserv
1
Para resumir: Essas expansões de parâmetros são uma ótima maneira de controlar se as variáveis ​​são definidas etc. Em relação ao status de saída do eco, notei que echo "abc" "${v1:?}"isso não parece ser executado (o abc nunca é impresso). E o shell retorna 1. Isso é verdade com ou sem um comando par ( "${v1:?}"diretamente no CLI). Mas, para o script do OP, tudo o que era necessário para acionar a armadilha era colocar sua atribuição de variável contendo uma substituição por um comando inexistente sozinho em uma única linha. Caso contrário, o comportamento do eco é retornar 0 sempre, a menos que seja interrompido como nos testes que você explicou.
Apenas v=$( madeup ). Não vejo o que é inseguro com isso. É apenas uma tarefa, a pessoa digitou errado o comando, por exemplo v="$(lss)". Erros. Sim, você pode verificar com o status de erro do último comando $? - porque é o comando na linha (uma atribuição sem nome de comando) e nada mais - não é um argumento para ecoar. Além disso, aqui está preso pela função como! = 0, para que você obtenha feedback duas vezes. Caso contrário, certamente, como você explica, há uma maneira melhor de fazer isso em ordem dentro de uma estrutura, mas o OP tinha uma única linha: um eco mais sua falha na substituição. Ele estava pensando em eco.
Sim, a tarefa simples é mencionada na pergunta e é um dado adquirido, e, embora eu não pudesse oferecer conselhos específicos sobre como usar o -E, tentei mostrar resultados semelhantes ao problema demonstrado que eram mais do que possíveis sem recorrer a basismos. De qualquer forma, são especificamente os problemas mencionados - como atribuição única por linha - que dificultam o manuseio de soluções em um pipeline, que também demonstrei como lidar. Embora seja verdade o que você diz - não há nada de inseguro em simplesmente atribuí-lo.
mikeserv
1
Bom argumento - talvez seja necessário mais esforço. Vou pensar nisso.
mikeserv
1

Se definido, qualquer interceptação no ERR é herdada por funções de shell, substituições de comandos e comandos executados em um ambiente de subshell

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

echo $( made up name )

$( made up name )é considerado como parte do comando. Mesmo se essa parte falhar e retornar com erro, o comando inteiro será executado com êxito, pois echonão sabemos sobre isso. Como o comando retorna com 0, nenhuma armadilha é acionada.

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

var=$(made_up_name)
echo $var
totti
fonte
echo $varnão falha - $varé expandido para esvaziar antes mesmo de echorealmente dar uma olhada.
mikeserv
0

Isso ocorre devido a um erro no bash. Durante a etapa Substituição de Comando

echo $( made up name )

madeé executado (ou falha em ser encontrado) em um subshell, mas o subshell é "otimizado" de forma a não usar algumas traps do shell pai. Isso foi corrigido na versão 4.4.5 :

Sob certas circunstâncias, um comando simples é otimizado para eliminar uma bifurcação, resultando em uma captura EXIT não sendo executada.

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

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

O manipulador de trap foi chamado conforme o esperado e o subshell é encerrado. ( set -eapenas faz com que o subshell saia, não o pai, de modo que a mensagem "não deve ser alcançada" deve, de fato, ser alcançada.)

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

echo $( ( made up name ) )

Os espaços extras são necessários para distinguir da expansão aritmética.

JigglyNaga
fonte