複数のコマンドのBash終了ステータスを効率的に確認する


260

'try'ステートメントのように、bash内にある、複数のコマンドのpipefailに似たものはありますか?私はこのようなことをしたいと思います:

echo "trying stuff"
try {
    command1
    command2
    command3
}

そして、いつでも、コマンドが失敗した場合は、ドロップしてそのコマンドのエラーをエコーアウトします。私は次のようなことをしたくありません:

command1
if [ $? -ne 0 ]; then
    echo "command1 borked it"
fi

command2
if [ $? -ne 0 ]; then
    echo "command2 borked it"
fi

など...または次のようなもの:

pipefail -o
command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3

私が信じている各コマンドの引数(間違っている場合は修正してください)が互いに干渉するからです。これらの2つの方法はひどく時間がかかり、厄介なように思えるので、ここではより効率的な方法を求めています。


2
見てみましょう非公式bashのstrictモードset -euo pipefail
Pablo A

1
@PabloBianchi set -e恐ろしいアイデアです。参照BashFAQの練習#105で実装(ちょうどそれが導入され、予期しないエッジケースの数、及び/又は異なるシェルとシェルのバージョン)との間の非互換性を示す比較を議論in-ulm.de/~mascheck/various/setを-e
Charles Duffy

回答:


274

コマンドを起動してテストする関数を作成できます。コマンドに設定されている環境変数を想定command1command2ています。

function mytest {
    "$@"
    local status=$?
    if (( status != 0 )); then
        echo "error with $1" >&2
    fi
    return $status
}

mytest "$command1"
mytest "$command2"

32
を使用しない$*でください。引数にスペースが含まれていると失敗します。"$@"代わりに使用してください。同様$1に、echoコマンドの引用符の中に入れます。
Gordon Davisson、2011年

82
またtest、組み込みのコマンドであるため、この名前は避けます。
John Kugelman、2011年

1
これは私が使った方法です。正直なところ、私は元の投稿では十分に明確ではなかったと思いますが、このメソッドを使用すると、独自の「テスト」関数を記述して、そこで実行するアクションに関連するエラーアクションを実行できますスクリプト。ありがとう:)
jwbensley

7
最後に実行されたコマンドが 'echo'だったため、エラーが発生した場合、test()によって返された終了コードは常に0を返しません。$の値を保存する必要があるかもしれませんか?最初。
magiconair

2
これは良い考えではなく、悪い習慣を助長します。の単純なケースを考えlsます。呼び出してls foo、フォームのエラーメッセージが表示されたls: foo: No such file or directory\n場合は、問題を理解しています。代わりにあなたがls: foo: No such file or directory\nerror with ls\nあなたを得るなら、あなたは余分な情報に気を取られます。この場合、過剰は取るに足らないことであると主張するのは簡単ですが、急速に増大します。簡潔なエラーメッセージは重要です。しかし、より重要なことに、このタイプのラッパーは、あまりにも多くのライターが適切なエラーメッセージを完全に省略することを奨励しています。
William Pursell 2017

185

「ドロップアウトしてエラーをエコーする」とはどういう意味ですか?コマンドが失敗したらすぐにスクリプトを終了したい場合は、次のようにします

set -e    # DON'T do this.  See commentary below.

スクリプトの開始時(ただし、以下の警告に注意してください)。エラーメッセージをエコーする必要はありません。失敗したコマンドで処理してください。つまり、次の場合:

#!/bin/sh

set -e    # Use caution.  eg, don't do this
command1
command2
command3

そして、command2が失敗し、エラーメッセージがstderrに出力されますが、希望どおりの結果が得られたようです。(私があなたが望むものを誤解しない限り!)

結果として、作成するコマンドはすべて正常に動作する必要があります。エラーはstdoutではなくstderrに報告する必要があり(質問のサンプルコードはエラーをstdoutに出力します)、失敗するとゼロ以外のステータスで終了する必要があります。

しかし、私はこれを良い習慣とは考えていません。 set -eは、bashの異なるバージョンでセマンティクスを変更しました。単純なスクリプトでは問題なく機能しますが、エッジケースが非常に多く、基本的に使用できません。(以下のようなことを考慮してください:set -e; foo() { false; echo should not print; } ; foo && echo ok ここのセマンティクスはある程度合理的ですが、オプション設定に依存してコードをリファクタリングして早期に終了させると、簡単に噛み付く可能性があります。)IMOの方が適切です:

 #!/bin/sh

 command1 || exit
 command2 || exit
 command3 || exit

または

#!/bin/sh

command1 && command2 && command3

1
このソリューションが最も簡単ですが、障害時にクリーンアップを実行できないことに注意してください。
Josh J

6
クリーンアップはトラップを使用して実行できます。(例:終了時trap some_func 0に実行さsome_funcれます)
William Pursell 2015年

3
また、errexit(set -e)のセマンティクスはbashの異なるバージョンで変更されており、関数の呼び出しやその他の設定中に予期せず動作することがよくあります。私はもはやその使用をお勧めしません。IMO、|| exit各コマンドの後に明示的に書くことをお勧めします。
ウィリアムパーセル2017年

87

Red Hatシステムで広範囲に使用する一連のスクリプト機能があります。彼らはシステム機能を使用して、/etc/init.d/functions[ OK ]と赤の[FAILED]ステータスインジケータを印刷します。

$LOG_STEPS失敗したコマンドをログに記録する場合は、オプションで変数をログファイル名に設定できます。

使用法

step "Installing XFS filesystem tools:"
try rpm -i xfsprogs-*.rpm
next

step "Configuring udev:"
try cp *.rules /etc/udev/rules.d
try udevtrigger
next

step "Adding rc.postsysinit hook:"
try cp rc.postsysinit /etc/rc.d/
try ln -s rc.d/rc.postsysinit /etc/rc.postsysinit
try echo $'\nexec /etc/rc.postsysinit' >> /etc/rc.sysinit
next

出力

Installing XFS filesystem tools:        [  OK  ]
Configuring udev:                       [FAILED]
Adding rc.postsysinit hook:             [  OK  ]

コード

#!/bin/bash

. /etc/init.d/functions

# Use step(), try(), and next() to perform a series of commands and print
# [  OK  ] or [FAILED] at the end. The step as a whole fails if any individual
# command fails.
#
# Example:
#     step "Remounting / and /boot as read-write:"
#     try mount -o remount,rw /
#     try mount -o remount,rw /boot
#     next
step() {
    echo -n "$@"

    STEP_OK=0
    [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$
}

try() {
    # Check for `-b' argument to run command in the background.
    local BG=

    [[ $1 == -b ]] && { BG=1; shift; }
    [[ $1 == -- ]] && {       shift; }

    # Run the command.
    if [[ -z $BG ]]; then
        "$@"
    else
        "$@" &
    fi

    # Check if command failed and update $STEP_OK if so.
    local EXIT_CODE=$?

    if [[ $EXIT_CODE -ne 0 ]]; then
        STEP_OK=$EXIT_CODE
        [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$

        if [[ -n $LOG_STEPS ]]; then
            local FILE=$(readlink -m "${BASH_SOURCE[1]}")
            local LINE=${BASH_LINENO[0]}

            echo "$FILE: line $LINE: Command \`$*' failed with exit code $EXIT_CODE." >> "$LOG_STEPS"
        fi
    fi

    return $EXIT_CODE
}

next() {
    [[ -f /tmp/step.$$ ]] && { STEP_OK=$(< /tmp/step.$$); rm -f /tmp/step.$$; }
    [[ $STEP_OK -eq 0 ]]  && echo_success || echo_failure
    echo

    return $STEP_OK
}

これは純金です。スクリプトの使用方法は理解していますが、各ステップを完全には把握していませんが、確かにbashスクリプトの知識の範囲外ですが、それでも芸術作品だと思います。
kingmilo 2015年

2
このツールには正式な名前がありますか?私は/ステップ/試みのこのスタイルに次のログをmanページを読むのが大好きです
ThorSummoner

これらのシェル関数はUbuntuでは使用できないようですか?私はこれを使いたいと思っていましたが、ポータブルなものでした
ThorSummoner '21 / 06/15

@ThorSummoner、これはUbuntuがSysV initの代わりにUpstartを使用しており、間もなくsystemdを使用するためです。RedHatは後方互換性を長期間維持する傾向があるため、init.dがまだ残っています。
dragon788

Johnのソリューションに拡張機能を投稿し、UbuntuなどのRedHat以外のシステムで使用できるようにしました。stackoverflow.com/a/54190627/308145を
マークトムソン

51

価値があるのは、各コマンドの成功を確認するコードを書く短い方法は次のとおりです。

command1 || echo "command1 borked it"
command2 || echo "command2 borked it"

それはまだ退屈ですが、少なくとも読みやすいです。


私が使った方法ではなく、これについては考えていませんでしたが、情報をありがとう:)
jwbensley

3
黙ってコマンドを実行し、同じことを達成するために:command1 &> /dev/null || echo "command1 borked it"
マット・バーン

私はこの方法のファンですが、ORの後に複数のコマンドを実行する方法はありますか?何かcommand1 || (echo command1 borked it ; exit)
AndreasKralj

38

代わりに&&、最初に失敗したコマンドが残りのコマンドの実行を防ぐように、コマンドを結合するだけです。

command1 &&
  command2 &&
  command3

これは質問で求めた構文ではありませんが、説明するユースケースの一般的なパターンです。一般的に、コマンドは失敗の印刷を担当する必要があるため、手動で実行する必要はありません(-qエラーが発生したくない場合にエラーを非表示にするフラグを使用する場合があります)。これらのコマンドを変更できる場合は、他のコマンドでラップするのではなく、失敗したときに叫ぶように編集します。


あなたがする必要がないことにも注意してください:

command1
if [ $? -ne 0 ]; then

あなたは単に言うことができます:

if ! command1; then

そして、あなたがたときにないリターンコードをチェックする必要はなく、算術コンテキストを使用[ ... -ne

ret=$?
# do something
if (( ret != 0 )); then

34

ランナー関数を作成したりset -e、を使用したりする代わりに、trap

trap 'echo "error"; do_cleanup failed; exit' ERR
trap 'echo "received signal to stop"; do_cleanup interrupted; exit' SIGQUIT SIGTERM SIGINT

do_cleanup () { rm tempfile; echo "$1 $(date)" >> script_log; }

command1
command2
command3

トラップは、それをトリガーしたコマンドの行番号とコマンドラインにもアクセスできます。変数は$BASH_LINENOおよび$BASH_COMMANDです。


4
tryブロックをさらに厳密に模倣したい場合は、を使用trap - ERRして「ブロック」の最後でトラップをオフにします。
Gordon Davisson、2011年

14

個人的には、ここで見られるように、軽量のアプローチを使用することを好みます

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
asuser() { sudo su - "$1" -c "${*:2}"; }

使用例:

try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"

8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3

6
実行しない$*でください。引数にスペースが含まれていると失敗します。"$@"代わりに使用してください。($ *はechoコマンドで大丈夫ですが)
Gordon Davisson

6

私はbashでほぼ完璧なtry&catch実装を開発しました。これにより、次のようなコードを記述できます。

try 
    echo 'Hello'
    false
    echo 'This will not be displayed'

catch 
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"

それらの中にtry-catchブロックをネストすることもできます!

try {
    echo 'Hello'

    try {
        echo 'Nested Hello'
        false
        echo 'This will not execute'
    } catch {
        echo "Nested Caught (@ $__EXCEPTION_LINE__)"
    }

    false
    echo 'This will not execute too'

} catch {
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
}

コードは私のbashボイラープレート/フレームワークの一部です。これは、バックトレースと例外(および他のいくつかの優れた機能)を使用したエラー処理のようなものでtry&catchのアイデアをさらに拡張します。

以下は、try&catchのみを担当するコードです。

set -o pipefail
shopt -s expand_aliases
declare -ig __oo__insideTryCatch=0

# if try-catch is nested, then set +e before so the parent handler doesn't catch us
alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e;
           __oo__insideTryCatch+=1; ( set -e;
           trap \"Exception.Capture \${LINENO}; \" ERR;"
alias catch=" ); Exception.Extract \$? || "

Exception.Capture() {
    local script="${BASH_SOURCE[1]#./}"

    if [[ ! -f /tmp/stored_exception_source ]]; then
        echo "$script" > /tmp/stored_exception_source
    fi
    if [[ ! -f /tmp/stored_exception_line ]]; then
        echo "$1" > /tmp/stored_exception_line
    fi
    return 0
}

Exception.Extract() {
    if [[ $__oo__insideTryCatch -gt 1 ]]
    then
        set -e
    fi

    __oo__insideTryCatch+=-1

    __EXCEPTION_CATCH__=( $(Exception.GetLastException) )

    local retVal=$1
    if [[ $retVal -gt 0 ]]
    then
        # BACKWARDS COMPATIBILE WAY:
        # export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}"
        # export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}"
        export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}"
        export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}"
        export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}"
        return 1 # so that we may continue with a "catch"
    fi
}

Exception.GetLastException() {
    if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]]
    then
        cat /tmp/stored_exception
        cat /tmp/stored_exception_line
        cat /tmp/stored_exception_source
    else
        echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}"
    fi

    rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source
    return 0
}

気軽にforkして、貢献してください-それはGitHubにあります


1
私は私の好みにそれのあまりにも多くの魔法ので(IMOそれは、Pythonを使用することをお勧めします場合は、1つのニーズがより抽象化電源)、レポを見てきましたし、これを自分で使うつもりはない、間違いなく大きな+1それだけで素晴らしい見えるので私から。
Alexander Malakhov

親切な言葉@AlexanderMalakhovをありがとう。「マジック」の量については同意します。これは、フレームワークの簡略化された3.0バージョンをブレインストーミングする理由の1つです。これにより、理解やデバッグなどがはるかに簡単になります。GHには3.0に関する未解決の問題があります。あなたはあなたの考えに欠けたいと思うでしょう。
niieani

3

最初の答えにコメントを付けることができず申し訳ありませんが、コマンドを実行するには新しいインスタンスを使用する必要があります:cmd_output = $($ @)

#!/bin/bash

function check_exit {
    cmd_output=$($@)
    local status=$?
    echo $status
    if [ $status -ne 0 ]; then
        echo "error with $1" >&2
    fi
    return $status
}

function run_command() {
    exit 1
}

check_exit run_command

2

以下のための魚シェルこのスレッドにつまずくのユーザー。

てみようfooではない「リターン」(エコー)値も関数であるが、それはいつものように終了コードを設定します。
$status関数を呼び出した後の チェックを回避するには、次のようにします。

foo; and echo success; or echo failure

そして、1行に収まらないほど長い場合:

foo; and begin
  echo success
end; or begin
  echo failure
end

1

使用するときssh、接続の問題が原因の問題とerrexitset -e)モードのリモートコマンドのエラーコードを区別する必要があります。次の関数を使用します。

# prepare environment on calling site:

rssh="ssh -o ConnectionTimeout=5 -l root $remote_ip"

function exit255 {
    local flags=$-
    set +e
    "$@"
    local status=$?
    set -$flags
    if [[ $status == 255 ]]
    then
        exit 255
    else
        return $status
    fi
}
export -f exit255

# callee:

set -e
set -o pipefail

[[ $rssh ]]
[[ $remote_ip ]]
[[ $( type -t exit255 ) == "function" ]]

rjournaldir="/var/log/journal"
if exit255 $rssh "[[ ! -d '$rjournaldir/' ]]"
then
    $rssh "mkdir '$rjournaldir/'"
fi
rconf="/etc/systemd/journald.conf"
if [[ $( $rssh "grep '#Storage=auto' '$rconf'" ) ]]
then
    $rssh "sed -i 's/#Storage=auto/Storage=persistent/' '$rconf'"
fi
$rssh systemctl reenable systemd-journald.service
$rssh systemctl is-enabled systemd-journald.service
$rssh systemctl restart systemd-journald.service
sleep 1
$rssh systemctl status systemd-journald.service
$rssh systemctl is-active systemd-journald.service

1

上記のRedHat以外のシステムで@ john-kugelmanのすばらしいソリューションを使用するには、彼のコードで次の行をコメント化します。

. /etc/init.d/functions

次に、以下のコードを最後に貼り付けます。完全な開示:これは、Centos 7から取得した上記のファイルの関連する部分を直接コピーして貼り付けたものです。

MacOSおよびUbuntu 18.04でテスト済み。


BOOTUP=color
RES_COL=60
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \\033[1;32m"
SETCOLOR_FAILURE="echo -en \\033[1;31m"
SETCOLOR_WARNING="echo -en \\033[1;33m"
SETCOLOR_NORMAL="echo -en \\033[0;39m"

echo_success() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_SUCCESS
    echo -n $"  OK  "
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 0
}

echo_failure() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
    echo -n $"FAILED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_passed() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"PASSED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_warning() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"WARNING"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
} 

0

機能的にステータスを確認する

assert_exit_status() {

  lambda() {
    local val_fd=$(echo $@ | tr -d ' ' | cut -d':' -f2)
    local arg=$1
    shift
    shift
    local cmd=$(echo $@ | xargs -E ':')
    local val=$(cat $val_fd)
    eval $arg=$val
    eval $cmd
  }

  local lambda=$1
  shift

  eval $@
  local ret=$?
  $lambda : <(echo $ret)

}

使用法:

assert_exit_status 'lambda status -> [[ $status -ne 0 ]] && echo Status is $status.' lls

出力

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