シェルスクリプトが意図した動作をしない
シェルスクリプトはエラーメッセージを出さずに間違った結果を返す場合がある。
以下はログファイルが10個を超えたときに警告を出すスクリプトである。
#!/bin/bash
LOG_DIR=/var/log/app
COUNT=$(ls "${LOG_DIR}" | wc -l)
if [ ${COUNT} > 10 ]; then
echo "too many log files: ${COUNT}"
fi
ログファイルは3個しかないにもかかわらず、警告が出力される。
$ ls /var/log/app
a.log b.log c.log
$ ./check.sh
too many log files: 3
条件式が正しく評価されていないと推測できるが、スクリプトの出力だけでは原因を特定できない。
bash -xで実行トレースを表示する
bash -xでスクリプトを実行すると、各コマンドを実行する直前に、実行内容が標準エラー出力へ表示される。
$ bash -x ./check.sh
+ LOG_DIR=/var/log/app
++ ls /var/log/app
++ wc -l
+ COUNT=3
+ '[' 3 ']'
+ echo 'too many log files: 3'
too many log files: 3
+ '[' 3 ']'の行に注目する。
スクリプトに書いた> 10が条件式から消えている。
bashが>をリダイレクトとして解釈し、[ 3 ]の標準出力を10という名前のファイルへ向けていた。
[ 3 ]は空文字列でない引数を真と判定するため、条件は常に成立する。
$ ls
10 check.sh
数値の比較には-gtを使う。
#!/bin/bash
LOG_DIR=/var/log/app
COUNT=$(ls "${LOG_DIR}" | wc -l)
if [ "${COUNT}" -gt 10 ]; then
echo "too many log files: ${COUNT}"
fi
修正後のトレースでは、条件式が3 -gt 10として評価され、警告が出力されなくなる。
$ bash -x ./check.sh
+ LOG_DIR=/var/log/app
++ ls /var/log/app
++ wc -l
+ COUNT=3
+ '[' 3 -gt 10 ']'
行頭の+の数
トレースの行頭には+が表示される。
+は間接実行のネストの深さに応じて繰り返される。
コマンド置換やサブシェルの中で実行されるコマンドは++で表示される。
#!/bin/bash
echo "today is $(date +%Y)"
$ bash -x ./date.sh
++ date +%Y
+ echo 'today is 2026'
today is 2026
表示されるのは展開後のコマンド
トレースには変数やコマンド置換を展開した後のコマンドが表示される。
上記の例では${LOG_DIR}が/var/log/appに置き換わっている。
変数へ実際に入った値を確認できるため、echoをスクリプト中に埋め込む必要がない。
一部分だけをトレースする
スクリプト全体のトレースは行数が多くなりやすい。
set -xとset +xで囲むと、対象の範囲だけをトレースできる。
#!/bin/bash
APP=myapp
set -x
VERSION=$(echo "1.2.3")
echo "deploy ${APP} ${VERSION}"
set +x
echo "done"
$ ./deploy.sh
++ echo 1.2.3
+ VERSION=1.2.3
+ echo 'deploy myapp 1.2.3'
deploy myapp 1.2.3
+ set +x
done
set +x自体もトレース対象となるため、末尾に+ set +xが表示される。
PS4で行番号や関数名を表示する
トレースの行頭に表示される文字列はPS4変数で決まる。
デフォルト値は+ である。
PS4にBASH_SOURCE、LINENO、FUNCNAMEを含めると、ファイル名、行番号、関数名を表示できる。
#!/bin/bash
PS4='+ ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]:-main}: '
set -x
build() {
local target=$1
echo "building ${target}"
}
build myapp
$ ./ps4.sh
+ ./ps4.sh:10:main: build myapp
+ ./ps4.sh:6:build: local target=myapp
+ ./ps4.sh:7:build: echo 'building myapp'
building myapp
FUNCNAME[0]は関数の外では未定義になるため、${FUNCNAME[0]:-main}としてmainを表示している。
PS4はシングルクォートで囲む点に注意する。
ダブルクォートで囲むと代入時に展開され、常に同じ行番号が表示される。
処理時間を調べたい場合は時刻を含める。
PS4='+ [$(date +%H:%M:%S)] '
+ [02:05:12] sleep 1
+ [02:05:13] echo done
done
dateはコマンドの実行ごとに外部コマンドを起動する。
bash 5.0以降ではEPOCHREALTIMEを使うと、外部コマンドを起動せずにマイクロ秒単位の時刻を表示できる。
PS4='+ ${EPOCHREALTIME} '
+ 1786414095.104811 sleep 0.5
+ 1786414095.606494 echo done
done
トレースの出力先を分ける
トレースは標準エラー出力へ書かれる。 スクリプト本来の出力と混ざって読みにくい場合は、標準エラー出力だけをファイルへリダイレクトする。
#!/bin/bash
NAME=world
echo "hello ${NAME}"
$ bash -x ./app.sh 2> trace.log
hello world
$ cat trace.log
+ NAME=world
+ echo 'hello world'
スクリプト自身が標準エラー出力へログを書く場合は、BASH_XTRACEFDにファイルディスクリプタ番号を指定すると、トレースだけを別のファイルへ分離できる。
#!/bin/bash
exec 9> trace.log
BASH_XTRACEFD=9
set -x
NAME=world
echo "hello ${NAME}"
$ ./xfd.sh
hello world
$ cat trace.log
+ NAME=world
+ echo 'hello world'
環境変数でトレースを切り替える
スクリプトを書き換えずにトレースを有効にしたい場合、環境変数で切り替える。
#!/bin/bash
[ -n "${DEBUG:-}" ] && set -x
echo "run"
通常の実行ではトレースが出ない。
$ ./sw.sh
run
DEBUGを指定するとトレースが出力される。
$ DEBUG=1 ./sw.sh
+ echo run
run
呼び出し先のスクリプトもトレースする
bash -xのトレース対象は実行したスクリプトだけである。
別のスクリプトを呼び出す部分は、呼び出したコマンド行のみが表示される。
$ ./parent.sh
+ APP=myapp
+ ./child.sh myapp
child: myapp
SHELLOPTSをexportすると、呼び出し先のスクリプトへもオプションが引き継がれる。
#!/bin/bash
export SHELLOPTS
set -x
APP=myapp
./child.sh "${APP}"
$ ./parent.sh
+ APP=myapp
+ ./child.sh myapp
+ echo 'child: myapp'
child: myapp
PS4の設定も引き継ぎたい場合はexport PS4を併せて指定する。
機密情報の出力に注意する
トレースには展開後の値が表示される。 パスワードやトークンを変数へ入れている場合、値がそのまま出力される。
+ curl -H 'Authorization: Bearer eyJhbGciOi...' https://example.com/api
CIのログへbash -xの出力を残す場合は、機密情報を扱う区間をset +xで囲んで除外する。
set +x
TOKEN=$(cat /run/secrets/token)
curl -H "Authorization: Bearer ${TOKEN}" https://example.com/api
set -x
構文チェックと実行前の内容表示
-x以外にもデバッグに使えるオプションがある。
bash -nはコマンドを実行せずに構文だけをチェックする。
以下はfiが抜けているスクリプトである。
#!/bin/bash
if [ -f /etc/hosts ]; then
echo yes
$ bash -n ./broken.sh
./broken.sh: line 4: syntax error: unexpected end of file
$ echo $?
2
bash -vは読み込んだ行をそのまま表示する。
-xが展開後のコマンドを表示するのに対し、-vはスクリプトに書いたままの内容を表示する。
$ bash -v ./app.sh
#!/bin/bash
NAME=world
echo "hello ${NAME}"
hello world
関連
- 【ShellCheck】Lintでシェルスクリプトの品質を高める
を使うと、実行前に問題を検出できる。上記の
[ ${COUNT} > 10 ]はSC2071として検出される - 【Shell Script】set -euo pipefailでエラーに強いシェルスクリプトを書く と組み合わせると、失敗した箇所で停止させながらトレースを確認できる
