path.join() はパスのセグメントを正しい区切り文字でつなぎ、.. や重複したスラッシュを整理しますが、最初が相対セグメントなら結果も相対パスのままです。path.resolve() は同じ処理を右から左へ行い、必ず絶対パスを返します。現在の作業ディレクトリから始まり、途中に絶対パスのセグメントがあればそこからやり直します。パスを組み立てるなら join、絶対パスが必要なら resolve を使います。
users/ada/notes.txt、次に users/grace/todo.txt(.. で ada が消えました)、そして /srv/app/public/index.html と src/lib/utils.js が表示されました。余分なスラッシュと空のセグメントは消えています。join は /public/ を新しい起点として扱わず、単なる次のセグメントとして扱います。
path.joinとpath.resolveの違い
違いが出るのは、セグメントが / で始まるときと、最初のセグメントが相対パスのときです。
途中に /public があると、join は /srv/app/public/img を作り、resolve はそれより前をすべて捨てて /public/img を作りました。.. の扱いはどちらも同じです。最後の2行を見ると、join は public/index.html を相対パスのまま残し、resolve は現在のフォルダを前に付けて絶対パスにしたことがわかります。
path.join | path.resolve | |
|---|---|---|
| 結果 | 入力と同じく相対パスか絶対パス | 常に絶対パス |
/ で始まるセグメント | ただのセグメントの1つ | そこからパスをやり直す |
| 起点 | なし | 現在の作業ディレクトリ |
| 引数なし | '.' | 現在の作業ディレクトリ |
| 主な使い道 | 部品からパスを組み立てる | ユーザー入力や設定を絶対パスにする |
basename、dirname、extname、parse
これらはパスを分解します。見ているのは文字列だけなので、ファイルが存在する必要はありません。
cat.final.jpg、cat.final、/home/ada/photos、.jpg が表示されました。extname が取るのは最後のドットより後ろの部分だけです。path.parse は全部を一度に返し(root、dir、base、ext、name)、path.format はそのオブジェクトを文字列に戻します。
__dirnameと__filename
CommonJSのファイルでは、__dirname はそのファイルがあるフォルダの絶対パスで、__filename はファイル自体のパスです。'data.json' のような相対パスは、ユーザーが node を実行した場所からの相対になるので、誰かが別のフォルダからスクリプトを起動した途端に壊れます。代わりに __dirname からパスを組み立てましょう。
config.json dark と index.js が表示されました。フルパスはマシンごとに違います。だからこそ手で書かずに path.join(__dirname, ...) で組み立てるのです。
ESモジュールの__dirname:import.meta.dirname
ESモジュールには __dirname がなく、使うと ReferenceError: __dirname is not defined in ES module scope が投げられます。Node.js 20.11以降は import.meta.dirname と import.meta.filename がその代わりです。
index.js と data/users.json が表示されました。20.11より前のNode.jsでは、モジュールのURLからフォルダを求めます。node:url の fileURLToPath を使って path.dirname(fileURLToPath(import.meta.url)) と書きます。
文字列でパスをつながない理由
'data' + '/' + file はmacOSやLinuxでは動きますが、区切り文字が \ のWindowsでは失敗しますし、..、二重のスラッシュ、スラッシュの抜けにも対処しません。path は動いているシステムのルールを使います。Windowsでの結果は、どのマシンからでも path.win32 で確認できます。
const path = require('path');
path.posix.join('users', 'ada', 'notes.txt'); // 'users/ada/notes.txt'
path.win32.join('users', 'ada', 'notes.txt'); // 'users\\ada\\notes.txt'
path.sep; // '/' on macOS and Linux, '\\' on Windows
path.join の結果をfs.readFileに渡せば、同じコードがどのシステムでもファイルを読めます。
よくある間違い
extname が二重の拡張子を理解すると思う。 path.extname('archive.tar.gz') は '.gz' で、.env のようなドットファイルには拡張子がまったくありません。path.extname('.env') は '' です。末尾全体が大事なときは、名前の終わりを自分で確認してください。
join がユーザーをフォルダの中にとどめてくれると信じる。 path.join('/srv/app', '../../etc/passwd') は '/etc/passwd' です。リクエストで指定されたファイルを読む前に、パスを解決して、それがまだ自分のフォルダで始まっているかを確認しましょう。
よくある質問
__dirnameとprocess.cwd()の違いは何ですか?
__dirname は今のソースファイルがあるフォルダで、変わることはありません。process.cwd() はユーザーが node を実行したフォルダです。両者が一致するのは、プログラムを自分のフォルダから起動したときだけです。
__dirnameと組み合わせるならpath.joinとpath.resolveのどちらですか?
path.join(__dirname, 'data.json') ならどちらでも動きます。__dirname はもともと絶対パスだからです。ユーザーが入力したパスのように、セグメント自体が絶対パスかもしれず、そちらを優先させたいときは resolve を使います。
URLにpathモジュールは使えますか?
使わないでください。URLは常に / を使い、.. やクエリ文字列に独自のルールがあります。new URL('../img/logo.png', 'https://example.com/docs/page') のように URL クラスを使いましょう。