Linux bashスクリプトでエラーをキャッチする方法は?


13

次のスクリプトを作成しました。

# !/bin/bash

# OUTPUT-COLORING
red='\e[0;31m'
green='\e[0;32m'
NC='\e[0m' # No Color

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    cd $1
    if [ $? = 0 ]
            then
                    echo -e "${green}$1${NC}"
            else
                    echo -e "${red}$1${NC}"
    fi
}

# EXE
directoryExists "~/foobar"
directoryExists "/www/html/drupal"

スクリプトは動作しますが、エコーのほかに、次の場合の出力もあります

cd $1

実行に失敗します。

testscripts//test_labo3: line 11: cd: ~/foobar: No such file or directory

これをキャッチすることは可能ですか?


参考までに、これをもっと簡単に行うこともできます。test -d /path/to/directory(または[[ -d /path/to/directory ]]bashで)指定されたターゲットがディレクトリかどうかを通知し、静かにそれを行います。
パトリック

@Patrickは、それがディレクトリであるかどうかをテストするだけであり、ディレクトリにアクセスできるかどうかはテストしませんcd
ステファンシャゼラス

@StephaneChazelasはい。関数名はdirectoryExistsです。
パトリック

回答:


8

スクリプトは実行時にディレクトリを変更します。つまり、一連の相対パス名では動作しません。その後、を使用する機能ではなく、ディレクトリの存在のみを確認したいので、後でコメントした cdので、回答はまったく使用する必要はありませんcd。改訂。の使用tput と色man terminfo

#!/bin/bash -u
# OUTPUT-COLORING
red=$( tput setaf 1 )
green=$( tput setaf 2 )
NC=$( tput setaf 0 )      # or perhaps: tput sgr0

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    # was: do the cd in a sub-shell so it doesn't change our own PWD
    # was: if errmsg=$( cd -- "$1" 2>&1 ) ; then
    if [ -d "$1" ] ; then
        # was: echo "${green}$1${NC}"
        printf "%s\n" "${green}$1${NC}"
    else
        # was: echo "${red}$1${NC}"
        printf "%s\n" "${red}$1${NC}"
        # was: optional: printf "%s\n" "${red}$1 -- $errmsg${NC}"
    fi
}

(テキスト内のエスケープシーケンスに作用する可能性printfのある問題の代わりに、より脆弱では echoないものを使用するように編集されています。)


また、ファイル名にバックスラッシュ文字が含まれる場合の問題も修正されます(xpg_echoがオンでない場合)。
ステファンシャゼル

12

set -eエラー終了モードを設定するために使用します。単純なコマンドがゼロ以外のステータス(失敗を示す)を返す場合、シェルは終了します。

それは用心set -e常にでキックしません。テスト位置のコマンドが失敗することが許可されている(例えばif failing_commandfailing_command || fallback)。サブシェル内のコマンドは、サブシェルを終了するだけで、親set -e; (false); echo fooは表示しませんfoo

代わりに、またはそれに加えて、bash(およびkshおよびzsh、プレーンshではない)で、コマンドがERRトラップなどでゼロ以外のステータスを返す場合に実行されるコマンドを指定できますtrap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR。のような(false); …場合、ERRトラップはサブシェルで実行されるため、親を終了させることはできません。


最近、私は少し実験して、||動作を修正する便利な方法を発見しました。これにより、トラップを使用せずに適切なエラー処理を簡単に行うことができます。私の答えをご覧ください。その方法についてどう思いますか?
-skozin

@ sam.kozinあなたの答えを詳細にレビューする時間はありません。原則としては良さそうです。移植性とは別に、ksh / bash / zshのERRトラップに勝る利点は何ですか?
ジル 'SO-悪である停止

機能を実行する前に設定された別のトラップを上書きするリスクがないため、おそらく唯一の利点は構成可能性です。これは、後で他のスクリプトからソースを作成して使用する一般的な関数を記述するときに便利な機能です。もう1つの利点は、POSIXとの完全な互換性ERRですが、すべての主要なシェルで擬似信号がサポートされているため、それほど重要ではありません。レビューをありがとう!=)
skozin

@ sam.kozin以前のコメントを書くのを忘れていました。これをCode Reviewに投稿し、チャットルームにリンクを投稿することをお勧めします。
ジル 'SO-悪であるのをやめる'

提案をありがとう、私はそれに従うようにしてみます。コードレビューについて知りませんでした。
-skozin

5

@Gillesの答えを拡張するには:

確かに、サブシェルで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をjustに置き換えます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、コマンドがサブシェルで実行されるため、渡すコマンドから行われたシェル変数のすべての変更が呼び出し関数に反映されないことです。


2

catch---報告して続行することで、正確に何を意味するかを言わない。さらに処理を中止しますか?

以来cd戻って失敗した場合に非ゼロの状態、あなたが行うことができます:

cd -- "$1" && echo OK || echo NOT_OK

失敗すると終了するだけです:

cd -- "$1" || exit 1

または、独自のメッセージをエコーし​​て終了します。

cd -- "$1" || { echo NOT_OK; exit 1; }

および/またはcd失敗時に提供されるエラーを抑制します。

cd -- "$1" 2>/dev/null || exit 1

標準では、コマンドはエラーメッセージをSTDERR(ファイル記述子2)に配置する必要があります。したがって2>/dev/null、STDERRはによって知られている「ビットバケット」にリダイレクトし/dev/nullます。

(変数を引用符で囲み、オプションの終わりをマークすることを忘れないでくださいcd)。


@Stephane Chazelasは、オプションの終わりを引用して通知するポイントをよく取り入れています。編集していただきありがとうございます。
JRFerguson

1

実際、あなたの場合、ロジックを改善できると思います。

cdの代わりに存在するかどうかを確認し、存在するかどうかを確認してからディレクトリに移動します。

if [ -d "$1" ]
then
     printf "${green}${NC}\\n" "$1"
     cd -- "$1"
else 
     printf "${red}${NC}\\n" "$1"
fi  

しかし、もしあなたの目的が可能性のあるエラーを沈黙cd -- "$1" 2>/dev/nullさせることであるなら、しかしこれはあなたを将来デバッグするのをより難しくします。ifテストフラグは次の場所で確認できます:Bash if documentation


この回答では、$1変数を引用できず、その変数に空白またはその他のシェルメタキャラクターが含まれていると失敗します。また、ユーザーにアクセス許可があるかどうかのチェックも失敗しますcd
イアンD.アレン

実際には、特定のディレクトリが存在するかどうかを確認しようとしていましたが、必ずしもそこにcdする必要はありません。しかし、私はよく知らなかったので、CDにしようとすると、存在しない場合はエラーを引き起こすと考えたので、なぜそれをキャッチしないのですか?if [-d $ 1]がまさに私が必要としていることを知りませんでした。どうもありがとうございます!(私はJavaのプログラミングに慣れており、ifステートメントのディレクトリのチェックはJavaでまったく一般的ではありません)
トーマスデワイルド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.