自動化スクリプトの出力に reactionsApplied: 12 と出ていた。ファイルを開いたら0件だった。原因は単純で、そのコマンドの既定が dry-run であり、書き込むには --write が要ったというだけである。
問題は「フラグを忘れた」ことではない。忘れたことが出力から分からなかったことである。exit code は 0、サマリは「12件適用」。後続の処理はこの出力を信じて進む。
なぜ気づけないのか
このパターンの CLI は、次の2つを同じ言葉で表している。
- 「12件を適用した」
- 「12件を適用できる(が、していない)」
dry-run の設計としては後者が正しいのに、出力の文言が前者と区別されていない。JSON に "write": false が入ってはいたが、サマリの直前ではなく別のキーにあった。目視で追うときは合計値のほうを見る。
exit code も紛らわしい。dry-run が成功したら 0 を返すのは妥当だが、そうすると cmd && next-step が通ってしまう。
最小の再現
apply.js として保存する。Node 18 以降で動く。
1 2 3 4 5 6 7 8 9 10 11
| "use strict"; const fs = require("node:fs"); const write = process.argv.includes("--write"); const target = "out.json";
const items = [{ id: 1 }, { id: 2 }, { id: 12 }];
if (write) fs.writeFileSync(target, JSON.stringify(items, null, 2));
console.log(JSON.stringify({ ok: true, applied: items.length, write }, null, 2));
|
実行する。
1 2 3
| rm -f out.json node apply.js echo "exit=$? file=$(ls out.json 2>/dev/null || echo なし)"
|
出力はこうなる。
1 2 3 4 5 6
| { "ok": true, "applied": 3, "write": false } exit=0 file=なし
|
ok: true / applied: 3 / exit=0 が揃っているのに、ファイルは無い。
直し方1: 文面で区別する
一行足すだけで、目視で気づけるようになる。
1 2 3 4 5 6 7
| console.log(JSON.stringify({ ok: true, applied: write ? items.length : 0, appliable: items.length, write, note: write ? `${target} に書き込んだ` : "dry-run: 何も書いていない(--write で実行する)" }, null, 2));
|
applied は実際に起きたことだけを数え、「できたはずの件数」は別のキーに置く。ログを grep したときにも効く。
直し方2: exit code を分ける
後続をコマンド連結で繋いでいるなら、dry-run を 0 以外にする手もある。
1 2
| if (!write && items.length > 0) process.exit(3);
|
1 2
| node apply.js; echo "dry-run exit=$?" node apply.js --write; echo "write exit=$?"
|
1 2
| dry-run exit=3 write exit=0
|
3 を選んだのは、1(エラー)や2(引数不正)と混ざらないようにするためである。cmd && next は止まり、cmd; next は進む。意味づけを README に1行書いておく。
直し方3: 呼び出し側で結果を検証する
CLI を直せない場合(他人のツール、ベンダー製)は、呼び出し側で確かめる。「実行した」ではなく「変わった」を見る。
1 2 3 4
| before=$(md5sum out.json 2>/dev/null | cut -d" " -f1) node apply.js --write > /dev/null after=$(md5sum out.json 2>/dev/null | cut -d" " -f1) [ "$before" != "$after" ] && echo "変更あり" || { echo "変更なし: 適用されていない"; exit 1; }
|
自動化の中でこれを挟むと、フラグの取り違えは次の1回で止まる。
実際に試す
前提: Node.js 18 以降、bash(Windows なら Git Bash)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| mkdir -p /tmp/dryrun-demo && cd /tmp/dryrun-demo cat > apply.js <<'EOF' "use strict"; const fs = require("node:fs"); const write = process.argv.includes("--write"); const items = [{ id: 1 }, { id: 2 }, { id: 12 }]; if (write) fs.writeFileSync("out.json", JSON.stringify(items, null, 2)); console.log(JSON.stringify({ ok: true, applied: write ? items.length : 0, appliable: items.length, write, note: write ? "out.json に書き込んだ" : "dry-run: 何も書いていない(--write で実行する)" }, null, 2)); if (!write && items.length > 0) process.exit(3); EOF
rm -f out.json node apply.js; echo "exit=$?" ls out.json 2>/dev/null || echo "out.json は無い"
node apply.js --write; echo "exit=$?" ls -l out.json
|
期待される出力(抜粋)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "ok": true, "applied": 0, "appliable": 3, "write": false, "note": "dry-run: 何も書いていない(--write で実行する)" } exit=3 out.json は無い { "ok": true, "applied": 3, "appliable": 3, "write": true, "note": "out.json に書き込んだ" } exit=0
|
まとめ
既定を dry-run にするのは安全側の設計で、それ自体は正しい。問題は、dry-run の出力が本番実行の出力と見分けられないことにある。
applied のような「起きたこと」を表す数は、起きていないときは 0 にする
- 「できたはずの件数」は別のキーに分ける
- dry-run に専用の exit code を与え、コマンド連結で先へ進ませない
- 直せないツールは、呼び出し側で「変わったか」を確かめる