Pułapka, błąd i echo linii błędu


30

Próbuję utworzyć raportowanie błędów za pomocą pułapki w celu wywołania funkcji dla wszystkich błędów:

Trap "_func" ERR

Czy można uzyskać informację, z której linii wysłano sygnał ERR? Powłoka to bash.

Jeśli to zrobię, mogę odczytać i zgłosić, które polecenie zostało użyte, i zarejestrować / wykonać niektóre działania.

A może źle to rozumiem?

Testowałem z następującymi:

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

echo hello | grep "asdf"

I $LINENOwraca 2. Nie działa.


Możesz spojrzeć na skrypt debuggera bash bashdb. Wydaje się, że pierwszy argument trapmoże zawierać zmienne, które są oceniane w pożądanym kontekście. Tak trap 'echo $LINENO' ERR'powinno działać.
darowizny z powodzeniem

hmm właśnie spróbowałem tego ze złym echem | polecenie grep i zwraca wiersz instrukcji Trap. Ale spojrzę na bashdb
Mechaflash

Tak mi przykro ... W swoim pierwotnym pytaniu nie podałem, że potrzebuję rozwiązania natywnego. Zredagowałem pytanie.
Mechaflash

Niestety, ja borked przykładowy wiersz: trap 'echo $LINENO' ERR. Pierwszym argumentem trapjest cały echo $LINENOzacytowany. To jest bash.
darowizny z powodzeniem

5
@Mechaflash Musiałoby to być trap 'echo $LINENO' ERR, z pojedynczymi cudzysłowami, a nie podwójnymi cudzysłowami. Polecenie, które napisałeś, $LINENOjest rozszerzane, gdy linia 2 jest analizowana, więc pułapka jest echo 2(a raczej ECHO 2, która by wyprowadziła bash: ECHO: command not found).
Gilles „SO- przestań być zły”

Odpowiedzi:


61

Jak wskazano w komentarzach, twoje cytaty są błędne. Potrzebujesz pojedynczych cudzysłowów, aby zapobiec $LINENOrozszerzaniu przy pierwszej analizie linii pułapki.

To działa:

#! /bin/bash

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

trap 'err_report $LINENO' ERR

echo hello | grep foo  # This is line number 9

Uruchamianie:

 $ ./test.sh
 Error on line 9

dzięki za przykład z wywołaniem funkcji. Nie wiedziałem, że podwójne cudzysłowy rozszerzyły zmienną w tym przypadku.
Mechaflash

echo hello | grep foonie wydaje mi się, że to błąd. Czy coś nie rozumiem?
geotheory

@geotheory W moim systemie grepma status wyjścia 0, jeśli było dopasowanie, 1, jeśli nie było dopasowania i> 1 dla błędu. Możesz sprawdzić zachowanie w swoim systemie za pomocąecho hello | grep foo; echo $?
Patrick

Nie, masz rację, to błąd :)
geotheory

Czy nie musisz używać -e w wierszu wywołania, aby spowodować błąd w przypadku niepowodzenia polecenia? To znaczy: #! / Bin / bash -e?
Tim Bird

14

Możesz także użyć wbudowanego programu wywołującego bash:

#!/bin/bash

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

trap err_report ERR

echo hello | grep foo

wypisuje także nazwę pliku:

$ ./test.sh
errexit on line 9 ./test.sh

6

Naprawdę podoba mi się odpowiedź udzielona przez @Mat powyżej. Opierając się na tym, napisałem małego pomocnika, który daje nieco więcej kontekstu dla błędu:

Możemy sprawdzić skrypt pod kątem linii, która spowodowała błąd:

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

Oto w małym skrypcie testowym:

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

Po uruchomieniu otrzymujemy:

$ /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

Byłoby to jeszcze lepsze wykorzystanie $(caller)danych do podania kontekstu, nawet jeśli błąd nie jest w bieżącym skrypcie, ale jednym z jego importów. Ale bardzo miło!
tricasse

2

Zainspirowany inną odpowiedzią, oto prostsza kontekstowa procedura obsługi błędów:

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

W razie potrzeby możesz także użyć awk zamiast ogona i głowy .


1
istnieje powód, dla którego druga odpowiedź zapewnia kontekst za pomocą 3 linii powyżej i 3 linii poniżej linii naruszającej - co jeśli błąd pochodzi z linii kontynuacji?
iruvar

@iruvar to zrozumiałe, ale nie potrzebuję żadnego dodatkowego kontekstu; jedna linia kontekstu jest tak prosta, jak to tylko możliwe, i tak wystarczająca, jak potrzebuję
sanmai

Ok mój przyjacielu, + 1
iruvar

1

Oto kolejna wersja, zainspirowana @sanmai i @unpythonic. Pokazuje wiersze skryptu wokół błędu, z numerami linii i statusem wyjścia - używając tail & head, ponieważ wydaje się to prostsze niż rozwiązanie awk.

Pokazuje to jako dwa wiersze dla czytelności - możesz połączyć te wiersze w jeden, jeśli wolisz (zachowując ;):

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

Działa to całkiem dobrze z set -euo pipefail( nieoficjalnym trybem ścisłym ) - każdy niezdefiniowany błąd zmiennej podaje numer linii bez odpalenia ERRpseudo-sygnału, ale inne przypadki pokazują kontekst.

Przykładowe dane wyjściowe:

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)

0

Czy można uzyskać informację, z której linii wysłano sygnał ERR?

Tak, LINENOa BASH_LINENOzmienne są kolacją przydatną do uzyskania linii awarii i linii, które do niej prowadzą.

A może źle to rozumiem?

Nie, po prostu brakuje -qopcji z grep ...

echo hello | grep -q "asdf"

... Z -qopcją grepwróci 0za truei 1za false. A w Bash to trapnie jest Trap...

trap "_func" ERR

... Potrzebuję natywnego rozwiązania ...

Oto traper, który może się przydać do debugowania rzeczy o bardziej złożonej cykliczności ...

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

... i przykładowy skrypt użycia do ujawnienia subtelnych różnic w ustawianiu powyższej pułapki do śledzenia funkcji również ...

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'

Powyższe testy zostały przetestowane na Bash w wersji 4+, więc zostaw komentarz, jeśli coś jest potrzebne w wersjach wcześniejszych niż cztery, lub otwórz problem, jeśli nie uda mu się uchwycić błędów w systemach z wersją minimum czterech.

Główne dania na wynos to ...

set -E -o functrace
  • -Epowoduje powiększanie się błędów w funkcjach

  • -o functrace powoduje pozwala na większą gadatliwość, gdy coś w funkcji zawiedzie

trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
  • Pojedyncze cudzysłowy są używane wokół wywołania funkcji, a podwójne cudzysłowy wokół poszczególnych argumentów

  • Odwołania do LINENOi BASH_LINENOsą przekazywane zamiast bieżących wartości, chociaż może to zostać skrócone w późniejszych wersjach programu powiązanego z pułapką, tak że końcowa linia awarii powoduje jej wyjście

  • Wartości BASH_COMMANDi status exit ( $?) są przekazywane, po pierwsze, aby uzyskać polecenie, które zwróciło błąd, a po drugie, aby upewnić się, że pułapka nie wyzwala się w stanach innych niż błąd

Podczas gdy inni mogą się nie zgodzić, uważam, że łatwiej jest zbudować tablicę wyjściową i użyć printf do wydrukowania każdego elementu tablicy we własnej linii ...

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

... także >&2bit na końcu powoduje, że błędy idą tam, gdzie powinny (błąd standardowy), i pozwala uchwycić tylko błędy ...

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

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

Jak pokazują te i inne przykłady dotyczące przepełnienia stosu, istnieje wiele sposobów na zbudowanie pomocy do debugowania przy użyciu wbudowanych narzędzi.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.