Node.jsのスクリプトに、Windowsのパスをそのまま貼り付けたときの話です。
実行すると、スクリプトの中身が動き出す前に落ちます。
SyntaxError: Invalid hexadecimal escape sequence
パスの綴りは合っていますし、そのフォルダも実在します。それでも落ちます。
問題はパスではなく、パスを文字列として書いたところにあります。そして厄介なのは、エラーにならず黙って別のパスに化ける書き方が混ざっていることです。
- Windowsのパスを貼ると SyntaxError になる理由
- エラーになる頭文字と、黙って化ける頭文字の一覧
- 直し方4つと、どれを選ぶか
- 化けているかどうかを1行で確かめる方法
- JSONファイルに書くときの注意点
原因は、円記号がエスケープの開始記号だからです。
JavaScriptの文字列リテラルでは、バックスラッシュ(Windowsでは円記号として表示されます)が特別な意味を持ちます。
「次の1文字は、そのままの文字ではありません」という合図です。
ところがWindowsのパス区切りも、同じ記号です。
たとえばXAMPPのPHPを呼びたくて、こう書いたとします。
console.log("C:xamppphpphp.exe");これを実行すると、次のようになります。
console.log("C:xamppphpphp.exe");
^^^^
SyntaxError: Invalid hexadecimal escape sequence指されているのは xam の部分です。
x は「16進エスケープ」の開始で、そのうしろに16進数2桁が来る決まりです。x41 と書けば A になります。
ここでは x のうしろが am なので、16進数として読めません。それで構文エラーになります。
フォルダ名が xampp だから起きたのではありません。x で始まるフォルダ名なら何でも起きます。
u で始まるフォルダも落ちます。
u も同じ仲間で、こちらはUnicodeエスケープです。
console.log("C:userstest");SyntaxError: Invalid Unicode escape sequenceここまでは、まだ親切です。実行前に止まるので、間違いに気づけます。
本当に困るのは、エラーが出ないほうです。
同じ書き方でも、フォルダ名の頭文字によってはエラーになりません。
そのかわり、中身がまったく別の文字列になります。
制御文字は目で見えないので、次のコードでは32未満の文字コードを <9> のように数字で出しています。
const show = s => s.split("").map(c =>
c.charCodeAt(0) < 32 ? "<" + c.charCodeAt(0) + ">" : c
).join("");
console.log(show("C:tempbuild"), "/ 長さ", "C:tempbuild".length);
console.log(show("C:Usersguest"), "/ 長さ", "C:Usersguest".length);C:<9>emp<8>uild / 長さ 11
C:Usersguest / 長さ 121行目の <9> はタブ、<8> はバックスペースです。
t と b が、そういう文字として解釈されました。
2行目はもっと静かです。U や g という組み合わせに決まった意味がないので、バックスラッシュだけが消えます。
区切りのないパスができあがり、エラーも警告も出ません。
頭文字ごとの結果をまとめます。
- x, u で始まる … 構文エラーで止まります。
- t, n, r, b, f, v で始まる … タブや改行などに化けます。エラーは出ません。
- それ以外 … バックスラッシュが消えて、区切りがなくなります。エラーは出ません。
Windowsでよく触るフォルダ名を思い浮かべると、temp、node_modules、bin、fonts あたりが2番目に当てはまります。
直し方は4つあります。
どれも結果は同じです。書きやすいものを選んでください。
// 1. スラッシュに書き換える
"C:/temp/build"
// 2. バックスラッシュを2つ重ねる
"C:\temp\build"
// 3. テンプレートリテラルを String.raw で囲む
String.raw`C:tempbuild`
// 4. path で組み立てる
require("path").join("C:\", "temp", "build")実行すると、4つとも同じ文字列になります。
C:/temp/build
C:tempbuild
C:tempbuild
C:tempbuildおすすめは1番のスラッシュです。
Windowsのファイルシステムはスラッシュ区切りも受け付けるので、fs.readFileSync("C:/temp/build/a.txt") はそのまま読めます。
そして何より、書き換えのときにエスケープを数えなくて済みます。
エクスプローラーからコピーしたパスを見た目のまま貼りたいときは、3番の String.raw が便利です。中身に一切手を入れなくてよいので、貼り付けだけで済みます。
化けているかどうかは、長さで分かります。
エラーが出ないパターンに当たると、画面に出したときも正しく見えることがあります。
疑わしいときは、文字数を数えるのが早いです。
console.log(p.length, JSON.stringify(p));C:tempbuild は本来13文字です。11文字なら、区切りが2つとも別の文字に化けています。
ファイルを開こうとして失敗したときは、例外の path プロパティも見てください。
try {
fs.readFileSync(p);
} catch (e) {
console.log(e.code, JSON.stringify(e.path));
}Nodeが実際に探しに行ったパスが出ます。JSON.stringify を通すと、タブや改行が t n として見えるようになります。
ENOENTなのにパスが合っているように見えるときは、たいていここで化けています。
JSONファイルに書くときは、逃げ道がありません。
設定ファイルにパスを書くこともあると思います。
JSONの文字列は、JavaScriptよりも規則が厳しくなっています。
JSON.parse(String.raw`{"p":"C:Users"}`);SyntaxError: Bad escaped character in JSON at position 9JavaScriptでは黙って通っていた U が、JSONでは弾かれます。意味のないエスケープを許さないためです。
一方で t は、JSONでも正しいエスケープです。
JSON.parse(String.raw`{"p":"C:temp"}`).p.length; // 6C:temp は7文字のはずですが、6文字になっています。エラーは出ないまま、タブに化けました。
JSONにWindowsのパスを書くときは、スラッシュにするか、バックスラッシュを2つ重ねるかの二択です。String.raw のような逃げ道はありません。
まとめです。
- Windowsのパスをそのまま文字列に貼ると、区切りがエスケープとして読まれます。
- x, u で始まるフォルダ名なら構文エラーになり、実行前に気づけます。
- t, n, r などで始まると、エラーが出ないまま別の文字に化けます。こちらのほうが厄介です。
- スラッシュに書き換えるのが一番確実です。Windowsでもそのまま動きます。
- 貼り付けたままにしたいときは String.raw を使います。
- JSONではスラッシュか二重のバックスラッシュのどちらかにします。
この記事の動作は、Node.js 24 のWindows環境で確認しています。