既定が dry-run のCLIは「成功した」と言って何も書かない

自動化スクリプトの出力に 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));

// これが事故のもと: write の値によらず同じ文面を出す
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
// dry-run は「未適用あり」を意味する専用コードにする
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 を与え、コマンド連結で先へ進ませない
  • 直せないツールは、呼び出し側で「変わったか」を確かめる

既定が dry-run のCLIは「成功した」と言って何も書かない
https://blog.hashito.biz/2026/09/23/dry-run-default-cli-reports-success-writes-nothing/
著者
hashito
作成日
2026年9月23日
著作権