Пастка, ERR та повторення рядка помилки


30

Я намагаюся створити деякі повідомлення про помилки за допомогою Trap для виклику функції щодо всіх помилок:

Trap "_func" ERR

Чи можливо отримати, з якої лінії був відправлений сигнал ERR? Шкаралупа баш.

Якщо я це роблю, я можу прочитати та повідомити, яка команда була використана, і ввійти / виконати деякі дії.

А може я йду на це все неправильно?

Я перевірив наступне:

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

echo hello | grep "asdf"

І $LINENOповертається 2. Не працює.


Ви можете подивитися сценарій налагодження bash bashdb. Здається, що перший аргумент trapможе містити змінні, які оцінюються в бажаному контексті. Так trap 'echo $LINENO' ERR'має працювати.
доношено успішно

хм, просто спробував це з поганим відлунням | команда grep, і вона повертає рядок оператора Trap. Але я погляну на bashdb
Mechaflash

Мені дуже шкода ... Я не вказав у своєму первісному запитанні, що мені потрібно рідне рішення. Я редагував питання.
Mechaflash

До жаль, я BORKED приклад рядка: trap 'echo $LINENO' ERR. Перший аргумент trap- це весь echo $LINENOтвердий текст. Це в басі.
доношено успішно

5
@Mechaflash Це повинно бути trap 'echo $LINENO' ERRз одинарними цитатами, а не подвійними лапками. Команда, яку ви написали, $LINENOрозширюється, коли рядок 2 розбирається, тому пастка є echo 2(точніше ECHO 2, яка б виводила bash: ECHO: command not found).
Жил "ТАК - перестань бути злим"

Відповіді:


61

Як зазначалося в коментарях, ваше цитування неправильне. Вам потрібні одиничні лапки, щоб не $LINENOрозширюватися при першому розборі лінії пастки.

Це працює:

#! /bin/bash

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

trap 'err_report $LINENO' ERR

echo hello | grep foo  # This is line number 9

Запуск:

 $ ./test.sh
 Error on line 9

дякую за приклад з викликом функції. Я не знав, що подвійні лапки розширюють змінну в цьому випадку.
Мечафлаш

echo hello | grep fooМабуть, не кидає для мене помилки. Я щось нерозумію?
геотеорія

@geotheory У моїй системі grepє статус виходу 0, якщо була відповідність, 1 якщо не було відповідності та> 1 для помилки. Ви можете перевірити поведінку вашої системи за допомогоюecho hello | grep foo; echo $?
Патріка

Ні, ти не прав, це помилка :)
geotheory

Чи не потрібно використовувати -e у рядку виклику, щоб викликати помилку при відмові команди? Тобто: #! / Bin / bash -e?
Тім Берд

14

Ви також можете використовувати bash вбудований 'calller':

#!/bin/bash

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

trap err_report ERR

echo hello | grep foo

він також друкує ім'я файлу:

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

7

Мені дуже подобається відповідь, подана вище @Mat. Спираючись на це, я написав трохи помічника, який дає трохи більше контексту для помилки:

Ми можемо перевірити сценарій для рядка, який спричинив збій:

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

Ось це в невеликому тестовому сценарії:

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

Коли ми запускаємо його, ми отримуємо:

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

Це було б ще краще, використовуючи $(caller)дані, щоб дати контекст, навіть якщо відмова не в поточному сценарії, а в одному з його імпортів. Дуже приємно, хоча!
tricasse

2

Натхненний іншою відповіддю, ось простіший обробник контекстних помилок:

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

Ви також можете використовувати awk замість хвоста та голови, якщо потрібно.


1
є причина, що інша відповідь надає контекст за допомогою 3-х рядків нагорі та 3-х рядків нижче рядка-порушника - що робити, якщо помилка походить від рядка продовження?
iruvar

@iruvar це зрозуміло, але мені не потрібен жоден додатковий контекст; один рядок контексту настільки ж простий, як це стає, і настільки достатній, як мені потрібно
sanmai

Ok мій друг, + 1
iruvar

1

Ось ще одна версія, натхненна @sanmai та @unpythonic. Він показує рядки сценарію навколо помилки, з номерами рядків та статусом виходу - використовуючи хвіст і голову, як здається, простіше, ніж рішення awk.

Показуючи це як два рядки для читабельності - ви можете приєднати ці рядки до одного, якщо хочете (зберігши ;):

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

Це дуже добре працює set -euo pipefail( неофіційний суворий режим ) - будь-яка невизначена помилка змінної дає рядковий номер без запуску ERRпсевдосигналу, але інші випадки показують контекст.

Приклад виводу:

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

Чи можливо отримати, з якої лінії був відправлений сигнал ERR?

Так, LINENOі BASH_LINENOзмінні вечері корисні для отримання лінії відмови та рядків, які ведуть до неї.

А може я йду на це все неправильно?

Ні, просто відсутній -qваріант із грепом ...

echo hello | grep -q "asdf"

... З -qопцією grepповернемось 0за trueі 1за false. А в Bash це trapне так Trap...

trap "_func" ERR

... Мені потрібно рідне рішення ...

Ось ловець, який вам може бути корисним для налагодження речей, які мають трохи більш цикломатичну складність ...

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

... і приклад сценарію використання для виявлення тонких відмінностей у тому, як встановити вищевказану пастку для відстеження функцій ...

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'

Викладене вище, де тестовано на Bash версії 4+, тому залиште коментар, якщо потрібні щось для версій до чотирьох, або Відкрийте випуск, якщо не вдасться усунути збої в системах з мінімальною версією чотирьох.

Основні вивезення - це ...

set -E -o functrace
  • -Eспричиняє помилки в функціях, що збільшуються

  • -o functrace Причини дозволяють отримати більше багатослівностей, коли щось у функції не вдається

trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
  • Одиничні лапки використовуються навколо виклику функції, а подвійні лапки - навколо окремих аргументів

  • Посилання на LINENOта BASH_LINENOпередаються замість поточних значень, хоча це може бути скорочено в пізніших версіях, пов’язаних з пасткою, таким чином, що кінцева лінія відмови робить її вихідною

  • Значення BASH_COMMANDта статус виходу ( $?) передаються, по-перше, щоб отримати команду, яка повернула помилку, а по-друге, для забезпечення того, щоб пастка не спрацьовувала в статусах без помилок

І хоча інші можуть не погодитися, я вважаю, що простіше створити вихідний масив і використовувати printf для друку кожного елемента масиву на його власному рядку ...

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

... також >&2біт в кінці призводить до помилок, куди слід (стандартна помилка), і дозволяє фіксувати лише помилки ...

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

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

Як показано в цих та інших прикладах на переповнення стека, існує безліч способів побудувати допомогу для налагодження за допомогою вбудованих утиліт.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.