web-dev-qa-db-ja.com

トラップ、ERR、およびエラー行のエコー

トラップを使用してすべてのエラーで関数を呼び出すエラーレポートを作成しようとしています。

Trap "_func" ERR

ERR信号が送信されたラインを取得することは可能ですか?シェルはbashです。

これを行うと、使用されたコマンドを読み取って報告し、いくつかのアクションをログに記録または実行できます。

それとも私はこれですべて間違っているのでしょうか?

私は以下でテストしました:

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

echo hello | grep "asdf"

そして$LINENOは2を返します。機能しません。

32
Mechaflash

コメントで指摘されているように、あなたの引用は間違っています。トラップ行が最初に解析されるときに$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
66
Mat

Bash組み込みの「呼び出し側」を使用することもできます。

#!/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
17
Andrew Ivanov

上記の@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
8
unpythonic

ERR信号が送信されたラインを取得することは可能ですか?

はい、LINENO変数とBASH_LINENO変数は、失敗の行とそれにつながる行を取得するのに便利です。

それとも私はこれですべて間違っているのでしょうか?

いいえ、grepの-qオプションがありません...

echo hello | grep -q "asdf"

...-qオプションを使用すると、grep0 fortrueを返しますおよび1 forfalse。そして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+でテストされているため、4より前のバージョンが必要な場合はコメントを残します。最小バージョンが4のシステムで障害をトラップできない場合は Open Issue を使用します。

主なテイクアウェイは...

set -E -o functrace
  • -E関数内でエラーが発生し、bubble up

  • -o functraceを使用すると、関数内の何かが失敗したときに、より多くの冗長性が可能になります

trap 'failure "LINENO" "BASH_LINENO" "${BASH_COMMAND}" "${?}"' ERR
  • 一重引用符は関数呼び出しの周りに使用され、二重引用符は個々の引数の周りに使用されます

  • LINENOおよびBASH_LINENOへの参照が現在の値の代わりに渡されますが、これはトラップにリンクされた以降のバージョンでは短縮され、最終的な障害ラインが出力に含まれるようになりました

  • BASH_COMMANDと終了ステータス($?)の値が渡され、最初に次のコマンドを取得しますエラーを返し、エラー以外のステータスでトラップがトリガーされないようにするための2番目

そして、他の人は同意しないかもしれませんが、出力配列を構築し、それ自体の行に各配列要素を出力するために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

スタックオーバーフローでこれらと 他の例 に示されているように、組み込みユーティリティを使用してデバッグ支援を構築する方法はたくさんあります。

2
S0AndS0

他の答えに触発されて、これがより簡単なコンテキストエラーハンドラです。

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

また、必要に応じて tail&headの代わりにawkを使用 することもできます。

2
sanmai

@sanmaiと@unpythonicに触発された別のバージョンがあります。エラーの周りのスクリプト行と、行番号、および終了ステータスが表示されます。これは、awkソリューションよりもシンプルに見えるため、tail&headを使用しています。

読みやすくするために、ここではこれを2行で示しています。必要に応じて、これらの行を1行に結合できます(;):

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

これはset -euo pipefailnofficial strict mode )-未定義の変数エラーは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)
1
RichVel