Claude Codeでbashをデバッグする — set -xとPS4で行番号付きトレース
bashスクリプトの不具合をClaude Codeに直させるとき、素のset -xでは行が分かりません。PS4に行番号と関数名を入れ、失敗の直前だけを切り出して渡す手順を、実行した出力つきで書きます。
「このシェルスクリプトが途中でおかしくなる。直して」とだけ頼むと、Claude Codeはスクリプトを読んで原因を推測します。推測が当たる日もありますが、実行時に何が起きたかを渡したほうが、直しは安定します。bashでその材料になるのが set -x のトレースです。
ただし素の set -x は、どの行のどの関数で実行されたかが出ません。この記事では、PS4 に行番号と関数名を入れる方法、数百行のトレースから失敗の直前だけを切り出す方法、Claude Codeへの渡し方を、手元で動かした出力つきで扱います。出力はmacOSのbash 3.2.57で取りました。
素のset -xは行番号が出ない
set -x(または bash -x script.sh)は、実行する各コマンドを展開後の形で標準エラー出力に書き出します。先頭には PS4 の値が付き、既定は + です。行番号もファイル名も付きません。
#!/usr/bin/env bash
set -eu
greet() { local n=$1; echo "hello $n"; }
count_files() {
local dir=$1
ls "$dir" | wc -l
}
set -x
greet world
total=$(count_files ./nodir)
echo "total=$total"既定の PS4 で動かしたトレースは、次の形になります。
+ greet world
+ local n=world
+ echo 'hello world'
++ count_files ./nodir
++ local dir=./nodir
++ ls ./nodir
++ wc -l
ls: ./nodir: No such file or directory
+ total=' 0'
+ echo 'total= 0'何が起きたかは読めます。ただし、トレースの行をスクリプトの何行目に戻せばよいか、手がかりがありません。短いスクリプトなら見当が付きますが、数百行の運用スクリプトでは、Claude Codeも人間も grep で該当箇所を探すところから始めることになります。
先頭の + が ++ になっている行にも意味があります。コマンド置換 $(...) の中で実行された行は、+ が1つ増えます。入れ子の深さがそのまま見えるので、PS4 を替えても先頭の + は残しておくのが定石です。
PS4に行番号・ファイル名・関数名を入れる
PS4 は変数なので、set -x の前に代入するだけで形を変えられます。bashは PS4 の中身も展開してからトレース行の頭に付けるため、$LINENO などが使えます。
PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '| 要素 | 出る内容 |
|---|---|
${BASH_SOURCE##*/} | 出る内容実行中のスクリプトのファイル名(ディレクトリは除く) |
${LINENO} | 出る内容実行中の行番号 |
${FUNCNAME[0]:-main} | 出る内容関数内なら関数名、関数の外なら main |
FUNCNAME は関数の外では空です。:-main で既定値を補っているのは、set -u を併用しているスクリプトで未定義変数のエラーを避けるためです。
先ほどのスクリプトの set -eu の次の行(3行目)にこの PS4 を入れ、以降の行を1つ下げて動かすと、トレースは次のように変わります。
+ deploy.sh:10:main: greet world
+ deploy.sh:4:greet: local n=world
+ deploy.sh:4:greet: echo 'hello world'
++ deploy.sh:11:main: count_files ./nodir
++ deploy.sh:6:count_files: local dir=./nodir
++ deploy.sh:7:count_files: ls ./nodir
++ deploy.sh:7:count_files: wc -l
ls: ./nodir: No such file or directory
+ deploy.sh:11:main: total=' 0'
+ deploy.sh:12:main: echo 'total= 0'ls が失敗したのは count_files の7行目、と読み取れるようになりました。呼び出し元は11行目です。「deploy.sh の7行目の ls が失敗しているのに、スクリプトは最後まで進んでいる」と、場所つきでClaude Codeに伝えられます。
トレースで見るべき3つの手がかり
行番号が付いたら、次はトレースのどこを読むかです。Claude Codeに読ませるときも、観点を先に伝えると的が絞れます。
トレース行の見た目には、引用符の有無が出ます。変数をクォートしていない呼び出しは、展開後に空白で割れて別々の引数になります。f="a b.txt" を使った次のスクリプトで確かめます。
#!/usr/bin/env bash
PS4='+ ${BASH_SOURCE##*/}:${LINENO}: '
f="a b.txt"
set -x
ls -l $f
ls -l "$f"+ q.sh:5: ls -l a b.txt
ls: a: No such file or directory
ls: b.txt: No such file or directory
+ q.sh:6: ls -l 'a b.txt'クォートなしの5行目は a と b.txt の2引数に割れ、クォートありの6行目は 'a b.txt' と引用符つきで1引数になっています。トレース上で引数が引用符で囲まれているかどうかが、そのまま単語分割の有無の証拠です。
空の変数は、空のまま展開されます。rm -rf "$dir"/* のようなコマンドで dir が空なら、トレースには rm -rf /* に近い形が出ます。実行前に気づく手段にはなりませんが、事故の後に何が起きたかをたどる材料にはなります。set -u が止めるのは未定義の変数の参照で、空文字が入った変数は素通しします。空のままでは困る変数は ${dir:?} のように書くと、空でも止まります。
++ が増えた行は、コマンド置換の中です。先ほど見たとおり、$(...) の中の失敗は呼び出し元の行より深い階層に出ます。親の行だけを見て原因を探すと、見当違いの場所を直すことになります。
これらを CLAUDE.md や依頼文に「引数の引用符、空の展開、++ の深さの3点を見て」と書いておくと、トレースを読む観点が固定されます。
set -vとの違い
似たオプションに set -v があります。set -x が展開後のコマンドを出すのに対し、set -v はスクリプトの行を読み込んだままの文字列で出します。name=world のあとに echo "hello $name" を実行すると、-v は echo "hello $name" と変数が展開されない形で、-x は echo 'hello world' と展開後の形で出ます。bash -vx で両方を有効にすると、元の行と展開後の行が交互に並びます。
デバッグで知りたいのは多くの場合、実際に渡された値です。展開後の形が出る set -x を主に使い、-v は元の行を並べて見たいときの補助にします。
長いトレースから、失敗の直前だけを渡す
実際のスクリプトでは、トレースが数百行になります。全部を貼ると、コンテキストを関係のない行が占め、肝心の失敗行が埋もれます。次の順で絞ります。
トレースを絞って渡す順番
- 1
トレースを端末ではなくファイルに落とす
標準エラー出力をファイルへ向けます。標準出力は別に保存しておくと、あとで突き合わせられます。
- 2
失敗の合図をトレースに混ぜる
trap ... ERRでエラー時に1行出しておくと、トレース内の目印になります。 - 3
目印の手前だけを切り出す
tailやsedで、目印の前の数行から末尾までだけを取り出します。
200回ループを含むスクリプトで試します。途中で存在しないファイルをコピーして失敗させています。
#!/usr/bin/env bash
set -euo pipefail
PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: '
trap 'echo "ERR exit=$? line=$LINENO" >&2' ERR
step() { echo "step $1"; }
for i in $(seq 1 200); do :; done
step one
cp ./missing.txt /tmp/out.txt
step twobash -x run.sh 2>trace.txt >out.txt; echo "exit=$?"
wc -l trace.txt
tail -n 4 trace.txtexit=1
410 trace.txt
+ run.sh:8:main: cp ./missing.txt /tmp/out.txt
cp: ./missing.txt: No such file or directory
++ run.sh:8:main: echo 'ERR exit=1 line=8'
ERR exit=1 line=8トレースは410行ありましたが、失敗を説明するのは末尾の4行です。ループの200回分は、1周ごとに2行ずつ出ていました。原因が見つかった後の行まで貼る必要はありません。
絞り方の使い分けを表にします。
| 状況 | 渡し方 | 向く理由 |
|---|---|---|
| 最後のコマンドで落ちた | 渡し方tail -n 20 trace.txt | 向く理由set -e で止まったなら、失敗行は末尾にある |
| 落ちずに誤った結果が出た | 渡し方疑わしい関数だけ set -x と set +x で挟む | 向く理由範囲を絞れば、そもそも数十行で済む |
| 目印が複数ある | 渡し方grep -n 'ERR exit' trace.txt で行番号を出し、その前後を sed -n で切る | 向く理由最初の失敗だけを渡せる |
範囲を挟む方法は、トレースを出す側で絞る手段です。
set -x
process_batch "$input"
set +xset +x 自体の行はトレースに残りますが、その後の行は出ません。
落とし穴
BASH_XTRACEFDは古いbashでは効かない
トレースだけを別ファイルに出したいときは、exec 5>trace.log のあとに BASH_XTRACEFD=5 を指定する方法があります。これはbash 4.1以降の機能です。macOS標準のbash 3.2.57で試したところ、指定は無視されてトレースは標準エラー出力に出たままで、trace.log は空でした。新しいbashが入っていない環境では、2>trace.txt のリダイレクトで分ける形が確実です。
Claude Codeへの渡し方
渡し方は2通りあります。
1つ目は、セッション内でClaude Codeにコマンドを実行させる方法です。公式のワークフロー集は、不具合を直すときは再現コマンドを伝え、再現が毎回か断続的かも伝えるよう勧めています。トレース取得の手順を依頼文に含めれば、その再現コマンドをそのまま渡せます。
deploy.sh の7行目あたりで ls が失敗しているのに、スクリプトが最後まで進んでしまう。
次の手順で行番号付きのトレースを取って、失敗した行とその直前の10行を読んでから原因を教えて。
PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' bash -x deploy.sh 2>trace.txt
tail -n 30 trace.txt
直すのは原因が分かってからにして。修正後は同じ手順でトレースを取り直し、失敗が消えたことを出力で見せて。最後の1文が効きます。修正の前後で同じトレースを並べれば、直った根拠が人間にも見えます。
2つ目は、トレースを標準入力で渡す方法です。ベストプラクティスのページには、コマンド出力を cat error.log | claude -p "what's going on here" のように流し込む例があります。絞ったトレースもそのまま流せます。
tail -n 30 trace.txt | claude -p "このbashトレースで最初に失敗している行と原因を、行番号つきで答えて"標準入力の上限は10MBで、超えるとエラー終了します。トレースが大きいときは、ファイルに書いてパスを依頼文に書くほうが安全です。非対話モードの詳しい使い方はclaude -pのガイドにあります。
毎回書かないために、規約をCLAUDE.mdに置く
行番号付きの PS4 は、毎回の依頼文に書くより CLAUDE.md に1回書いておくほうが漏れません。
## シェルスクリプトのデバッグ
- トレースは `set -x` を直書きせず、次の形で取る
`PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' bash -x <script> 2>trace.txt`
- トレースは全文を読まず、`tail -n 30 trace.txt` から始める
- 直す前に失敗行を行番号つきで報告し、直した後は同じ手順で取り直す
- 検証が終わったら、`set -x` の行を残していないか `grep -n 'set -x'` で確かめるbash -x で外から付ける形にしておけば、スクリプト本体を書き換えずに済み、set -x の消し忘れも起きません。ただし CLAUDE.md は従うよう促す文脈であって、強制力のある仕組みではありません。確実に防ぎたいときは、編集後にフックで検査する構成を選びます。シェルスクリプトの静的検査をフックに載せる方法はShellCheckのフックの記事にあります。
トレースを渡す前に確かめること
トレースは、スクリプトが実際に扱った値をそのまま展開して書きます。上の例でも total=' 0' のように、変数の中身がトレース行に出ていました。トークンやパスワードを変数で扱うスクリプトでは、それらの値もトレースに出ます。Claude Codeに渡す前に、grep -n -i 'token\|password\|secret' trace.txt のような確認を挟むか、機密を扱う区間だけ set +x で外してください。
もう1点、PS4 の書き方はbashの流儀です。macOSの既定シェルはzshで、bash構文をzshで走らせて失敗するケースもあります。起動に使うシェルとスクリプトの先頭行(#!/usr/bin/env bash)が揃っているかは、トレースを取る前に確認します。この食い違いはClaude Codeがzshでbash構文を書いて失敗する記事で扱っています。
トレースで原因を確定したあとは、同じ失敗を再発させないテストを足す段階です。bats で異常系を書く手順はbatsのシェルスクリプトテストにまとめています。
まとめ
素の set -x は何が実行されたかを教えますが、どこで実行されたかは教えません。PS4 に ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main} を入れるだけで、トレースの各行がスクリプトの行に戻せます。渡すときは全文ではなく失敗の目印の手前だけに絞り、修正の前後で同じ手順のトレースを並べて直った根拠にします。macOS標準のbash 3.2では BASH_XTRACEFD が効かない点と、トレースに秘密の値が出る点は、先に押さえておく価値があります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。