Bashでエラーを処理するお気に入りの方法は何ですか?私がウェブ上で見つけたエラーを処理する最良の例は、William Shotts Jrが http://www.linuxcommand.org で書いたものです。
彼は、Bashのエラー処理に次の関数を使用することを提案しています。
#!/bin/bash
# A slicker error handling routine
# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run. You can get this
# value from the first item on the command line ($0).
# Reference: This was copied from <http://www.linuxcommand.org/wss0150.php>
PROGNAME=$(basename $0)
function error_exit
{
# ----------------------------------------------------------------
# Function for exit due to fatal program error
# Accepts 1 argument:
# string containing descriptive error message
# ----------------------------------------------------------------
echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
exit 1
}
# Example call of the error_exit function. Note the inclusion
# of the LINENO environment variable. It contains the current
# line number.
echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."
Bashスクリプトで使用するより良いエラー処理ルーチンがありますか?
トラップを使用してください!
tempfiles=( )
cleanup() {
rm -f "${tempfiles[@]}"
}
trap cleanup 0
error() {
local parent_lineno="$1"
local message="$2"
local code="${3:-1}"
if [[ -n "$message" ]] ; then
echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
else
echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
fi
exit "${code}"
}
trap 'error ${LINENO}' ERR
...その後、一時ファイルを作成するたびに:
temp_foo="$(mktemp -t foobar.XXXXXX)"
tempfiles+=( "$temp_foo" )
$temp_foo
は終了時に削除され、現在の行番号が出力されます。 (set -e
は同様にエラー終了時の動作を提供します 深刻な警告がありますが であり、コードの予測可能性と移植性を弱めます)。
トラップがerror
を呼び出すようにするか(この場合、デフォルトの終了コード1を使用し、メッセージなし)、または自分で呼び出して明示的な値を提供できます。例えば:
error ${LINENO} "the foobar failed" 2
ステータス2で終了し、明示的なメッセージを表示します。
それは素晴らしい解決策です。ただ追加したかった
set -e
初歩的なエラーメカニズムとして。単純なコマンドが失敗すると、スクリプトがすぐに停止します。これはデフォルトの動作だったはずだと思います。このようなエラーはほとんどの場合、予期しないものを意味するため、次のコマンドを実行し続けるのは本当に「正気」ではありません。
このページのすべての回答を読んで、私はたくさんのインスピレーションを受けました。
だから、ここに私のヒントがあります:
ファイルの内容:lib.trap.sh
lib_name='trap'
lib_version=20121026
stderr_log="/dev/shm/stderr.log"
#
# TO BE SOURCED ONLY ONCE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
if test "${g_libs[$lib_name]+_}"; then
return 0
else
if test ${#g_libs[@]} == 0; then
declare -A g_libs
fi
g_libs[$lib_name]=$lib_version
fi
#
# MAIN CODE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
set -o pipefail # trace ERR through pipes
set -o errtrace # trace ERR through 'time command' and other functions
set -o nounset ## set -u : exit the script if you try to use an uninitialised variable
set -o errexit ## set -e : exit the script if any statement returns a non-true return value
exec 2>"$stderr_log"
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: EXIT_HANDLER
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
function exit_handler ()
{
local error_code="$?"
test $error_code == 0 && return;
#
# LOCAL VARIABLES:
# ------------------------------------------------------------------
#
local i=0
local regex=''
local mem=''
local error_file=''
local error_lineno=''
local error_message='unknown'
local lineno=''
#
# PRINT THE HEADER:
# ------------------------------------------------------------------
#
# Color the output if it's an interactive terminal
test -t 1 && tput bold; tput setf 4 ## red bold
echo -e "\n(!) EXIT HANDLER:\n"
#
# GETTING LAST ERROR OCCURRED:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
#
# Read last file from the error log
# ------------------------------------------------------------------
#
if test -f "$stderr_log"
then
stderr=$( tail -n 1 "$stderr_log" )
rm "$stderr_log"
fi
#
# Managing the line to extract information:
# ------------------------------------------------------------------
#
if test -n "$stderr"
then
# Exploding stderr on :
mem="$IFS"
local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' )
IFS=':'
local stderr_parts=( $shrunk_stderr )
IFS="$mem"
# Storing information on the error
error_file="${stderr_parts[0]}"
error_lineno="${stderr_parts[1]}"
error_message=""
for (( i = 3; i <= ${#stderr_parts[@]}; i++ ))
do
error_message="$error_message "${stderr_parts[$i-1]}": "
done
# Removing last ':' (colon character)
error_message="${error_message%:*}"
# Trim
error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
fi
#
# GETTING BACKTRACE:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
_backtrace=$( backtrace 2 )
#
# MANAGING THE OUTPUT:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
local lineno=""
regex='^([a-z]{1,}) ([0-9]{1,})$'
if [[ $error_lineno =~ $regex ]]
# The error line was found on the log
# (e.g. type 'ff' without quotes wherever)
# --------------------------------------------------------------
then
local row="${BASH_REMATCH[1]}"
lineno="${BASH_REMATCH[2]}"
echo -e "FILE:\t\t${error_file}"
echo -e "${row^^}:\t\t${lineno}\n"
echo -e "ERROR CODE:\t${error_code}"
test -t 1 && tput setf 6 ## white yellow
echo -e "ERROR MESSAGE:\n$error_message"
else
regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$"
if [[ "$_backtrace" =~ $regex ]]
# The file was found on the log but not the error line
# (could not reproduce this case so far)
# ------------------------------------------------------
then
echo -e "FILE:\t\t$error_file"
echo -e "ROW:\t\tunknown\n"
echo -e "ERROR CODE:\t${error_code}"
test -t 1 && tput setf 6 ## white yellow
echo -e "ERROR MESSAGE:\n${stderr}"
# Neither the error line nor the error file was found on the log
# (e.g. type 'cp ffd fdf' without quotes wherever)
# ------------------------------------------------------
else
#
# The error file is the first on backtrace list:
# Exploding backtrace on newlines
mem=$IFS
IFS='
'
#
# Substring: I keep only the carriage return
# (others needed only for tabbing purpose)
IFS=${IFS:0:1}
local lines=( $_backtrace )
IFS=$mem
error_file=""
if test -n "${lines[1]}"
then
array=( ${lines[1]} )
for (( i=2; i<${#array[@]}; i++ ))
do
error_file="$error_file ${array[$i]}"
done
# Trim
error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
fi
echo -e "FILE:\t\t$error_file"
echo -e "ROW:\t\tunknown\n"
echo -e "ERROR CODE:\t${error_code}"
test -t 1 && tput setf 6 ## white yellow
if test -n "${stderr}"
then
echo -e "ERROR MESSAGE:\n${stderr}"
else
echo -e "ERROR MESSAGE:\n${error_message}"
fi
fi
fi
#
# PRINTING THE BACKTRACE:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
test -t 1 && tput setf 7 ## white bold
echo -e "\n$_backtrace\n"
#
# EXITING:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
test -t 1 && tput setf 4 ## red bold
echo "Exiting!"
test -t 1 && tput sgr0 # Reset terminal
exit "$error_code"
}
trap exit_handler EXIT # ! ! ! TRAP EXIT ! ! !
trap exit ERR # ! ! ! TRAP ERR ! ! !
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: BACKTRACE
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
function backtrace
{
local _start_from_=0
local params=( "$@" )
if (( "${#params[@]}" >= "1" ))
then
_start_from_="$1"
fi
local i=0
local first=false
while caller $i > /dev/null
do
if test -n "$_start_from_" && (( "$i" + 1 >= "$_start_from_" ))
then
if test "$first" == false
then
echo "BACKTRACE IS:"
first=true
fi
caller $i
fi
let "i=i+1"
done
}
return 0
使用例:
ファイルの内容:trap-test.sh
#!/bin/bash
source 'lib.trap.sh'
echo "doing something wrong now .."
echo "$foo"
exit 0
ランニング:
bash trap-test.sh
出力:
doing something wrong now ..
(!) EXIT HANDLER:
FILE: trap-test.sh
LINE: 6
ERROR CODE: 1
ERROR MESSAGE:
foo: unassigned variable
BACKTRACE IS:
1 main trap-test.sh
Exiting!
下のスクリーンショットからわかるように、出力は色付けされており、エラーメッセージは使用言語で表示されます。
「set -e」の代替案は次のとおりです。
set -o errexit
これにより、フラグの意味が「-e」よりもいくらか明確になります。
ランダムな追加:フラグを一時的に無効にし、(終了コードに関係なく実行を継続する)デフォルトに戻すには、単に使用します
set +e
echo "commands run here returning non-zero exit codes will not cause the entire script to fail"
echo "false returns 1 as an exit code"
false
set -e
これは、他の応答で言及された適切なエラー処理を排除しますが、迅速かつ効果的です(bashと同様)。
ここに提示されたアイデアに触発されて、私は bashボイラープレートプロジェクト でbashスクリプトのエラーを処理する読みやすく便利な方法を開発しました。
ライブラリをソースするだけで、次のようになります(つまり、trap
のERR
と、いくつかのbash-fuのおかげで、set -e
を使用しているかのように、エラーが発生すると実行を停止します) :
try and catchやthrowキーワードなど、エラーの処理に役立つ追加機能があります。これにより、バックトレースを確認するポイントで実行を中断できます。さらに、ターミナルがサポートしている場合、電力線の絵文字を吐き出し、出力の一部に色を付けて読みやすくし、コード行のコンテキストで例外を引き起こしたメソッドに下線を引きます。
欠点は-移植性がない-コードはbashで動作することで、おそらく4以上のみです(しかし、3をbashすることで移植できると思います)。
コードは処理を改善するために複数のファイルに分かれていますが、 上記のLuca Borrioneによる回答 のバックトレースのアイデアに触発されました。
詳細を読むか、ソースを見るには、GitHubを参照してください。
https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw
本当に簡単に電話できるものが好きです。だから、私は少し複雑に見えるものを使いますが、使いやすいです。通常、以下のコードをコピーしてスクリプトに貼り付けます。コードの後に説明があります。
#This function is used to cleanly exit any script. It does this displaying a
# given error message, and exiting with an error code.
function error_exit {
echo
echo "$@"
exit 1
}
#Trap the killer signals so that we can exit with a good message.
trap "error_exit 'Received signal SIGHUP'" SIGHUP
trap "error_exit 'Received signal SIGINT'" SIGINT
trap "error_exit 'Received signal SIGTERM'" SIGTERM
#Alias the function so that it will print a message with the following format:
#prog-name(@line#): message
#We have to explicitly allow aliases, we do this because they make calling the
#function much easier (see example).
shopt -s expand_aliases
alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"'
通常、クリーンアップ関数の呼び出しはerror_exit関数の横に配置しますが、これはスクリプトごとに異なるため、省略しました。トラップは一般的な終端信号をキャッチし、すべてがクリーンアップされることを確認します。エイリアスは本当の魔法をするものです。私はすべての失敗をチェックするのが好きです。だから、一般的に私はプログラムを「if!」タイプ文。行番号から1を引くことにより、エイリアスは障害が発生した場所を教えてくれます。また、電話するのは非常に簡単で、ほとんどばか証拠です。以下に例を示します(/ bin/falseを、これから呼び出すものに置き換えてください)。
#This is an example useage, it will print out
#Error prog-name (@1): Who knew false is false.
if ! /bin/false ; then
die "Who knew false is false."
fi
もう1つの考慮事項は、返される終了コードです。ほんの「-1
」がかなり標準ですが、少数の bashが使用する予約済みの終了コード があり、同じページでは、準拠するためにユーザー定義コードが64〜113の範囲内にある必要があると主張しています。 C/C++標準。
mount
が終了コードに使用するビットベクトルアプローチを検討することもできます。
0 success
1 incorrect invocation or permissions
2 system error (out of memory, cannot fork, no more loop devices)
4 internal mount bug or missing nfs support in mount
8 user interrupt
16 problems writing or locking /etc/mtab
32 mount failure
64 some mount succeeded
OR
-コードを一緒に使用すると、スクリプトで複数の同時エラーを通知できます。
次のトラップコードを使用します。また、エラーをパイプと「time」コマンドでトレースするも許可します
#!/bin/bash
set -o pipefail # trace ERR through pipes
set -o errtrace # trace ERR through 'time command' and other functions
function error() {
JOB="$0" # job name
LASTLINE="$1" # line of error occurrence
LASTERR="$2" # error code
echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}"
exit 1
}
trap 'error ${LINENO} ${?}' ERR
私は使用しました
die() {
echo $1
kill $$
}
前;何らかの理由で「終了」が失敗したからだと思います。ただし、上記のデフォルトは良いアイデアのようです。
これ は、しばらく私に役立っています。エラーまたは警告メッセージを赤でパラメーターごとに1行で出力し、オプションの終了コードを許可します。
# Custom errors
EX_UNKNOWN=1
warning()
{
# Output warning messages
# Color the output red if it's an interactive terminal
# @param $1...: Messages
test -t 1 && tput setf 4
printf '%s\n' "$@" >&2
test -t 1 && tput sgr0 # Reset terminal
true
}
error()
{
# Output error messages with optional exit code
# @param $1...: Messages
# @param $N: Exit code (optional)
messages=( "$@" )
# If the last parameter is a number, it's not part of the messages
last_parameter="${messages[@]: -1}"
if [[ "$last_parameter" =~ ^[0-9]*$ ]]
then
exit_code=$last_parameter
unset messages[$((${#messages[@]} - 1))]
fi
warning "${messages[@]}"
exit ${exit_code:-$EX_UNKNOWN}
}
これが役に立つかどうかはわかりませんが、エラーのチェック(前のコマンドからの終了コード)を含めるために、ここで提案する機能の一部を変更しました。また、各「チェック」で、ロギング目的のエラーの「メッセージ」をパラメーターとして渡します。
#!/bin/bash
error_exit()
{
if [ "$?" != "0" ]; then
log.sh "$1"
exit 1
fi
}
同じスクリプト内(またはexport -f error_exit
を使用している場合は別のスクリプト内)で呼び出すには、次のように関数の名前を記述し、パラメーターとしてメッセージを渡します。
#!/bin/bash
cd /home/myuser/afolder
error_exit "Unable to switch to folder"
rm *
error_exit "Unable to delete all files"
これを使用して、自動化されたプロセス用に非常に堅牢なbashファイルを作成できました。エラーが発生した場合は停止し、通知します(log.sh
がそれを行います)
この機能は最近かなりよく役立っています:
action () {
# Test if the first parameter is non-zero
# and return straight away if so
if test $1 -ne 0
then
return $1
fi
# Discard the control parameter
# and execute the rest
shift 1
"$@"
local status=$?
# Test the exit status of the command run
# and display an error message on failure
if test ${status} -ne 0
then
echo Command \""$@"\" failed >&2
fi
return ${status}
}
実行するコマンドの名前に0または最後の戻り値を追加して呼び出すことにより、エラー値を確認せずにコマンドをチェーンできます。これにより、このステートメントブロックは次のようになります。
command1 param1 param2 param3...
command2 param1 param2 param3...
command3 param1 param2 param3...
command4 param1 param2 param3...
command5 param1 param2 param3...
command6 param1 param2 param3...
これになります:
action 0 command1 param1 param2 param3...
action $? command2 param1 param2 param3...
action $? command3 param1 param2 param3...
action $? command4 param1 param2 param3...
action $? command5 param1 param2 param3...
action $? command6 param1 param2 param3...
<<<Error-handling code here>>>
コマンドのいずれかが失敗した場合、エラーコードはブロックの最後に単に渡されます。以前のコマンドが失敗した場合に後続のコマンドを実行したくないが、スクリプトをすぐに終了したくない場合(ループ内など)に便利です。
このトリックは、欠落しているコマンドまたは機能に役立ちます。欠落している関数(または実行可能ファイル)の名前は$ _に渡されます
function handle_error {
status=$?
last_call=$1
# 127 is 'command not found'
(( status != 127 )) && return
echo "you tried to call $last_call"
return
}
# Trap errors.
trap 'handle_error "$_"' ERR
トラップの使用は常にオプションではありません。たとえば、エラー処理を必要とし、任意のスクリプトから呼び出すことができる何らかの種類の再利用可能な関数を記述している場合(ヘルパー関数でファイルをソースした後)、その関数は外部スクリプトの終了時間について何も仮定できません。トラップの使用が非常に困難になります。トラップを使用するもう1つの欠点は、構成可能性が悪いことです。これは、呼び出し側チェーンの初期に設定されている可能性がある以前のトラップを上書きするリスクがあるためです。
トラップなしで適切なエラー処理を行うために使用できる小さなトリックがあります。他の回答からすでにご存じかもしれませんが、コマンドの中でset -e
演算子を使用すると、サブシェルで実行しても、コマンド内で||
は機能しません。たとえば、これは機能しません:
#!/bin/sh
# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer
set -e
outer() {
echo '--> outer'
(inner) || {
exit_code=$?
echo '--> cleanup'
return $exit_code
}
echo '<-- outer'
}
inner() {
set -e
echo '--> inner'
some_failed_command
echo '<-- inner'
}
outer
ただし、クリーンアップの前に外部関数から戻るのを防ぐには、||
演算子が必要です。秘Theは、バックグラウンドで内部コマンドを実行し、すぐにそれを待つことです。 wait
ビルトインは内部コマンドの終了コードを返しますが、現在は内部関数ではなくwait
の後に||
を使用しているため、set -e
は内部で適切に機能します。
#!/bin/sh
# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup
set -e
outer() {
echo '--> outer'
inner &
wait $! || {
exit_code=$?
echo '--> cleanup'
return $exit_code
}
echo '<-- outer'
}
inner() {
set -e
echo '--> inner'
some_failed_command
echo '<-- inner'
}
outer
このアイデアに基づいた汎用関数を次に示します。 local
キーワードを削除すると、すべてのPOSIX互換シェルで動作するはずです。つまり、すべてのlocal x=y
をx=y
だけに置き換えます。
# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
local cmd="$1"; shift
local exit_code=0
local e_was_set=1; if ! is_Shell_attribute_set e; then
set -e
e_was_set=0
fi
"$cmd" "$@" &
wait $! || {
exit_code=$?
}
if [ "$e_was_set" = 0 ] && is_Shell_attribute_set e; then
set +e
fi
if [ -n "$CLEANUP" ]; then
RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
return $?
fi
return $exit_code
}
is_Shell_attribute_set() { # attribute, like "x"
case "$-" in
*"$1"*) return 0 ;;
*) return 1 ;;
esac
}
使用例:
#!/bin/sh
set -e
# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh
main() {
echo "--> main: $@"
CLEANUP=cleanup run inner "$@"
echo "<-- main"
}
inner() {
echo "--> inner: $@"
sleep 0.5; if [ "$1" = 'fail' ]; then
oh_my_god_look_at_this
fi
echo "<-- inner"
}
cleanup() {
echo "--> cleanup: $@"
echo " RUN_CMD = '$RUN_CMD'"
echo " RUN_EXIT_CODE = $RUN_EXIT_CODE"
sleep 0.3
echo '<-- cleanup'
return $RUN_EXIT_CODE
}
main "$@"
例の実行:
$ ./so_3 fail; echo "exit code: $?"
--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
RUN_CMD = 'inner'
RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127
$ ./so_3 pass; echo "exit code: $?"
--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
RUN_CMD = 'inner'
RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0
このメソッドを使用する際に注意する必要があるのは、コマンドがサブシェルで実行されるため、run
に渡すコマンドから行われたシェル変数のすべての変更が呼び出し関数に伝播しないことです。