Trampa, ERR y eco de la línea de error

30

Estoy tratando de crear algunos informes de errores usando una trampa para llamar a una función en todos los errores:

Trap "_func" ERR

¿Es posible obtener desde qué línea se envió la señal ERR? El caparazón es bash.

Si hago eso, puedo leer e informar qué comando se usó y registrar / realizar algunas acciones.

¿O tal vez me estoy yendo mal?

Probé con lo siguiente:

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

echo hello | grep "asdf"

Y $LINENOestá regresando 2. No funciona.

Mechaflash
fuente
Puede mirar el script del depurador bash bashdb. Parece que el primer argumento trappuede contener variables que se evalúan en el contexto deseado. Entonces trap 'echo $LINENO' ERR'debería funcionar.
donothings exitosamente
hmm acabo de intentar esto con un mal eco | comando grep y devuelve la línea de la instrucción Trap. Pero echaré un vistazo a bashdb
Mechaflash
Lo siento mucho ... No especifiqué en mi pregunta original que necesito una solución nativa. Edité la pregunta.
Mechaflash
Lo siento, me borked la línea de ejemplo: trap 'echo $LINENO' ERR. El primer argumento trapes la echo $LINENOcita completa . Esto es en bash.
donothings exitosamente
55
@Mechaflash Tendría que ser trap 'echo $LINENO' ERR, con comillas simples, no comillas dobles. Con el comando que escribió, $LINENOse expande cuando se analiza la línea 2, por lo que la trampa es echo 2(o mejor dicho ECHO 2, la que saldría bash: ECHO: command not found).
Gilles 'SO- deja de ser malvado'

Respuestas:

61

Como se señaló en los comentarios, su cita es incorrecta. Necesita comillas simples para evitar que $LINENOse expanda cuando la línea de captura se analiza por primera vez.

Esto funciona:

#! /bin/bash

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

trap 'err_report $LINENO' ERR

echo hello | grep foo  # This is line number 9

Ejecutándolo:

 $ ./test.sh
 Error on line 9
Mat
fuente
gracias por el ejemplo con una llamada a función. No sabía que las comillas dobles expandían la variable en este caso.
Mechaflash
echo hello | grep fooNo parece arrojar un error para mí. ¿Estoy malinterpretando algo?
geotheory
@geotheory En mi sistema greptiene un estado de salida de 0 si hubo una coincidencia, 1 si no hubo coincidencia y> 1 por un error. Puede verificar el comportamiento en su sistema conecho hello | grep foo; echo $?
Patrick
No, tienes razón, es un error :)
geotheory
¿No necesita usar -e en la línea de invocación para causar un error en la falla del comando? Es decir: #! / Bin / bash -e?
Tim Bird
14

También puede usar el 'llamador' integrado de bash:

#!/bin/bash

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

trap err_report ERR

echo hello | grep foo

también imprime el nombre del archivo:

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

Realmente me gusta la respuesta dada por @Mat arriba. Sobre la base de esto, escribí un pequeño ayudante que da un poco más de contexto para el error:

Podemos inspeccionar el script para la línea que causó la falla:

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

Aquí está en un pequeño script de prueba:

#!/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

Cuando lo ejecutamos obtenemos:

$ /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
poco pitón
fuente
Esto sería aún mejor usando $(caller)los datos de '' para dar el contexto, incluso si la falla no está en el script actual sino en una de sus importaciones. ¡Muy bonito sin embargo!
Tricasse
2

Inspirado por otra respuesta, aquí hay un controlador de errores contextuales más simple:

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

También puede usar awk en lugar de cola y cabeza si es necesario.

sanmai
fuente
1
hay una razón por la que la otra respuesta proporciona contexto a través de 3 líneas arriba y 3 líneas debajo de la línea ofensiva: ¿qué pasa si el error emana de una línea de continuación?
iruvar
@iruvar esto se entiende, pero no necesito nada de ese contexto adicional; una línea de contexto es tan simple como parece, y tan suficiente como necesito
sanmai
Ok mi amigo, + 1
iruvar
1

Aquí hay otra versión, inspirada en @sanmai y @unpythonic. Muestra líneas de script alrededor del error, con números de línea y el estado de salida, usando tail & head, ya que parece más simple que la solución awk.

Mostrando esto como dos líneas aquí para facilitar la lectura: puede unir estas líneas en una si lo prefiere (preservando ;):

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

Esto funciona bastante bien con set -euo pipefail( modo estricto no oficial ): cualquier error variable indefinido da un número de línea sin disparar la ERRpseudo-señal, pero los otros casos muestran contexto.

Salida de ejemplo:

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
fuente
0

¿Es posible obtener desde qué línea se envió la señal ERR?

Sí, LINENOy las BASH_LINENOvariables son útiles para obtener la línea de falla y las líneas que conducen a ella.

¿O tal vez me estoy yendo mal?

No, solo falta la -qopción con grep ...

echo hello | grep -q "asdf"

... Con la -qopción grepvolverá 0por truey 1para false. Y en Bash es trapno Trap...

trap "_func" ERR

... Necesito una solución nativa ...

Aquí hay un trampero que puede ser útil para depurar cosas que tienen un poco más de complejidad 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}
}

... y un script de uso de ejemplo para exponer las diferencias sutiles en cómo configurar la trampa anterior para el rastreo de funciones también ...

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'

Lo anterior se probó en Bash versión 4+, así que deje un comentario si necesita algo para versiones anteriores a la cuatro, o abra un problema si no logra atrapar fallas en sistemas con una versión mínima de cuatro.

Las principales conclusiones son ...

set -E -o functrace
  • -Eprovoca errores en funciones a burbujear

  • -o functrace causas permite más verbosidad cuando algo dentro de una función falla

trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
  • Las comillas simples se usan alrededor de la llamada de función y las comillas dobles se usan alrededor de argumentos individuales.

  • Las referencias LINENOy BASH_LINENOse pasan en lugar de los valores actuales, aunque esto podría acortarse en versiones posteriores de vinculados a trap, de modo que la línea de falla final llegue a la salida

  • Se pasan los valores BASH_COMMANDy el estado de salida ( $?), primero para obtener el comando que devolvió un error, y segundo para garantizar que la trampa no se active en estados sin error

Y aunque otros pueden estar en desacuerdo, creo que es más fácil construir una matriz de salida y usar printf para imprimir cada elemento de la matriz en su propia línea ...

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

... también el >&2bit al final hace que los errores vayan a donde deberían (error estándar), y permite capturar solo errores ...

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

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

Como se muestra en estos y otros ejemplos en Stack Overflow, hay muchas formas de crear una ayuda de depuración utilizando utilidades integradas.

S0AndS0
fuente