Claude Media
Claude Codeでbashの引数をgetoptsで解析する — -hの出力と終了コードを完了条件に

Claude Codeでbashの引数をgetoptsで解析する — -hの出力と終了コードを完了条件に

Claude Codeにgetoptsを使うbashスクリプトを書かせるとき、オプション仕様と終了コードを先に渡し、-hの出力と不正引数の挙動を実行して確かめさせる手順です。

bashの引数解析にはgetoptsが使えます。Claude Codeに「オプション付きのスクリプトを書いて」と頼むと、-hのusageが出ない、-nに数字以外が来ても通る、長い--helpを期待すると通らない、といった抜けが残りがちです。コードの出来を目で判断する代わりに、-hの出力と不正引数のときの終了コードを仕様として先に渡し、Claude自身にそれを実行させて完了を判定させます。

この記事では、ログの末尾を取り出す小さなスクリプトを題材に、getoptsの組み方、Claudeへの渡し方、claude -pで自動化するときの許可の書き方を順に扱います。出力はすべて、手元のbash 3.2.57(macOS標準)で実際に走らせたものです。

先に決めるのは仕様の3点

Claudeに渡す仕様は、実装方法ではなく、外から観測できる振る舞いに絞ります。観測できるものは完了条件に変えられるからです。

仕様

仕様として固定する3点

  • オプション表

    文字、引数の有無、既定値を表にします。今回は-h、-v、-n COUNT、-o FILEと、位置引数のDIRが1つです。

  • usageの形

    -hでは標準出力に出して終了コード0、誤用のときは標準エラーに出します。出力先を分けると、パイプの先にヘルプが流れ込みません。

  • 終了コードの割り当て

    誤用は64、入力ディレクトリが無いときは66にします。sysexits.hの慣習に合わせた番号で、呼び出し側が原因を切り分けられます。

数字は何でもかまいません。大切なのは、番号を仕様に書いて、テストで固定することです。

getoptsの動きを先に押さえる

getoptsはbashの組み込みコマンドで、呼ぶたびに次のオプションを1つ変数に入れます。bashのマニュアルにある動きのうち、スクリプトの書き方に効くものは次のとおりです。

  • optstringでオプション文字の後ろにコロンを付けると、そのオプションは引数を取る
  • 引数はOPTARGに、次に処理する位置はOPTINDに入る
  • オプションの終わりに達すると、0より大きい値を返して終了する
  • optstringの先頭をコロンにすると静かなエラー報告になり、不正なオプションは?、引数の欠落は:として受け取れる
  • 静かなモードでは診断メッセージが出ないので、自前のusageを出せる

先頭コロンの「静かなモード」を使うと、エラー文言と終了コードをスクリプト側で決められます。標準のメッセージに任せると、文言がシェルの実装次第になり、テストで固定しづらくなります。

動いたスクリプトの全体

次が、仕様に沿って組んだscripts/loglines.shです。

#!/usr/bin/env bash
# loglines.sh [-h] [-v] [-n COUNT] [-o FILE] DIR
set -euo pipefail
 
usage() {
  cat <<'USAGE'
usage: loglines.sh [-h] [-v] [-n COUNT] [-o FILE] DIR
  -h        show this help and exit 0
  -v        verbose
  -n COUNT  lines to keep per file (positive integer, default 10)
  -o FILE   write the result to FILE instead of stdout
USAGE
}
 
die() {
  echo "loglines.sh: $1" >&2
  usage >&2
  exit 64
}
 
count=10 verbose=0 out=
while getopts ':hvn:o:' opt; do
  case $opt in
    h) usage; exit 0 ;;
    v) verbose=1 ;;
    n) count=$OPTARG ;;
    o) out=$OPTARG ;;
    :) die "option -$OPTARG requires an argument" ;;
    \?) die "unknown option: -$OPTARG" ;;
  esac
done
shift $((OPTIND - 1))
 
[[ $count =~ ^[1-9][0-9]*$ ]] || die "-n must be a positive integer: $count"
[ $# -eq 1 ] || die "expected exactly one DIR, got $#"
[ -d "$1" ] || { echo "loglines.sh: no such directory: $1" >&2; exit 66; }

後半は、run関数でディレクトリ内の*.logの末尾を取り出して出力する処理です。引数解析の話からは外れるので省略します。

要点は3つあります。shift $((OPTIND - 1))でオプションを取り除いてから位置引数を数えること、数値の検証をgetoptsの外で行うこと、エラーはすべてdieに集めることです。getoptsは「-nに値が付いているか」までしか見ないので、中身が整数かどうかは自分で確かめます。

実行して確かめた挙動

11通りの引数で走らせ、終了コードと標準エラーの先頭行を並べると、次のようになりました。

引数終了コード標準エラーの先頭行
-h終了コード0標準エラーの先頭行なし(usageは標準出力)
なし終了コード64標準エラーの先頭行expected exactly one DIR, got 0
-x d終了コード64標準エラーの先頭行unknown option: -x
-n終了コード64標準エラーの先頭行option -n requires an argument
-n abc d終了コード64標準エラーの先頭行-n must be a positive integer: abc
-n 0 d終了コード64標準エラーの先頭行-n must be a positive integer: 0
nodir終了コード66標準エラーの先頭行no such directory: nodir
-v -n 2 -- d終了コード0標準エラーの先頭行== d/a.log(verboseの出力)
d -n 2終了コード64標準エラーの先頭行expected exactly one DIR, got 3
--help d終了コード64標準エラーの先頭行unknown option: --
-n -v d終了コード64標準エラーの先頭行-n must be a positive integer: -v

注目したいのは表の下3行です。

  • d -n 2は誤用になります。getoptsは最初の非オプション引数で解析を止めるため、後ろの-n 2は位置引数として数えられ、3個になります
  • --helpは使えません。getoptsが扱うのは1文字のオプションだけです。--helpは2文字目の-が未知のオプション文字として読まれ、unknown option: --で64になります(ちょうど--だけを渡したときは、オプションの終わりの印として扱われます)。長いオプションが欲しければ、case "$1" in --help)のように自前で分岐するか、別の仕組みが要ります。仕様に長いオプションが含まれるなら、getoptsを使うか自前の解釈にするかを先に決め、プロンプトに書いておきます
  • -n -v dは、-vを値として受け取ります。-nが次の語を無条件に引数にするので、結果は-n must be a positive integer: -vで64になります。数値の検証を入れておいたから、静かに通らずに済んでいます

特に3つ目は、検証を省くと気づきにくい動きです。Claudeが書いたコードにこの検証が無い場合、実行して見つけさせる価値があります。

仕様に無かった終了コードを実行で見つける

表に含めなかった入力が、あと2種類あります。どちらも実行して初めて分かった挙動です。

  • 束ねた書き方は通ります。-vhは-vと-hとして解釈されてusageを出し、-n2は-n 2と同じ扱いになります。getoptsは1文字オプションの束ね書きと、値の直結をどちらも処理します
  • 出力先に書けないとき、終了コードは1になります。-o /nonexist/x dを渡すと、リダイレクトの失敗でbashのエラーが出て、終了コードは1でした。仕様に書いた64とも66とも違う番号です

後者は、仕様表に「出力ファイルが書けない」という行が無かったために起きています。Claudeが書いたコードの不具合ではなく、依頼側の抜けです。完了条件の実行結果を見て、番号が仕様に無い値で返ってきたら、仕様表に行を足して73(出力ファイルを作れない)のような番号を割り当て直します。この往復を経ると、完了条件の一覧が実際の失敗のしかたに近づいていきます。

Claudeに渡すプロンプト

仕様表と完了条件を1つのプロンプトにまとめます。

scripts/loglines.sh を新規に作ります。bash 3.2 でも動くこと。
引数の解析は組み込みの getopts を使い、optstring は先頭をコロンにした静かなモード。
 
オプション: -h(ヘルプ、終了コード0、標準出力)、-v、-n COUNT(正の整数、既定10)、
-o FILE。位置引数は DIR が1つだけ。
誤用は usage を標準エラーに出して終了コード64、DIR が無ければ66。
 
完了条件は、次をあなた自身が実行して結果を貼ることです。
1. ./scripts/loglines.sh -h の出力と終了コード
2. 引数なし、未知のオプション、-n の値が無い、-n abc、-n 0、存在しない DIR の
   各終了コードと標準エラー1行目
3. オプションを DIR の後ろに置いた場合(d -n 2)の挙動
貼る前に「完了」と書かないでください。

3番目を入れる理由は、前節で見たとおり、getoptsの仕様がそのまま落とし穴になるからです。仕様に書かれていない境界は、Claudeが確認しないまま通りがちです。

Claudeの返答は、実行結果が貼られているかどうかで判断します。貼られた終了コードが仕様の番号とずれていれば、その場で直させます。実行結果を貼らせる進め方は、Claude CodeでBashをtrapとset -euo pipefailで堅牢にする手順でも同じです。

検証をbatsに固定する

手で確かめた挙動は、テストに落とします。bats自体の書き方と、異常系の洗い出しはClaude Codeでbatsのシェルスクリプトテストにあります。ここでは、引数解析に特有の部分だけを示します。

setup() {
  SCRIPT="$BATS_TEST_DIRNAME/../scripts/loglines.sh"
  mkdir -p "$BATS_TEST_TMPDIR/d"
  seq 1 30 >"$BATS_TEST_TMPDIR/d/a.log"
}
 
@test "-h prints usage and exits 0" {
  run -0 "$SCRIPT" -h
  [[ "$output" == usage:* ]]
}
 
@test "option after the operand is a usage error" {
  run -64 "$SCRIPT" "$BATS_TEST_TMPDIR/d" -n 2
}
 
@test "-n swallows the next word and is rejected" {
  run -64 "$SCRIPT" -n -v "$BATS_TEST_TMPDIR/d"
  [[ "$output" == *"must be a positive integer"* ]]
}
 
@test "missing directory exits 66" {
  run -66 "$SCRIPT" "$BATS_TEST_TMPDIR/none"
}

実行はbats test/の1行です。run -64は、その終了コードでなければテストを失敗にします。Claudeには「テストが全部通るまで直す」を完了条件にして、bats --print-output-on-failure test/の出力を読ませると、失敗の理由が見えます。

落とし穴

optstringを変えたのに、usageとテストが古いまま残る

オプションを1つ足すと、optstring、caseの分岐、usage、テストの4か所が同時に変わります。Claudeが前の2つしか直さないと、-hの出力が実際の挙動とずれます。依頼の文面に「usageの出力に載っていないオプションがあれば失敗とする」と書き、テストでは-hの出力に各オプション文字が含まれるかを確かめておくと、ずれを拾えます。

claude -pで回すときの許可

スクリプトの作成と検証を、CIや定期ジョブのような対話なしの実行で回すこともできます。claude -pは成功すると終了コード0、失敗すると0以外で終わるので、呼び出し側のシェルが結果で分岐できます。

claude -p "$(cat prompts/loglines.txt)" \
  --allowedTools "Read,Write,Edit,Bash(./scripts/loglines.sh *),Bash(bats *)"

--allowedToolsにBash(./scripts/loglines.sh *)のような規則を並べると、そのコマンドだけが承認なしで走ります。末尾の半角スペースと*は前方一致の指定で、スペースを抜いたBash(git diff*)はgit diff-indexにも一致する、とドキュメントが注意しています。スクリプト名の後ろにも同じ理屈が当てはまるので、スペースを入れたまま書きます。

許可に無い呼び出しが承認を要する場合、対話の無い-pの実行では、既定ではそのまま拒否されます。Claude Code v2.1.302以降では、権限モードを何も指定しない-pの実行は手動モードで始まります。拒否された呼び出しが出ても止まらないよう、必要なコマンドを許可の一覧に入れておきます。

ほかに、スクリプトやCIで再現性を揃える目的では--bareを足します。ホストのフックやCLAUDE.mdを読み込まないので、ホスト側の設定に左右されにくくなります。ただしbareモードは、サブスクリプションのログインを使わず、ANTHROPIC_API_KEYなどの設定が前提です。

不正なフラグを渡した場合は、実行の前にエラーが標準エラーに出ます。実行の途中で認証が無いなどの失敗が起きたときは、失敗の内容が標準出力に結果として出る、と書かれています。ラッパーのシェルは、終了コードと標準出力の両方を見ておくと取りこぼしません。

ログのような大きな入力をclaude -pに渡すときの上限は、claude -pにログを渡すと10MB上限で止まるで扱っています。

書かせたスクリプトを静的解析にも通す

getoptsのスクリプトは、クォート漏れや$OPTARGの未クォート展開のような、実行しても気づきにくい不具合が混ざりやすい形です。ShellCheckを自動で走らせる設定はClaude CodeでShellCheckを使う手順にあります。実行で確かめる検証と、読んで確かめる検証は別の層なので、両方入れておくと、-nの値に空白が入ったときのような境界を拾えます。

よくある質問

OPTINDをリセットしなくてよいのは、いつですか

スクリプトの先頭からgetoptsを1回のループで使うなら、リセットは要りません。bashのマニュアルによると、OPTINDはスクリプトの起動ごとに1へ初期化されます。同じシェルの中で、関数からgetoptsを複数回呼ぶ場合にだけ、手動で1に戻す必要があります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html
この記事を共有:XはてブLinkedIn