Piège, ERR et écho de la ligne d'erreur

30

J'essaie de créer des rapports d'erreur à l'aide d'un piège pour appeler une fonction sur toutes les erreurs:

Trap "_func" ERR

Est-il possible d'obtenir de quelle ligne le signal ERR a été envoyé? La coquille est bash.

Si je le fais, je peux lire et signaler quelle commande a été utilisée et enregistrer / effectuer certaines actions.

Ou peut-être que je me trompe?

J'ai testé avec les éléments suivants:

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

echo hello | grep "asdf"

Et $LINENOrevient 2. Ne fonctionne pas.

Mechaflash
la source
Vous pouvez regarder le script de débogage bash bashdb. Il semble que le premier argument à trapcontenir puisse contenir des variables évaluées dans le contexte souhaité. Cela trap 'echo $LINENO' ERR'devrait donc fonctionner.
donothingsuccess
hmm vient de l'essayer avec un mauvais écho | grep et renvoie la ligne de l'instruction Trap. Mais je vais jeter un œil à bashdb
Mechaflash
Je suis vraiment désolé ... Je n'ai pas précisé dans ma question d'origine que j'avais besoin d'une solution native. J'ai édité la question.
Mechaflash
Désolé, je foireuse la ligne d'exemple: trap 'echo $LINENO' ERR. Le premier argument de trapest l'intégralité du echo $LINENOtexte. C'est en bash.
donothingsuccess
5
@Mechaflash Cela devrait être trap 'echo $LINENO' ERR, avec des guillemets simples, pas des guillemets doubles. Avec la commande que vous avez écrite, $LINENOest développée lorsque la ligne 2 est analysée, de sorte que le piège est echo 2(ou plutôt ECHO 2, qui sortirait bash: ECHO: command not found).
Gilles 'SO- arrête d'être méchant'

Réponses:

61

Comme indiqué dans les commentaires, votre citation est erronée. Vous avez besoin de guillemets simples pour éviter $LINENOd'être étendu lors de la première analyse de la ligne d'interruption.

Cela marche:

#! /bin/bash

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

trap 'err_report $LINENO' ERR

echo hello | grep foo  # This is line number 9

Exécuter:

 $ ./test.sh
 Error on line 9
Mat
la source
merci pour l'exemple avec un appel de fonction. Je ne savais pas que les guillemets doubles développaient la variable dans ce cas.
Mechaflash
echo hello | grep foone semble pas me jeter d’erreur. Suis-je en train de mal comprendre quelque chose?
géothèque
@geotheory Sur mon système, l' grepétat de sortie est 0 s'il y a correspondance, 1 s'il n'y a pas de correspondance et> 1 pour une erreur. Vous pouvez vérifier le comportement de votre système avececho hello | grep foo; echo $?
Patrick
Non, vous avez raison, c'est une erreur :)
geotheory
N'avez-vous pas besoin d'utiliser -e sur la ligne d'appel pour provoquer une erreur en cas d'échec de la commande? C'est-à-dire: #! / Bin / bash -e?
Tim Bird
14

Vous pouvez également utiliser l'appelant bash intégré:

#!/bin/bash

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

trap err_report ERR

echo hello | grep foo

il imprime également le nom de fichier:

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

J'aime vraiment la réponse donnée par @Mat ci-dessus. Sur cette base, j'ai écrit un petit assistant qui donne un peu plus de contexte à l'erreur:

Nous pouvons inspecter le script pour la ligne qui a provoqué l'échec:

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

Le voici dans un petit script de test:

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

Lorsque nous l'exécutons, nous obtenons:

$ /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
impythonique
la source
Ce serait encore mieux d'utiliser $(caller)les données de pour donner le contexte même si l'échec n'est pas dans le script actuel mais dans l'une de ses importations. Très bien quand même!
tricasse
2

Inspiré par d'autres réponses, voici un gestionnaire d'erreurs contextuelles plus simple:

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

Vous pouvez également utiliser awk au lieu de tail & head si nécessaire.

sanmai
la source
1
il y a une raison pour laquelle l'autre réponse fournit le contexte par 3 lignes au-dessus et 3 lignes en dessous de la ligne incriminée - que se passe-t-il si l'erreur provient d'une ligne de continuation?
iruvar
@iruvar c'est compris, mais je n'ai pas besoin de ce contexte supplémentaire; une ligne de contexte est aussi simple que possible et aussi suffisante que nécessaire
sanmai
Ok mon ami, + 1
iruvar
1

Voici une autre version, inspirée de @sanmai et @unpythonic. Il montre les lignes de script autour de l'erreur, avec les numéros de ligne et l'état de sortie - en utilisant tail & head car cela semble plus simple que la solution awk.

En affichant ici deux lignes pour plus de lisibilité - vous pouvez joindre ces lignes en une seule si vous préférez (en préservant le ;):

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

Cela fonctionne assez bien avec set -euo pipefail( mode strict non officiel ) - toute erreur de variable non définie donne un numéro de ligne sans déclencher le ERRpseudo-signal, mais les autres cas montrent le contexte.

Exemple de sortie:

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
la source
0

Est-il possible d'obtenir de quelle ligne le signal ERR a été envoyé?

Oui, LINENOet les BASH_LINENOvariables sont très utiles pour obtenir la ligne d'échec et les lignes qui y mènent.

Ou peut-être que je me trompe?

Non, il manque juste une -qoption avec grep ...

echo hello | grep -q "asdf"

... Avec l' -qoption grepreviendra 0pour trueet 1pour false. Et dans Bash ce n'est trappas Trap...

trap "_func" ERR

... j'ai besoin d'une solution native ...

Voici un trappeur que vous pourriez trouver utile pour déboguer des choses qui ont un peu plus de complexité cyclomatique ...

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}
}

... et un exemple de script d'utilisation pour exposer les différences subtiles dans la façon de définir le piège ci-dessus pour le traçage de fonction aussi ...

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'

Ce qui précède a été testé sur Bash version 4+, alors laissez un commentaire si quelque chose pour les versions antérieures à quatre est nécessaire, ou ouvrez un problème s'il ne parvient pas à intercepter les échecs sur les systèmes avec une version minimale de quatre.

Les principaux plats à emporter sont ...

set -E -o functrace
  • -Efait bouillonner les erreurs dans les fonctions

  • -o functrace causes permet plus de verbosité lorsque quelque chose dans une fonction échoue

trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
  • Les guillemets simples sont utilisés autour de l'appel de fonction et les guillemets doubles sont autour des arguments individuels

  • Les références à LINENOet BASH_LINENOsont passées à la place des valeurs actuelles, bien que cela puisse être raccourci dans les versions ultérieures de Linked to Trap, de sorte que la ligne d'échec finale la rende en sortie

  • Les valeurs de BASH_COMMANDet exit status ( $?) sont transmises, d'abord pour obtenir la commande qui a renvoyé une erreur, et ensuite pour s'assurer que l'interruption ne se déclenche pas sur les états sans erreur

Et tandis que d'autres peuvent être en désaccord, je trouve qu'il est plus facile de créer un tableau de sortie et d'utiliser printf pour imprimer chaque élément du tableau sur sa propre ligne ...

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

... aussi le >&2bit à la fin fait que les erreurs vont là où elles devraient (erreur standard), et permet de capturer uniquement les erreurs ...

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

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

Comme le montrent ces exemples et d' autres sur Stack Overflow, il existe de nombreuses façons de créer une aide au débogage à l'aide d'utilitaires intégrés.

S0AndS0
la source