はじめに (対象読者・この記事でわかること)

この記事は、bashスクリプトを書いていてtimeoutコマンドを使った際に「終了ステータスが正しく取得できない」という問題に直面した方を対象にしています。

この記事を読むことで、timeoutコマンドの仕組みと、なぜ終了ステータスが意図しない値になるのかを理解できます。また、正しく終了ステータスを取得するための実用的なテクニックを習得し、スクリプトの信頼性を高めることができるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - bashの基本的な文法(変数、if文、関数) - 終了ステータス(exit status)の基本概念 - コマンドラインでの基本的な操作

timeoutコマンドとは何か・なぜ終了ステータスが混乱するのか

timeoutコマンドは、指定した時間内にコマンドが完了しない場合に強制終了させるための便利なツールです。CI/CDパイプラインやバッチ処理で長時間の処理を防ぐ際に広く使われています。

しかし、このtimeoutコマンドを使うと、実行したコマンドの終了ステータスではなく、timeoutコマンド自体の終了ステータスが返されるため、スクリプト内で「成功か失敗か」を判定する際に混乱を引き起こします。

例えば、タイムアウトしなかった場合でも、実行したコマンドが失敗していた場合の終了ステータスを知りたいというニーズはよくあります。

timeoutコマンドの終了ステータスを正しく取得する方法

ここでは、シェルスクリプトからtimeoutコマンドを実行した際に、正しく終了ステータスを取得するための方法を解説します。

timeoutコマンドの基本的な使い方と終了ステータス

まず、timeoutコマンドの基本的な使い方をおさらいします。

Bash
timeout 10s 実行したいコマンド

このコマンドは、10秒以内に「実行したいコマンド」が完了しなければ、SIGTERMを送信してプロセスを終了させます。

では、終了ステータスはどうなるでしょうか?

Bash
timeout 10s sleep 5 echo $? # 0(成功)
Bash
timeout 2s sleep 5 echo $? # 124(timeoutした)

timeoutコマンドの終了ステータスは以下のルールに従います:

  • 実行コマンドが正常終了した場合 → 0
  • 実行コマンドがタイムアウトした場合 → 124
  • 実行コマンドがシグナルで終了した場合 → 128 + シグナル番号
  • 実行コマンドが存在しないなどのエラー → 127

timeoutコマンドに--preserve-statusオプションを使う

timeoutコマンドには--preserve-statusというオプションがあり、これを使うことで実行コマンドの終了ステータスをそのまま返すことができます。

Bash
timeout --preserve-status 2s false echo $? # 1(falseコマンドの終了ステータス)

ただし、注意点があります。タイムアウトした場合は、やはり124が返ります。

Bash
timeout --preserve-status 2s sleep 5 echo $? # 124(タイムアウト)

スクリプト内でタイムアウトとコマンド失敗を区別する実装

--preserve-statusを使ってもタイムアウトした場合は124が返るため、スクリプト内で「タイムアウトしたのか、コマンドが失敗したのか」を区別するには、少し工夫が必要です。

以下は、そのための実用的なスクリプト例です:

Bash
#!/bin/bash # 実行したいコマンドを変数に格納 COMMAND="curl -s https://example.com/api" # timeoutで実行(--preserve-status付き) timeout --preserve-status 10s $COMMAND EXIT_STATUS=$? # 終了ステータスを判定 case $EXIT_STATUS in 0) echo "コマンドが正常に完了しました" ;; 124) echo "コマンドがタイムアウトしました" # タイムアウト時の処理 ;; *) echo "コマンドが失敗しました(終了ステータス: $EXIT_STATUS)" # エラー処理 ;; esac

より高度なエラーハンドリング

より高度なエラーハンドリングが必要な場合は、一時ファイルを使って実行コマンドの終了ステータスを保存する方法もあります:

Bash
#!/bin/bash # 一時ファイルを作成 TEMPFILE=$(mktemp) # コマンドを実行し、終了ステータスを一時ファイルに保存 # timeoutが発生した場合でも、コマンドの終了ステータスを取得できる timeout 10s bash -c "curl -s https://example.com/api; echo \$? > $TEMPFILE" || true # 一時ファイルから終了ステータスを読み取り if [ -s $TEMPFILE ]; then COMMAND_EXIT_STATUS=$(cat $TEMPFILE) else # ファイルが空の場合はタイムアウトしたと判断 COMMAND_EXIT_STATUS="TIMEOUT" fi # 後処理 rm -f $TEMPFILE echo "コマンドの終了ステータス: $COMMAND_EXIT_STATUS"

ハマった点やエラー解決

筆者が実際にハマったのは、timeoutコマンドを使った後に「コマンドが失敗したのか、タイムアウトしたのか」を区別できず、エラーハンドリングがうまくいかなかったことです。

特に、CI/CDパイプラインでテストを実行する際に、テストが失敗したのか、それともテストが重くてタイムアウトしたのかを区別できないと、適切な対応ができませんでした。

解決策

最終的に、以下のような実装で解決しました:

Bash
#!/bin/bash # タイムアウト時間(秒) TIMEOUT_SECONDS=30 # 実行コマンド COMMAND="npm test" # timeout実行 timeout $TIMEOUT_SECONDS $COMMAND EXIT_STATUS=$? # 終了ステータスに基づいた処理 if [ $EXIT_STATUS -eq 124 ]; then echo "エラー: コマンドが${TIMEOUT_SECONDS}秒以内に完了しませんでした" exit 1 elif [ $EXIT_STATUS -ne 0 ]; then echo "エラー: コマンドが失敗しました(終了ステータス: $EXIT_STATUS)" exit $EXIT_STATUS else echo "成功: コマンドが正常に完了しました" fi

この方法により、タイムアウトとコマンドの失敗を明確に区別でき、適切なエラーメッセージを表示できるようになりました。

まとめ

本記事では、timeoutコマンドをbashスクリプトから実行した際に、終了ステータスが正しく取得できない問題を解決する方法を紹介しました。

  • timeoutコマンドの終了ステータスの仕組み
  • --preserve-statusオプションの活用
  • タイムアウトとコマンド失敗を区別する実装

この記事を通して、シェルスクリプトでtimeoutコマンドを使う際の終了ステータスの扱い方を理解し、より堅牢なエラーハンドリングができるようになりました。

今後は、timeoutコマンドの他のオプションや、シグナルを使ったより細かい制御についても記事にする予定です。

参考資料