【Node.js】pathモジュールの使い方|join・resolve・basename・__dirname

【Node.js】pathモジュールの使い方|join・resolve・basename・__dirname Node.js

Node.jsでファイルのパスを組み立てるとき、文字列を+でつなぐと、区切り文字(/\)の付け忘れや二重付け、WindowsとLinuxの違いでバグが起きます。これを安全に処理してくれるのが標準のpathモジュールです。

使い方はシンプルですが、最大の混乱はpath.joinpath.resolveの違いです。どちらもパスをつなぎますが、joinは単純な連結、resolve絶対パスを作ります。この違いを知らないと、意図しない場所を指してしまいます。この記事では、実機のNode.jsで確認しながら、pathの使い方を整理します。

先に結論

  • パスの連結はpath.join(...)。区切り文字を自動で正しく入れてくれます。
  • path.joinは相対的な連結、path.resolveは絶対パスを作ります。用途で使い分けます。
  • ファイル名はpath.basename、フォルダはpath.dirname、拡張子はpath.extnameです。
  • まとめて分解するならpath.parse{ root, dir, base, ext, name }が得られます。
  • ファイルの場所を基準にするなら、path.join(__dirname, "...")を使います。
  • 区切り文字はOSで変わります(Windowsは\、Linuxは/)。常に/で扱いたいならpath.posixです。

ESモジュールで__dirnameを使う方法はrequireとimportの違い、パスを使ったファイル操作はfsでファイルを読み書きする方法、依存管理はnpmとpackage.jsonの基礎もあわせて参考になります。

スポンサーリンク

なぜpathモジュールを使うのか

パスを文字列の+でつなぐと、思わぬバグが起きます。区切り文字が二重になったり、OSによって/\が混ざったりするためです。

why-path.js
// NG: 文字列結合は区切りのミスやOS差が起きやすい
const dir = "logs/";
const file = "/app.log";
const p1 = dir + file;        // "logs//app.log"(区切りが二重)

// OK: path.join なら区切りを正しく処理する
const path = require("path");
const p2 = path.join("logs", "app.log");   // logs\app.log(環境に合った区切り)

path.joinは、余分な区切りを取り除き、その環境に合った区切り文字でつないでくれます。手で文字列を組み立てるより、はるかに安全です。

path.joinでパスをつなぐ

path.joinは、渡したパスの断片を区切り文字でつなぎます。..(上の階層)なども正しく整理してくれます。

join.js
const path = require("path");

// 複数の断片をつなぐ(Windowsでの実行結果)
path.join("foo", "bar", "baz.txt");   // foo\bar\baz.txt

// .. は整理される(bar を1つ上がる)
path.join("foo", "bar", "..", "baz.txt");   // foo\baz.txt

// 余分な区切りもまとめられる
path.join("logs/", "/app.log");   // logs\app.log

実機(Windows)では、path.join("foo", "bar", "baz.txt")foo\bar\baz.txtになりました。Linux環境なら区切りは/になり、foo/bar/baz.txtです。区切り文字をコードに直接書かず、path.joinに任せることで、どのOSでも正しく動きます。

【最重要】joinとresolveの違い(相対と絶対)

path.joinとよく似たpath.resolveがありますが、性質が違います。join断片を連結するだけresolve絶対パスを組み立てるのが目的です。

join-vs-resolve.js
const path = require("path");

// join: そのまま連結(相対パスのまま)
path.join("a", "b");      // a\b

// resolve: 現在の作業フォルダを起点に絶対パスにする
path.resolve("a", "b");   // C:\現在のフォルダ\a\b

// resolve は、途中に絶対パスがあるとそれ以前を無視する
path.resolve("foo", "C:\\abs", "bar");   // C:\abs\bar

// 「アプリの起点からの絶対パス」がほしいときは resolve
const root = path.resolve("config", "db.json");
絶対パスがほしいならresolve、連結だけならjoin

実機で確認したところ、path.join("a", "b")a\b(相対)、path.resolve("a", "b")C:\現在のフォルダ\a\b(絶対)になりました。resolveは、指定がすべて相対なら現在の作業フォルダ(カレントディレクトリ)を起点にします。さらに、途中に絶対パスが現れると、それより前は無視されます。「今いるフォルダに依存しない確実な絶対パスがほしい」なら、次に説明する__dirnameと組み合わせます。

ファイル名・フォルダ・拡張子を取り出す

パスから一部分を取り出すには、basename(ファイル名)、dirname(フォルダ)、extname(拡張子)を使います。

basename-dirname-extname.js
const path = require("path");
const p = "/dir/sub/report.txt";

path.basename(p);          // report.txt(ファイル名)
path.basename(p, ".txt");  // report(拡張子を除く)
path.dirname(p);           // /dir/sub(フォルダ部分)
path.extname(p);           // .txt(拡張子)
path.extname("README");    // ""(拡張子なしは空文字)

実機でも、path.basename("/dir/sub/report.txt")report.txt、第2引数に".txt"を渡すと拡張子を除いたreportになりました。拡張子で処理を分けたいときはpath.extnameが便利です。拡張子の無いファイルでは空文字が返ります。

パスを分解する(path.parse)

パスの各部分をまとめて取り出すならpath.parseが便利です。オブジェクトとして、ルート・フォルダ・ファイル名・拡張子・拡張子なしの名前が得られます。

parse.js
const path = require("path");

const info = path.parse("C:\\dir\\report.txt");
console.log(info);
// {
//   root: "C:\\",
//   dir:  "C:\\dir",
//   base: "report.txt",
//   ext:  ".txt",
//   name: "report"
// }

console.log(info.name);   // report
console.log(info.ext);    // .txt

実機でも、path.parserootdirbaseextnameを持つオブジェクトを返しました。name(拡張子なしのファイル名)とextを組み合わせれば、「拡張子を変えた新しいファイル名」なども簡単に作れます。逆に部品からパスを組み立てるpath.formatもあります。

__dirnameを基準に絶対パスを作る

スクリプトと同じフォルダにある設定ファイルを読む、といった処理では、そのファイルの場所を基準にパスを作ります。CommonJSでは__dirname(このファイルがあるフォルダ)が使えます。

dirname-absolute.js
const path = require("path");
const fs = require("fs");

// このスクリプトと同じフォルダの config.json を読む
const configPath = path.join(__dirname, "config.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));

// 1つ上のフォルダの data を指す
const dataDir = path.join(__dirname, "..", "data");
ESモジュールには__dirnameが無い

注意点として、ESモジュール(.mjs"type": "module")では__dirnameが使えません。代わりにimport.meta.urlから作ります。import { fileURLToPath } from "node:url"を使い、path.dirname(fileURLToPath(import.meta.url))とすると同じものが得られます。詳しくはrequireとimportの違いで解説しています。__dirnameを基準にすれば、どこから実行しても同じファイルを正しく指せます。

OSの区切り文字(sep / posix)

区切り文字はOSによって違います。Windowsは\、Linux・macOSは/です。現在の区切り文字はpath.sepで取得できます。常に/で扱いたい場合はpath.posixを使います。

sep-posix.js
const path = require("path");

path.sep;   // Windows は \ 、Linux は /(区切り文字そのもの)

// 常に / で連結したい(URL組み立てなど)
path.posix.join("foo", "bar", "baz.txt");   // foo/bar/baz.txt

// 常に \ で扱いたい(Windows形式を強制)
path.win32.join("foo", "bar");   // foo\bar

// 絶対パスかどうかの判定
path.isAbsolute("C:\\x");   // true
path.isAbsolute("foo");      // false

実機(Windows)では、path.sep\path.posix.joinは常に/区切りのfoo/bar/baz.txtを返しました。URLのパス部分を組み立てるときは、OSに関係なく/を使うpath.posixが便利です。通常のファイル操作では、OSに合わせてくれるpath.joinをそのまま使えば問題ありません。

よくある失敗

パスを文字列の+でつないで区切りを間違える

+は区切りの二重付けや付け忘れが起きます。path.joinを使えば、区切りを正しく処理してくれます。

joinとresolveを取り違える

joinは相対的な連結、resolveは絶対パスの組み立てです。確実な絶対パスがほしいならresolve__dirnameとの組み合わせを使います。

区切り文字を\や/で直接書く

OSによって区切りが違います。直接書かず、path.joinに任せます。URL用ならpath.posixです。

ESモジュールで__dirnameを使ってエラー

ESモジュールに__dirnameはありません。path.dirname(fileURLToPath(import.meta.url))で作ります。

カレントディレクトリ依存のパスで動かなくなる

相対パスは「実行したフォルダ」が基準になります。実行場所に左右されたくないなら、__dirnameを基準に絶対パスを作ります。

よくある質問

Qpath.joinとpath.resolveはどう違いますか?
Apath.joinは断片を区切り文字で連結するだけで、結果は相対パスのままです。path.resolveは絶対パスを組み立て、すべて相対なら現在の作業フォルダを起点にします。確実な絶対パスがほしいときはresolve__dirnameとの組み合わせを使います。
Q区切り文字は / と \\ のどちらを書けばいいですか?
Aどちらも直接書かず、path.joinに任せます。Windowsでは\\、Linuxでは/と、その環境に合った区切りを自動で使ってくれます。URLのパスを組み立てるときだけ、常に/path.posix.joinを使います。
Qファイルの拡張子だけ取り出すには?
Apath.extname("report.txt").txtが得られます。拡張子を除いたファイル名はpath.basename(p, ".txt")path.parse(p).nameで取り出せます。
Qスクリプトと同じフォルダのファイルを読むには?
Apath.join(__dirname, "ファイル名")で絶対パスを作ります。__dirnameはそのファイルがあるフォルダを指すため、どこから実行しても同じファイルを正しく読めます。ESモジュールではimport.meta.urlから作ります。
QESモジュールで__dirnameが使えません。
AESモジュールには__dirnameがありません。import { fileURLToPath } from "node:url"import { dirname } from "node:path"を使い、dirname(fileURLToPath(import.meta.url))で同じものを作ります。

まとめ

  • パスの連結はpath.joinに任せ、区切り文字を直接書きません。
  • joinは相対的な連結、resolveは絶対パスの組み立てです。
  • 取り出しはbasenamedirnameextname、まとめて分解はpath.parseです。
  • ファイルの場所を基準にするならpath.join(__dirname, "...")を使います。
  • 区切り文字はOS依存(path.sep)。常に/ならpath.posixです。
  • ESモジュールでは__dirnameの代わりにimport.meta.urlから作ります。

Node.jsのパス操作は、文字列結合をやめてpathモジュールに任せるだけで、OS差や区切りのバグから解放されます。joinresolveの違い、そして__dirnameを基準にする習慣を押さえれば、どの環境でも安定して動くコードを書けます。