定義
インストールした CLI を実行したのに、エラーも出力も成果物もなく終了コード 0 で終わった — そんな経験があるならこの概念が必要だ。
main ガード(直接実行ガード) は、スクリプトが「いま自分は直接実行されたのか、それとも他のモジュールから import されたのか」を判別する定型句だ。Node の ESM では import.meta.url === pathToFileURL(process.argv[1]).href、Python では __name__ == "__main__" の系列がその役割を担う。問題は、これが パス文字列の比較 だという点にある。実行経路がシンボリックリンクだと二つの値がずれ、ガードが静かに false になり、本体が一行も実行されないまま正常終了を装う。
なぜ必要か
npm i -g でインストールした CLI は実ファイルではなく、~/.nvm/versions/node/*/bin/<cmd> のような シンボリックリンク として置かれる。実行すると process.argv[1] はリンクの経路になり、import.meta.url はリンクが指す実ファイルの経路になる。二つの文字列が異なるのでガードが止めてしまう。
この失敗モードが特に悪いのは、失敗の合図が一つもない ことだ。stderr は空で終了コードも 0 なので CI でも通ってしまう。利用者はツールのバグを疑えず、自分の入力が間違っていると思い込んで時間を溶かす。
動作原理
npm i -g mytool→bin/mytool.mjsを指すシンボリックリンクが PATH にできる。- 利用者が
mytool logを実行する。 - Node はリンクを辿って実ファイルを読み込む →
import.meta.urlは 実経路。 - しかし
process.argv[1]は利用者が入力したままの リンク経路。 import.meta.url === pathToFileURL(process.argv[1]).hrefが false → 本体は未実行。- モジュールのトップレベルだけが動き、正常終了(exit 0)する。
| 値 | シンボリックリンク実行時 |
|---|---|
process.argv[1] | /Users/me/.nvm/.../bin/mytool |
import.meta.url | file:///Users/me/dev/mytool/bin/mytool.mjs |
| 比較結果 | false → 静かな no-op |
実務適用
診断は三行で終わる。
ls -l "$(which mytool)" # シンボリックリンクか確認
node "$(readlink -f "$(which mytool)")" log # 実経路で直接実行
# ここで動くなら原因は main ガードで確定
修正は両側を実経路に正規化する。
import { realpathSync } from "node:fs";
import { pathToFileURL } from "node:url";
const isDirectRun =
import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
if (isDirectRun) await main(process.argv.slice(2));
より単純な代替は、ガードそのものをなくすことだ。bin/cli.mjs はライブラリを import して即実行するだけにし、ライブラリ側のファイルには実行コードを置かない。比較すべき経路自体が消える。
トレードオフ
- realpath 正規化: 一行で済むが、存在しない経路では
realpathSyncが例外を投げる。process.argv[1]がない REPL 実行を考慮した防御が要る。 - bin エントリの分離: 最も堅牢で意図も明確。代わりにファイルが一つ増え、パッケージの
binフィールドを直す必要がある。 - ガードを残してログを出す: ガードに阻まれたとき stderr に一行残せば、少なくとも静かな失敗は防げる。根本解決ではない。
使ってはいけない場合
- ライブラリとして import される可能性がまったくない純粋な実行スクリプト。ガード自体が不要な複雑さだ。
- 実行経路が常に固定のコンテナのエントリポイント。経路がシンボリックリンクを経由しないなら問題自体が存在しない。
よくある間違い
- exit 0 を成功と信じる。 出力も成果物もないなら成功ではない。失敗は必ず見えるようにする。
- パス文字列を直接比較する。 シンボリックリンク以外にも、macOS の
/private接頭辞、大文字小文字を保存するファイルシステム、Windows の短縮パスでずれる。 - シェルの出力だけで判断する。 ラッパーやプロキシ、フィルタが挟まったシェルでは出力が加工され診断を誤らせうる。ファイルの内容は別の手段で確認し直す。
whichが返した経路を実経路と勘違いする。readlink -fでもう一段ほどく必要がある。
関連概念
- shell-word-splitting-quoting — シェル層が診断をひっくり返すもう一つの例
- serverless-stateless-execution — 実行環境への仮定が崩れる系統