Trap, ERR e ecoando a linha de erro

30

Estou tentando criar alguns relatórios de erros usando um trap para chamar uma função em todos os erros:

Trap "_func" ERR

É possível obter de que linha o sinal ERR foi enviado? O shell é uma festança.

Se fizer isso, posso ler e relatar qual comando foi usado e registrar / executar algumas ações.

Ou talvez eu esteja indo para tudo errado?

Eu testei com o seguinte:

#!/bin/bash
trap "ECHO $LINENO" ERR

echo hello | grep "asdf"

E $LINENOestá retornando 2. Não está funcionando.

Mechaflash
fonte
Você pode olhar para o script do depurador do bash bashdb. Parece que o primeiro argumento para trappode conter variáveis ​​que são avaliadas no contexto desejado. Então trap 'echo $LINENO' ERR'deve funcionar.
sucesso #
hmm tentei isso com um eco ruim | comando grep e retorna a linha da instrução Trap. Mas vou dar uma olhada no bashdb
Mechaflash
Sinto muito ... Não especifiquei na minha pergunta original que preciso de uma solução nativa. Eu editei a pergunta.
Mechaflash
Desculpe, eu borked a linha de exemplo: trap 'echo $LINENO' ERR. O primeiro argumento para trapé todo o echo $LINENOtexto citado. Isso está no bash.
sucesso #
5
@ Mechaflash Teria que ser trap 'echo $LINENO' ERR, com aspas simples, não aspas duplas. Com o comando que você escreveu, $LINENOé expandido quando a linha 2 é analisada, de modo que a interceptação é echo 2(ou melhor ECHO 2, qual seria a saída bash: ECHO: command not found).
Gilles 'SO- stop be evil'

Respostas:

61

Como indicado nos comentários, sua citação está errada. Você precisa de aspas simples para impedir que $LINENOseja expandida quando a linha de interceptação for analisada pela primeira vez.

Isso funciona:

#! /bin/bash

err_report() {
    echo "Error on line $1"
}

trap 'err_report $LINENO' ERR

echo hello | grep foo  # This is line number 9

Executando:

 $ ./test.sh
 Error on line 9
Mat
fonte
obrigado pelo exemplo com uma chamada de função. Eu não sabia que aspas duplas expandiam a variável nesse caso.
Mechaflash
echo hello | grep foonão parece lançar erro para mim. Estou entendendo mal alguma coisa?
Geotheory
@geotheory No meu sistema grep, o status de saída é 0 se houver uma correspondência, 1 se não houver correspondência e> 1 para um erro. Você pode verificar o comportamento em seu sistema comecho hello | grep foo; echo $?
Patrick
Não, você está certo, é um erro :)
geotheory
Você não precisa usar -e na linha de chamada, para causar erro na falha do comando? Ou seja: #! / Bin / bash -e?
Tim pássaro
14

Você também pode usar o 'chamador' do bash:

#!/bin/bash

err_report() {
  echo "errexit on line $(caller)" >&2
}

trap err_report ERR

echo hello | grep foo

também imprime o nome do arquivo:

$ ./test.sh
errexit on line 9 ./test.sh
Andrew Ivanov
fonte
7

Eu realmente gosto da resposta dada pelo @Mat acima. Com base nisso, escrevi um pequeno auxiliar que fornece um pouco mais de contexto para o erro:

Podemos inspecionar o script para a linha que causou a falha:

err() {
    echo "Error occurred:"
    awk 'NR>L-4 && NR<L+4 { printf "%-5d%3s%s\n",NR,(NR==L?">>>":""),$0 }' L=$1 $0
}
trap 'err $LINENO' ERR

Aqui está um pequeno script de teste:

#!/bin/bash

set -e

err() {
    echo "Error occurred:"
    awk 'NR>L-4 && NR<L+4 { printf "%-5d%3s%s\n",NR,(NR==L?">>>":""),$0 }' L=$1 $0
}
trap 'err $LINENO' ERR

echo one
echo two
echo three
echo four
false
echo five
echo six
echo seven
echo eight

Quando executamos, obtemos:

$ /tmp/test.sh
one
two
three
four
Error occurred:
12      echo two
13      echo three
14      echo four
15   >>>false
16      echo five
17      echo six
18      echo seven
antitônico
fonte
Seria ainda melhor usar $(caller)os dados de para fornecer o contexto, mesmo que a falha não esteja no script atual, mas em uma de suas importações. Muito bom!
tricasse 7/03
2

Inspirado em outra resposta, aqui está um manipulador de erros contextuais mais simples:

trap '>&2 echo Command failed: $(tail -n+$LINENO $0 | head -n1)' ERR

Você também pode usar awk em vez de cauda e cabeça, se necessário.

sanmai
fonte
1
há uma razão pela qual a outra resposta fornece contexto por meio de 3 linhas acima e 3 linhas abaixo da linha incorreta - e se o erro emanar de uma linha de continuação?
iruvar 23/03
@iruvar isso é entendido, mas não preciso de nenhum contexto extra; uma linha de contexto é tão simples quanto possível e suficiente quanto eu preciso
sanmai 23/03
Ok meu amigo, + 1
iruvar 23/03
1

Aqui está outra versão, inspirada em @sanmai e @unpythonic. Ele mostra as linhas de script em torno do erro, com os números das linhas e o status da saída - usando tail & head como parece mais simples que a solução awk.

Mostrando isso como duas linhas aqui para facilitar a leitura - você pode unir essas linhas em uma, se preferir (preservando o ;):

trap 'echo >&2 "Error - exited with status $? at line $LINENO:"; 
         pr -tn $0 | tail -n+$((LINENO - 3)) | head -n7' ERR

Isso funciona muito bem com set -euo pipefail( modo estrito não oficial ) - qualquer erro variável indefinido fornece um número de linha sem disparar o ERRpseudo-sinal, mas os outros casos mostram o contexto.

Exemplo de saída:

myscript.sh: line 27: blah: command not found
Error - exited with status 127 at line 27:
   24   # Do something
   25   lines=$(wc -l /etc/passwd)
   26   # More stuff
   27   blah
   28   
   29   # Check time
   30   time=$(date)
RichVel
fonte
0

É possível obter de que linha o sinal ERR foi enviado?

Sim, LINENOe as BASH_LINENOvariáveis ​​são úteis para obter a linha de falha e as linhas que levam a ela.

Ou talvez eu esteja indo para tudo errado?

Não, apenas falta a -qopção com grep ...

echo hello | grep -q "asdf"

... Com a -qopção grepretornará 0para truee 1para false. E em Bash é trapnão Trap...

trap "_func" ERR

... preciso de uma solução nativa ...

Aqui está um captador que você pode achar útil para depurar coisas que têm um pouco mais de complexidade ciclomática ...

failure.sh

## Outputs Front-Mater formatted failures for functions not returning 0
## Use the following line after sourcing this file to set failure trap
##    trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
failure(){
    local -n _lineno="${1:-LINENO}"
    local -n _bash_lineno="${2:-BASH_LINENO}"
    local _last_command="${3:-${BASH_COMMAND}}"
    local _code="${4:-0}"

    ## Workaround for read EOF combo tripping traps
    if ! ((_code)); then
        return "${_code}"
    fi

    local _last_command_height="$(wc -l <<<"${_last_command}")"

    local -a _output_array=()
    _output_array+=(
        '---'
        "lines_history: [${_lineno} ${_bash_lineno[*]}]"
        "function_trace: [${FUNCNAME[*]}]"
        "exit_code: ${_code}"
    )

    if [[ "${#BASH_SOURCE[@]}" -gt '1' ]]; then
        _output_array+=('source_trace:')
        for _item in "${BASH_SOURCE[@]}"; do
            _output_array+=("  - ${_item}")
        done
    else
        _output_array+=("source_trace: [${BASH_SOURCE[*]}]")
    fi

    if [[ "${_last_command_height}" -gt '1' ]]; then
        _output_array+=(
            'last_command: ->'
            "${_last_command}"
        )
    else
        _output_array+=("last_command: ${_last_command}")
    fi

    _output_array+=('---')
    printf '%s\n' "${_output_array[@]}" >&2
    exit ${_code}
}

... e um exemplo de script de uso para expor as diferenças sutis de como definir a interceptação acima também para o rastreamento de funções ...

example_usage.sh

#!/usr/bin/env bash

set -E -o functrace

## Optional, but recommended to find true directory this script resides in
__SOURCE__="${BASH_SOURCE[0]}"
while [[ -h "${__SOURCE__}" ]]; do
    __SOURCE__="$(find "${__SOURCE__}" -type l -ls | sed -n 's@^.* -> \(.*\)@\1@p')"
done
__DIR__="$(cd -P "$(dirname "${__SOURCE__}")" && pwd)"


## Source module code within this script
source "${__DIR__}/modules/trap-failure/failure.sh"

trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR


something_functional() {
    _req_arg_one="${1:?something_functional needs two arguments, missing the first already}"
    _opt_arg_one="${2:-SPAM}"
    _opt_arg_two="${3:0}"
    printf 'something_functional: %s %s %s' "${_req_arg_one}" "${_opt_arg_one}" "${_opt_arg_two}"
    ## Generate an error by calling nothing
    "${__DIR__}/nothing.sh"
}


## Ignoring errors prevents trap from being triggered
something_functional || echo "Ignored something_functional returning $?"
if [[ "$(something_functional 'Spam!?')" == '0' ]]; then
    printf 'Nothing somehow was something?!\n' >&2 && exit 1
fi


## And generating an error state will cause the trap to _trace_ it
something_functional '' 'spam' 'Jam'

O exemplo acima foi testado no Bash versão 4+, portanto, deixe um comentário se for necessário algo para versões anteriores ao quatro ou Abra um problema se ele não detectar falhas nos sistemas com uma versão mínima de quatro.

Os principais tópicos são ...

set -E -o functrace
  • -Efaz com que erros nas funções borbulhem

  • -o functrace causas permite mais verbosidade quando algo dentro de uma função falha

trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
  • Aspas simples são usadas em torno da chamada de função e as aspas duplas em torno de argumentos individuais

  • Referências LINENOe BASH_LINENOsão passadas em vez dos valores atuais, embora isso possa ser abreviado em versões posteriores do vinculado à interceptação, de modo que a linha de falha final chegue à saída

  • Os valores de BASH_COMMANDe exit status ( $?) são passados, primeiro para obter o comando que retornou um erro e, segundo, para garantir que a interceptação não seja acionada em status de não erro

E enquanto outros podem discordar, acho mais fácil criar uma matriz de saída e usar printf para imprimir cada elemento da matriz em sua própria linha ...

printf '%s\n' "${_output_array[@]}" >&2

... também o >&2bit no final faz com que os erros cheguem aonde deveriam (erro padrão) e permite capturar apenas erros ...

## ... to a file...
some_trapped_script.sh 2>some_trapped_errros.log

## ... or by ignoring standard out...
some_trapped_script.sh 1>/dev/null

Conforme mostrado por esses e outros exemplos no Stack Overflow, existem várias maneiras de criar um auxiliar de depuração usando utilitários embutidos.

S0AndS0
fonte