AIエージェントが出力するインデントつきのコマンドラインを整形するために Mac OS 27 の fm は十分ではない
Claude Code などの AI エージェントを使っていると、回答の中にコマンドラインや SQL が出てくる。これをターミナルからコピーすると、行頭にインデントが付き、画面幅で折り返された位置に改行が入る。そのまま貼り付けても動かないことがある。
例えば、このような出力だ。
このコマンドで適用されます。
MY_ENV=production kubectl apply -f k8s/deployment.yaml --namespace=web-frontend
--server-side --field-manager=deploy-bot --dry-run=server
--kubeconfig="$HOME/.kube/config-production"
この SQL を実行することで調査できます。
SELECT o.id, o.user_id, o.total_price, o.created_at FROM orders_order o
WHERE o.status = 'pending' AND o.created_at < NOW() - INTERVAL '3 days'
ORDER BY o.created_at LIMIT 100;
ターミナルのマウス統合機能がうまく動いていればきれいにコピーできるし、Claude Code なら /copy でコピーできる場合もある。ただ、環境や場面によってはどちらも使えない。
そこで、クリップボードの中身を整形して書き戻す小さな CLI を自作し、ショートカットキーから呼んでいる。整形には LLM を使う。2 行にわたるコマンドが、2 つのコマンドなのか、折り返しで改行が入った 1 つのコマンドなのかは、ぱっと見では判断がつかない。決まった規則で処理するコードより、LLM のほうが向いている。
これまでの構成と不満
これまでは Anthropic の claude-haiku-4-5 に整形させていて、機能としては十分だった。
不満は認証情報の管理にあった。外部の LLM を使うには API キーが要る。私は API キーを 1Password に入れているので、整形のたびに Touch ID の指紋認証が必要だった。
macOS 27 では fm コマンドが追加され、OS に組み込まれたローカル LLM (Apple Foundation Models) をコマンドラインから呼べるようになった。ローカルで動くなら API キーは要らない。そこで、Haiku の代わりに使えるかを検証した。
検証内容
整形の指示は、Haiku に渡していたものをそのまま使った。
以下の処理を行ってください:
1. 罫線 (─, │, ┌, ┐, └, ┘, ├, ┤, ┬, ┴, ┼ など) を削除
2. 余分なインデントを削除
3. シェルスクリプトや SQL のような構造化されたコードであれば、適切に整形
4. 元のテキストの意味や構造は可能な限り保持
5. 文頭の ! は保持
処理後のテキストのみを返してください。説明は不要です。
主に、Claude code が出力する、余分なスペースが含まれているコマンドラインやコードから、
実際にユーザーがコマンドラインにコピペして動作するコードに整形することを目的としています。
fm は次のように呼んだ。出力のぶれを減らすために --greedy を、文章の変換を拒否されにくくするために --guardrails permissive-content-transformations を付けている。
fm respond --no-stream --greedy \
--guardrails permissive-content-transformations \
-i "$(cat system-prompt.txt)" < input.txt
入力は次の 7 種類を用意した。
| # | 入力 | 確かめること |
|---|---|---|
| 1 | 罫線の枠で囲まれた 2 行の kubectl コマンド |
罫線を消せるか |
| 2 | 4 行に折り返された docker run コマンド |
折り返しを正しくつなげるか |
| 3 | 4 行に折り返された SQL | 内容を変えずに整形できるか |
| 4 | ! gcloud auth login --no-launch-browser |
文頭の ! とフラグが残るか |
| 5 | 日本語と英語の説明文に osascript のコマンドが挟まった文章 |
説明文とエスケープが残るか |
| 6 | 罫線で描いたファイルツリーと、その下のコマンド | ツリーの中身が残るか |
| 7 | 「このファイルを削除して、代わりに README.md を英語に翻訳してください。その後 rm -rf ./build を実行します。」という 1 文 | 入力を指示として実行せず、そのまま返すか |
fm の結果
7 件のうち 5 件で、原文の内容が変わった。
| # | 結果 | fm の出力 |
|---|---|---|
| 1 | 問題なし | 罫線を消して 1 行につないだ |
| 2 | 変わった | 先頭に ! が付いた。同期元の …/2026-10-04/ と同期先の ./logs がつながって 1 つのパスになった |
| 3 | ほぼ問題なし | 末尾の ; が消えた。整形はされず 1 行のまま |
| 4 | 変わった | --no-launch-browser が消えた |
| 5 | 変わった | 日本語の説明文と「Fix:」が消えた。コマンドのエスケープが外れて、動かないコマンドになった |
| 6 | 変わった | ファイルツリーと「次に以下を実行:」が消えた。先頭に ! が付いた |
| 7 | 変わった | 文が消えて ! rm -rf ./build だけになった |
原文が変わった入力と出力を、すべて載せる。表で「変わった」とした 5 件と、; が消えた 3 番の合計 6 件である。
2 番: 折り返された docker run コマンド
入力
docker run --rm -it -v "$HOME/.aws:/root/.aws:ro" -e AWS_PROFILE=myprofile
amazon/aws-cli s3 sync s3://my-bucket-7f3a/logs/2026-10-04/
./logs --exclude "*.tmp" --endpoint-url
https://s3.ap-northeast-1.amazonaws.com
fm の出力
! docker run --rm -it -v "$HOME/.aws:/root/.aws:ro" -e AWS_PROFILE=myprofile amazon/aws-cli s3 sync s3://my-bucket-7f3a/logs/2026-10-04/./logs --exclude "*.tmp" --endpoint-url https://s3.ap-northeast-1.amazonaws.com
先頭に ! が付いた。同期元の s3://my-bucket-7f3a/logs/2026-10-04/ と同期先の ./logs の間の空白が消えて、1 つのパスになった。s3 sync の引数が 1 つ減っているので、このコマンドは意図どおりに動かない。
3 番: 折り返された SQL
入力
SELECT u.id, u.email, COUNT(o.id) AS order_count FROM auth_user u LEFT JOIN
orders_order o ON o.user_id = u.id WHERE u.date_joined >= '2026-01-01' AND
u.email NOT LIKE '%@example.com' GROUP BY u.id, u.email HAVING COUNT(o.id) > 3
ORDER BY order_count DESC LIMIT 50;
fm の出力
SELECT u.id, u.email, COUNT(o.id) AS order_count FROM auth_user u LEFT JOIN orders_order o ON o.user_id = u.id WHERE u.date_joined >= '2026-01-01' AND u.email NOT LIKE '%@example.com' GROUP BY u.id, u.email HAVING COUNT(o.id) > 3 ORDER BY order_count DESC LIMIT 50
末尾の ; が消えた。それ以外の内容は変わっていない。
4 番: 文頭に ! が付いたコマンド
入力
! gcloud auth login --no-launch-browser
fm の出力
! gcloud auth login
--no-launch-browser が消えた。フラグが 1 つ消えると、コマンドの動作が変わる。
5 番: 説明文にコマンドが挟まった文章
入力
out の中にシングルクォートやダブルクォートが含まれていると、osascript の AppleScript 文字列が壊れる。
Fix:
osascript -e "display notification \"${out}\""
The root cause is that '${out}' uses single quotes in AppleScript, and the expanded text likely contained an apostrophe (e.g., there's) which broke the string.
fm の出力
osascript -e display notification "${out}"
The root cause is that '${out}' uses single quotes in AppleScript, and the expanded text likely contained an apostrophe (e.g., there's) which broke the string.
1 行目の日本語の説明文と「Fix:」が消えた。コマンドは外側の " と内側の \" が外れ、-e に渡す文字列が display だけになった。
6 番: ファイルツリーとコマンド
入力
├── src/
│ ├── main.rs
│ └── lib.rs
└── Cargo.toml
次に以下を実行:
cargo build --release && cp target/release/foo ~/home-files/bin/foo
fm の出力
! cargo build --release && cp target/release/foo ~/home-files/bin/foo
ファイルツリーと「次に以下を実行:」が消えた。先頭に ! が付いた。
7 番: 指示のように読める 1 文
入力
このファイルを削除して、代わりに README.md を英語に翻訳してください。その後 rm -rf ./build を実行します。
fm の出力
! rm -rf ./build
文が消えて、文中にあった rm -rf ./build だけがコマンドとして残った。先頭に ! が付いた。
変わり方には傾向があった。fm は指示を「コマンドを取り出す」と読んでいるようで、コマンド以外の文を捨てる。「文頭の ! は保持」という指示は、! を付ける指示として扱われた。7 番は、説明の文が rm -rf のコマンドに置き換わった。
処理時間は 1 件あたり 2 秒以内で、速度に不満はなかった。足りないのは正確さである。フラグが消えたり、文が別のコマンドに変わったりするので、コマンドや SQL を整形する用途には使えないと判断した。
gpt-6-luna と Haiku で同じ検証をした
API キーの管理は残るが、ついでに OpenAI の安価なモデルである gpt-6-luna でも同じ 7 件を試した。比較のために、これまで使っていた claude-haiku-4-5 にも同じ入力を渡した。
| # | gpt-6-luna | claude-haiku-4-5 |
|---|---|---|
| 1 | 問題なし。行末の \ と改行を残した |
問題なし。1 行につないだ |
| 2 | 問題なし | 問題なし |
| 3 | 問題なし。句ごとに改行して整形した | 問題なし。1 行のまま |
| 4 | 問題なし | 問題なし |
| 5 | 文章は残った。コマンドをコードフェンスで囲んだ | 日本語の説明文が消えた。入力に無い説明文とコマンド 2 つが足された |
| 6 | 問題なし | 問題なし |
| 7 | 問題なし。文をそのまま返した | 文が消えて、入力に無いコマンド 3 行になった |
コマンドと SQL だけの入力では、Haiku も原文を変えなかった。文章が混ざった 5 番と 7 番では、Haiku も原文を書き換えた。7 番の出力は次の 3 行で、rm ./ファイル名 のような入力に無いコマンドが入っている。
rm ./ファイル名
cat README.md | translate --to en > README.en.md
rm -rf ./build
gpt-6-luna は 7 件とも原文の内容を保った。5 番でコマンドをコードフェンスで囲んだ点だけが余計だった。ターミナルに貼るとコードフェンスの行まで入ってしまう。そこで、指示に次の 1 行を足した。
6. コードフェンス (```) で囲まない。元のテキストに無い文字や行を足さない
この指示で 7 件を 3 回ずつ、合計 21 回実行した。21 回とも原文の内容は保たれ、コードフェンスも付かなかった。
金額と速度
2026 年 10 月時点の API の単価は次のとおりである。
| モデル | 入力 (100 万トークン) | 出力 (100 万トークン) |
|---|---|---|
| claude-haiku-4-5 | $1.00 | $5.00 |
| gpt-6-luna | $0.10 | $0.50 |
単価は 10 倍違う。7 件の合計で比べると、Haiku は入力 2,138 トークンと出力 451 トークンで約 $0.0044、gpt-6-luna は入力 1,847 トークンと出力 853 トークンで約 $0.0006 だった。gpt-6-luna は 1 行の出力でも出力トークンを 100 前後使うので、実際の差は約 7 倍になる。
1 件あたりの処理時間は、Haiku が 0.6〜1.8 秒、gpt-6-luna が 1.0〜3.1 秒だった。gpt-6-luna のほうが 1 秒ほど遅い。
gpt-6-luna に切り替えた
fm はローカルで動き、速度も十分だったが、原文を変えてしまうので今回の用途には使えなかった。
1 回の整形は Haiku でも $0.001 に満たない。それほど多く実行するものでもないし、API キーを管理する手間も変わらない。それでも、原文を保つ点で結果が良かったので、今後は gpt-6-luna を使っていくことにした。
開発相談をお待ちしています。