Bashでのエラー処理


239

Bashでエラーを処理するためのお気に入りの方法は何ですか?私がWebで見つけたエラー処理の最も良い例は、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スクリプトで使用するより良いエラー処理ルーチンはありますか?


1
この詳細な回答を参照してください:Bashスクリプトでエラーを発生させます
codeforester

1
ログとエラー処理の実装については、こちらをご覧ください:github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

回答:


154

トラップを使用してください!

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で終了し、明示的なメッセージが表示されます。


4
@draemon変数の大文字は意図的なものです。オールキャップスは、シェルのビルトインと環境変数の場合にのみ従来型です。他のすべてに小文字を使用すると、名前空間の競合を防止できます。stackoverflow.com/questions/673055/…
Charles Duffy

1
もう一度壊す前に、変更をテストします。規約は良いことですが、機能するコードの二次的なものです。
ドラえもん

3
@Draemon、私は実際には同意しません。明らかに壊れたコードは気づかれ、修正されます。悪い習慣ですが、ほとんど機能するコードは永遠に存続します(そして伝播します)。
Charles Duffy

1
気づかなかった 機能しているコードが主な関心事であるため、壊れたコードに気づきます。
ドラえもん14

5
それは完全に無料ではありません(stackoverflow.com/a/10927223/26334)。コードがすでにPOSIXと互換性がない場合、functionキーワードを削除してもPOSIX shで実行できなくなりますが、私の主なポイントは、 ve(IMO)は、set -eの使用を推奨することを弱めることにより、回答の価値を下げました。Stackoverflowは「あなたの」コードではなく、最良の答えを得ることです。
ドラえもん2014

123

それは素晴らしい解決策です。追加したかっただけ

set -e

初歩的なエラーメカニズムとして。単純なコマンドが失敗した場合、スクリプトはすぐに停止します。私はこれがデフォルトの動作であるはずだと思います。そのようなエラーはほとんど常に予期しないことを示しているため、次のコマンドを実行し続けることは実際には「常識」ではありません。


29
set -e落とし穴がないわけではありません。いくつかについては、mywiki.wooledge.org / BashFAQ / 105を参照してください。
Charles Duffy

3
@CharlesDuffy、落とし穴のいくつかは克服できますset -o pipefail
ホブ

7
@CharlesDuffy落とし穴を指摘してくれてありがとう。全体的には、私はまだset -e高い費用便益比を持っていると思います。
Bruno De Fraine 2012

3
@BrunoDeFraine私はset -e自分自身を使用していますが、irc.freenode.org#bashの他の多くの常連がそれに対して(非常に強い意味で)アドバイスしています。少なくとも、問題の問題はよく理解されているはずです。
Charles Duffy

3
set -e -o pipefail -u#そして、あなたが何をしているのかを知っています
Sam Watkins

78

このページのすべての答えを読んで、私はたくさんの刺激を受けました。

だから、これが私のヒントです:

ファイルの内容: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!


以下のスクリーンショットからわかるように、出力は色分けされ、エラーメッセージは使用言語で表示されます。

ここに画像の説明を入力してください


3
これはすごいです.. githubプロジェクトを作成して、人々が簡単に改善を加えて貢献できるようにする必要があります。私はこれをlog4bashと組み合わせて、優れたbashスクリプトを作成するための強力な環境を作成します。
ドミニクドーン

1
FYI- test ${#g_libs[@]} == 0POSIXに準拠していません(POSIXテストは=文字列比較または-eq数値比較をサポートしてい==ますが、POSIXに配列がないことは言うまでもありません)。POSIX準拠を試みていない場合は、なぜtest数学のコンテキストではなく、世界を使用していますか?(( ${#g_libs[@]} == 0 ))結局のところ、読みやすいです。
Charles Duffy

2
@ルカ-これは本当に素晴らしいです!あなたの写真は私にこれの私自身の実装を作成するように促しました。以下の私の回答にそれを掲載しました。
niieani

3
ブラビッシモ!! これは、スクリプトをデバッグするための優れた方法です。Grazie mille追加したのは、次のようなOS Xのチェックだけでした case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
。– SaxDaddy

1
少し恥知らずな自己プラグインですが、このスニペットを取り、クリーンアップし、機能を追加し、出力フォーマットを改善し、POSIX互換性を高めました(LinuxとOSXの両方で動作します)。これは、GithubのPrivex ShellCoreの一部として公開されています:github.com/Privex/shell-core
Someguy123

22

「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のように)迅速で効果的です。


1
$(foo)単なるものでfooはなく、裸のラインで使用することは、通常は間違っていることです。例としてそれを宣伝するのはなぜですか?
Charles Duffy

20

ここで紹介するアイデアに触発されて、私はbashボイラープレートプロジェクトの bashスクリプトのエラーを処理する、読みやすく便利な方法を開発しました。

単純にライブラリを調達することで、次のものがすぐに利用できます(つまりset -etrapon ERRbash-fuを使用しているかのように、エラーが発生すると実行が停止します)。

bash-oo-frameworkエラー処理

エラーを処理するのに役立つ追加機能がいくつかあります。たとえば、try and catchthrowキーワードなど、ある時点で実行を中断してバックトレースを確認することができます。さらに、ターミナルがサポートしている場合は、電力線の絵文字を出力し、読みやすいように出力の一部に色を付け、コード行のコンテキストで例外の原因となったメソッドを強調します。

欠点は、移植性がないことです。コードはbashで機能し、おそらく> = 4のみです(ただし、bash 3を作成するために多少の労力をかけて移植できると思います)。

コードは処理を改善するために複数のファイルに分割されていますが、Luca Borrioneによる上記の回答からバックトレースのアイデアに触発されました。

詳細を確認するか、ソースを確認するには、GitHubを参照してください。

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw


これは、Bashオブジェクト指向フレームワークプロジェクト内にあります。...幸いなことに、LOCは7.4kしかありません(GLOCによると)。OOP-オブジェクト指向の苦痛?
ingyhere

@ingyhereは非常にモジュール化されている(そして削除に適した)ため、例外部分を使用できるのは、それが目的の場合のみです;)
niieani

11

本当に電話しやすいものが好きです。少し複雑に見えますが、使いやすいものを使用しています。通常は、以下のコードをコピーしてスクリプトに貼り付けます。コードの後に​​説明があります。

#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

2
「エイリアスを明示的に許可する必要があります」という文を拡張できますか?予期しない動作が発生するのではないかと心配しています。小さな影響で同じことを達成する方法はありますか?
2015

必要ない$LINENO - 1です。それなしで正しく表示します。
kyb 2018

bashとzshでの短い使用例false || die "hello death"
kyb 2018

6

別の考慮事項は、返す終了コードです。「1」だけがかなり標準ですが、bash自体が使用する予約済みの終了コードがいくつかあります。その同じページでは、ユーザー定義コードはC / C ++標準に準拠するには64〜113の範囲にある必要があると主張しています。

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コードを一緒に使用すると、スクリプトで複数の同時エラーを通知できます。


4

次のトラップコードを使用します。これにより、パイプと「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

5
このfunctionキーワードは不必要にPOSIX互換ではありません。前に宣言せずにerror() {、宣言を行うことを検討してくださいfunction
Charles Duffy

5
${$?}ただべきである$?、または${?}あなたが不要な括弧を使う、という場合。インナー$は間違っています。
Charles Duffy

3
@CharlesDuffyまでに、POSIXは不当にGNU / Linuxと互換性がありません(それでも、私はあなたの
主張に従い

3

私は使いました

die() {
        echo $1
        kill $$
}

前; 何かの理由で「exit」が失敗していたからだと思います。ただし、上記のデフォルトは良い考えのようです。


STDERRにエラーメッセージを送信するほうがいいでしょう。
ankostis

3

これはしばらくの間私によく役立ちました。エラーまたは警告メッセージをパラメーターごとに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}
}

3

これがあなたに役立つかどうかはわかりませんが、エラー(前のコマンドの終了コード)のチェックを含めるために、ここで提案された関数の一部を変更しました。各「チェック」で、ロギングの目的でエラーの内容の「メッセージ」もパラメーターとして渡します。

#!/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を行います)


2
関数を定義するためにPOSIX構文の使用を検討してください- functionキーワードではなく、単にerror_exit() {
Charles Duffy

2
ただやらない理由はありますcd /home/myuser/afolder || error_exit "Unable to switch to folder"か?
Pierre-Olivier Vares 14

@ Pierre-OlivierVares ||を使用しない理由は特にありません。これは既存のコードの抜粋であり、関係する各行の後に「エラー処理」行を追加しました。一部は非常に長く、別の(即時)ラインに配置したほうがきれいでした
ネルソンロドリゲス

しかし、クリーンなソリューションのように見えますが、シェルチェックは不平を言っています:github.com/koalaman/shellcheck/wiki/SC2181
mhulse

1

このトリックは、不足しているコマンドや関数に役立ちます。欠落している関数(または実行可能ファイル)の名前は$ _で渡されます

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

$_同じ機能では利用できません$?か?関数で一方を使用し、もう一方を使用する理由がないかどうかはわかりません。
ingyhere

1

この機能は最近、かなりよく機能しています。

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

コマンドのいずれかが失敗した場合、エラーコードは単にブロックの最後に渡されます。以前のコマンドが失敗した場合に後続のコマンドを実行したくないが、スクリプトがすぐに終了しないようにする場合(たとえば、ループ内)に便利です。


0

トラップの使用は常にオプションであるとは限りません。たとえば、エラー処理を必要とし、任意のスクリプトから呼び出せる(ヘルパー関数でファイルをソースした後)ある種の再利用可能な関数を記述している場合、その関数は外部スクリプトの終了時間について何も想定できません。トラップの使用が非常に難しくなります。トラップを使用することのもう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

ただし、||演算子は、クリーンアップの前に外部関数から戻るのを防ぐために必要です。秘訣は、内部コマンドをバックグラウンドで実行し、すぐにそれを待つことです。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、コマンドがサブシェルで実行されるため、渡したコマンドから実行されたシェル変数のすべての変更が呼び出し元の関数に伝達されないことです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.