Windows + Git Bash という組み合わせで、こんな経験はないでしょうか。
- Git Bash で作業用のファイルを作った
-
catで中身も確認できた - そのパスを Node.js に渡したら
ENOENT: no such file or directoryで落ちた
私は先日これで手を止めました。原因が分かってしまえば単純なのですが、知らないと「なぜ?」で数分持っていかれる種類の罠です。この記事では、症状の見分け方・原因・その場で使える対処を2通りまとめます。
症状:確かに作ったのに「そんなファイルは無い」
Git Bash 側でファイルを置いて、そのパスをそのまま Node.js に渡すと、返ってくるのはこれです。
ENOENT: no such file or directory, open 'C:\tmp\__nope.json'
ここに違和感の入口があります。渡したパスの先頭にドライブレターなど付けていないのに、エラーメッセージには C:\tmp\ と書かれている。 渡したものと、探しに行った先が別物になっているわけです。
Python でも同じことが確認できます。「そのフォルダはあるか」「絶対パスに直すとどこか」を聞いてみると、返事はこうでした。
exists: False
abspath: C:\tmp
「そんなフォルダは無い」と言いつつ、絶対パスに直すと C:\tmp になっている。ここが核心です。
原因:ふたつの世界が、同じ名前の別の場所を見ている
Git Bash(MSYS2 / MinGW をもとにした環境)は、Unix 風のファイル構造を Windows の上に被せて見せています。この環境の中では、/ や /tmp といった Unix 的な場所が、Windows の実在フォルダに割り当てられている。
ところが、そこから起動される Node.js や Python は Windows ネイティブのプログラムです。Git Bash が被せた地図を持っていません。彼らにとっての先頭のスラッシュは、単に「いま作業しているドライブのいちばん上」という意味でしかない。だから、Git Bash が「テンポラリ領域」と読むはずのパスを、Windows 側は C: ドライブ直下として素直に解釈します。
| 誰がパスを解決するか | 何を見るか | 結果 |
|---|---|---|
Git Bash 内の ls / cat
|
被せた地図 | ファイルは見える |
| Git Bash から起動された Node / Python | 地図を持っていない | 別の場所を探しに行く |
つまり、シェルの中で cat して中身が出てくることは、Node が読めることの保証になりません。 ここが一番だまされやすいところだと思います。
確かめ方:cygpath に翻訳させる
Git Bash には、その地図を教えてくれるコマンドが最初から入っています。
$ cygpath -w /tmp
C:\Users\<ユーザー名>\AppData\Local\Temp
-w は「Windows 形式に直して」という指定です。逆方向(Windows 形式 → Unix 形式)は -u を付ければできます。
実際の割り当て先はユーザー環境変数などによって変わるので、自分の環境の答えは一度自分で打って確かめてください。少なくとも「シェルが見ている先」と「Windows 側が見る先」がずれていることは、この1行で目に見えるようになります。
対処1:渡す直前に Windows 形式へ翻訳する
いちばん素直な直し方です。シェルの中では今までどおりのパスで扱い、Windows ネイティブのプログラムに渡す瞬間だけ翻訳します。
WIN_PATH=$(cygpath -w /tmp/work/data.json)
node script.js "$WIN_PATH"
ポイントは、翻訳する場所を「外部プログラムに渡す一歩手前」に限定することです。
ファイルを作った時点で変換して持ち回ると、今度はシェル側のコマンドがバックスラッシュ混じりのパスを受け取って別の問題を起こします。境界をまたぐところでだけ翻訳する、と覚えておくと迷いません。
対処2:はじめから Windows 側の場所を使う
そもそも両者が同じ場所を見るようにしてしまう手もあります。私は結局こちらを選ぶことが多いです。翻訳の付け忘れが起きないからです。
- 作業用フォルダを、Windows から見ても Git Bash から見ても同じ実体になる場所(プロジェクト配下など)に決め打ちする
- そのフォルダのパスを、はじめから Windows 形式で持ち回る
特に、ファイルを書く側と読む側が別々の道具になる場合(シェルで書いて Node で読む、エディタで書いて Python で読む、など)は、最初から Windows 形式に統一しておくほうが事故が減ります。「あとで変換すればいい」は、たいてい1回どこかで忘れます。
ついでに踏んだ:引数が長すぎてコマンドが起動しない
この件を追いかけている最中に、もうひとつ別の壁にぶつかりました。ファイルの中身をコマンドラインに直接書いて渡そうとしたら、実行前にこう止められたのです。
ENAMETOOLONG: name too long, uv_spawn
プログラムを起動する前の段階で失敗しています。OS には「1回のコマンドに渡せる長さ」の上限があり、それを超えたという意味です。中身が数百行あるようなデータをコマンドラインに直接埋め込むと、環境によってはあっさりこの上限に当たります。
対処はシンプルで、長い中身はコマンドラインではなくファイルにして、ファイルのパスだけを渡す。そしてそのファイルを Windows 形式のパスに置く。結局この記事の本題と同じ結論に戻ってきます。
まとめ
| 症状 | 原因 | 対処 |
|---|---|---|
作ったはずのファイルが ENOENT になる。エラー文のパスに C:\ が付いている |
Windows ネイティブのプログラムが、先頭のスラッシュをドライブ直下と解釈している |
cygpath -w で変換してから渡す |
シェルで cat すると読めるのに、Node や Python からは読めない |
シェルは被せた地図を見ている。外部プログラムは見ていない | 「シェルで読めた」を動作確認の証拠にしない |
| 変換したら今度はシェル側のコマンドが動かない | バックスラッシュ形式のパスをシェル側に持ち込んでいる | 変換は外部プログラムに渡す引数の位置だけで行う |
| ENAMETOOLONG でプログラムが起動すらしない | コマンドラインに渡せる長さの上限を超えている | 中身をファイルに書き出し、パスだけを渡す |
おわりに
この手の問題がやっかいなのは、どちらの側も嘘をついていないところだと思います。シェルは正しく自分の地図を読んでいるし、Node も正しく自分のルールでパスを解釈している。ただ、地図が2枚あることを私が忘れていただけでした。
「確かにあるのに無いと言われた」ときは、まず cygpath -w を1行打つ。それで済むことが多いはずです。