ファイルパスの操作はサーバーサイド開発において頻繁に発生する処理ですが、OSごとにパス区切り文字が異なるため、直接文字列を連結するとクロスプラットフォーム対応に問題が生じます。Node.jsのpathモジュールは、これらのOS間の違いを吸収し、一貫したパス操作を実現するための標準ライブラリです。
本記事では、pathモジュールの主要なAPIを網羅的に解説し、実践的なユースケースとともにクロスプラットフォーム開発のベストプラクティスを紹介します。
実行環境#
| 項目 |
バージョン |
| Node.js |
20.x LTS以上 |
| npm |
10.x以上 |
| OS |
Windows/macOS/Linux |
pathモジュールの基本#
pathモジュールはnode:pathという形式でインポートします。
1
2
3
4
5
|
// CommonJS
const path = require('node:path');
// ES Modules
import path from 'node:path';
|
このモジュールは実行中のOSに応じて、適切なパス区切り文字や処理ロジックを自動的に選択します。
WindowsとPOSIXの違い#
pathモジュールのデフォルト動作は、Node.jsが動作しているOSによって異なります。
パス区切り文字の違い#
| OS |
パス区切り文字 |
例 |
| Windows |
\(バックスラッシュ) |
C:\Users\name\file.txt |
| POSIX(macOS/Linux) |
/(スラッシュ) |
/home/name/file.txt |
path.sep#
path.sepはプラットフォーム固有のパス区切り文字を提供します。
1
2
3
4
5
6
7
8
9
10
|
import path from 'node:path';
console.log(path.sep);
// Windows: '\\'
// POSIX: '/'
// パスをセグメントに分割
const filePath = 'foo/bar/baz';
console.log(filePath.split(path.sep));
// POSIX: ['foo', 'bar', 'baz']
|
path.delimiter#
path.delimiterは環境変数PATHなどで使用される区切り文字を提供します。
1
2
3
4
5
6
7
8
9
10
11
|
import path from 'node:path';
console.log(path.delimiter);
// Windows: ';'
// POSIX: ':'
// 環境変数PATHを配列に分割
const paths = process.env.PATH.split(path.delimiter);
console.log(paths);
// Windows: ['C:\\Windows\\system32', 'C:\\Windows', ...]
// POSIX: ['/usr/bin', '/bin', '/usr/sbin', ...]
|
パス結合API#
path.join()#
path.join()は複数のパスセグメントをプラットフォーム固有の区切り文字で結合し、正規化されたパスを返します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import path from 'node:path';
// 基本的な結合
console.log(path.join('/foo', 'bar', 'baz/asdf', 'quux'));
// '/foo/bar/baz/asdf/quux'
// 相対パス(..)を含む結合
console.log(path.join('/foo', 'bar', 'baz/asdf', 'quux', '..'));
// '/foo/bar/baz/asdf'
// 空文字列はカレントディレクトリ(.)として扱われる
console.log(path.join('foo', '', 'bar'));
// 'foo/bar'
|
path.join()の特徴は以下の通りです。
- 相対パス(
..や.)を解決する
- 連続するスラッシュを単一の区切り文字に正規化する
- 結果が空文字列の場合は
.を返す
path.resolve()#
path.resolve()は右から左へパスセグメントを処理し、絶対パスを構築します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import path from 'node:path';
// 相対パスから絶対パスを構築
console.log(path.resolve('/foo/bar', './baz'));
// '/foo/bar/baz'
// 途中で絶対パスが現れると、それ以前のセグメントは無視される
console.log(path.resolve('/foo/bar', '/tmp/file/'));
// '/tmp/file'
// 相対パスのみの場合、カレントワーキングディレクトリを起点とする
console.log(path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif'));
// 例: '/home/user/project/wwwroot/static_files/gif/image.gif'
|
path.join()とpath.resolve()の違い
両者の違いを理解することは重要です。
1
2
3
4
5
6
7
8
9
|
import path from 'node:path';
// path.join() は単純に連結
console.log(path.join('/foo', '/bar'));
// '/foo/bar'
// path.resolve() は右から絶対パスを探す
console.log(path.resolve('/foo', '/bar'));
// '/bar' (/barが絶対パスなので、/fooは無視される)
|
flowchart LR
subgraph "path.join()"
A1["/foo"] --> B1["結合"]
A2["bar"] --> B1
B1 --> C1["/foo/bar"]
end
subgraph "path.resolve()"
D1["/foo"] --> E1["右から処理"]
D2["/bar"] --> E1
E1 --> F1["/bar<br>(絶対パス発見で終了)"]
endパス分解API#
path.dirname()#
path.dirname()はパスからディレクトリ部分を抽出します。
1
2
3
4
5
6
7
8
9
10
|
import path from 'node:path';
console.log(path.dirname('/foo/bar/baz/asdf/quux'));
// '/foo/bar/baz/asdf'
console.log(path.dirname('/foo/bar/baz/'));
// '/foo/bar'
console.log(path.dirname('file.txt'));
// '.'
|
path.basename()#
path.basename()はパスから最後の部分(ファイル名)を抽出します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import path from 'node:path';
// ファイル名を取得
console.log(path.basename('/foo/bar/baz/asdf/quux.html'));
// 'quux.html'
// 拡張子を除外してファイル名を取得
console.log(path.basename('/foo/bar/baz/asdf/quux.html', '.html'));
// 'quux'
// 末尾の区切り文字は無視される
console.log(path.basename('/foo/bar/baz/'));
// 'baz'
|
path.extname()#
path.extname()はパスから拡張子を抽出します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import path from 'node:path';
console.log(path.extname('index.html'));
// '.html'
console.log(path.extname('index.coffee.md'));
// '.md' (最後のドット以降)
console.log(path.extname('index.'));
// '.'
console.log(path.extname('index'));
// '' (拡張子なし)
console.log(path.extname('.index'));
// '' (隠しファイル扱い)
console.log(path.extname('.index.md'));
// '.md'
|
パス解析と構築#
path.parse()#
path.parse()はパス文字列を構成要素に分解したオブジェクトを返します。
1
2
3
4
5
6
7
8
9
10
11
12
|
import path from 'node:path';
// POSIXの例
const parsed = path.parse('/home/user/dir/file.txt');
console.log(parsed);
// {
// root: '/',
// dir: '/home/user/dir',
// base: 'file.txt',
// ext: '.txt',
// name: 'file'
// }
|
パス構造を図で表すと以下のようになります。
graph TB
subgraph "path.parse() の結果"
A["root: '/'"]
B["dir: '/home/user/dir'"]
C["base: 'file.txt'"]
D["name: 'file'"]
E["ext: '.txt'"]
end
F["'/home/user/dir/file.txt'"] --> A
F --> B
F --> C
C --> D
C --> EWindowsパスの場合は以下のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
|
import path from 'node:path';
// Windowsの例(path.win32を使用)
const parsed = path.win32.parse('C:\\path\\dir\\file.txt');
console.log(parsed);
// {
// root: 'C:\\',
// dir: 'C:\\path\\dir',
// base: 'file.txt',
// ext: '.txt',
// name: 'file'
// }
|
path.format()はpath.parse()の逆操作で、オブジェクトからパス文字列を構築します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import path from 'node:path';
// 基本的な使用例
const filePath = path.format({
root: '/',
dir: '/home/user/dir',
base: 'file.txt'
});
console.log(filePath);
// '/home/user/dir/file.txt'
// name + ext で base を省略
const anotherPath = path.format({
root: '/',
name: 'file',
ext: '.txt'
});
console.log(anotherPath);
// '/file.txt'
|
プロパティの優先順位に注意してください。
dirが指定されるとrootは無視される
baseが指定されるとnameとextは無視される
パス検証と変換#
path.isAbsolute()#
path.isAbsolute()はパスが絶対パスかどうかを判定します。
1
2
3
4
5
6
7
8
9
10
11
12
|
import path from 'node:path';
// POSIX
console.log(path.isAbsolute('/foo/bar')); // true
console.log(path.isAbsolute('/baz/..')); // true
console.log(path.isAbsolute('qux/')); // false
console.log(path.isAbsolute('.')); // false
// Windows(path.win32を使用)
console.log(path.win32.isAbsolute('C:\\foo')); // true
console.log(path.win32.isAbsolute('\\\\server')); // true(UNCパス)
console.log(path.win32.isAbsolute('bar\\baz')); // false
|
path.normalize()#
path.normalize()はパス内の..や.を解決し、連続する区切り文字を正規化します。
1
2
3
4
5
6
7
8
9
|
import path from 'node:path';
// POSIXでの正規化
console.log(path.normalize('/foo/bar//baz/asdf/quux/..'));
// '/foo/bar/baz/asdf'
// Windowsでの正規化
console.log(path.win32.normalize('C:\\temp\\\\foo\\bar\\..\\'));
// 'C:\\temp\\foo\\'
|
path.relative()#
path.relative()は2つのパス間の相対パスを計算します。
1
2
3
4
5
6
7
8
9
|
import path from 'node:path';
// POSIXでの相対パス
console.log(path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'));
// '../../impl/bbb'
// Windowsでの相対パス
console.log(path.win32.relative('C:\\orandea\\test\\aaa', 'C:\\orandea\\impl\\bbb'));
// '..\\..\\impl\\bbb'
|
クロスプラットフォーム対応#
path.posixとpath.win32#
どのOSで実行しても一貫した結果を得たい場合は、path.posixまたはpath.win32を使用します。
1
2
3
4
5
6
7
8
9
|
import path from 'node:path';
// どのOSでもPOSIX形式で処理
console.log(path.posix.basename('/tmp/myfile.html'));
// 'myfile.html'
// どのOSでもWindows形式で処理
console.log(path.win32.basename('C:\\temp\\myfile.html'));
// 'myfile.html'
|
path.posixとpath.win32は個別にインポートすることもできます。
1
2
3
4
5
|
// POSIX専用モジュール
import posix from 'node:path/posix';
// Windows専用モジュール
import win32 from 'node:path/win32';
|
ユースケース:設定ファイルの一貫したパス処理#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import path from 'node:path';
// 設定ファイルで定義されたパス(常にPOSIX形式で保存)
const configuredPath = 'assets/images/logo.png';
// 実行環境に合わせて変換
function toNativePath(posixPath) {
return posixPath.split(path.posix.sep).join(path.sep);
}
// 逆変換:ネイティブパスをPOSIX形式に
function toPosixPath(nativePath) {
return nativePath.split(path.sep).join(path.posix.sep);
}
console.log(toNativePath(configuredPath));
// Windows: 'assets\\images\\logo.png'
// POSIX: 'assets/images/logo.png'
|
URLオブジェクトとの連携#
ES Modulesでは__dirnameが使用できないため、import.meta.urlとURL APIを組み合わせてパス操作を行います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { fileURLToPath } from 'node:url';
import path from 'node:path';
// ESMでの__dirname相当の取得
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.log(__dirname);
// 例: '/home/user/project/src'
// 相対パスからの絶対パス構築
const configPath = path.join(__dirname, '../config/settings.json');
console.log(configPath);
// 例: '/home/user/project/config/settings.json'
|
pathToFileURL#
ファイルパスをfile:// URLに変換する場合はpathToFileURLを使用します。
1
2
3
4
5
6
7
8
9
10
11
|
import { pathToFileURL } from 'node:url';
import path from 'node:path';
const filePath = path.join(process.cwd(), 'data', 'sample.json');
const fileUrl = pathToFileURL(filePath);
console.log(fileUrl.href);
// 例: 'file:///home/user/project/data/sample.json'
// 動的インポートに使用
const module = await import(fileUrl);
|
fileURLToPathの注意点#
URLオブジェクトのpathnameを直接使用すると問題が発生する場合があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { fileURLToPath } from 'node:url';
// 正しい方法
console.log(fileURLToPath('file:///C:/path/'));
// 'C:\\path\\' (Windows)
// 間違った方法(pathnameを直接使用)
const url = new URL('file:///C:/path/');
console.log(url.pathname);
// '/C:/path/' (先頭にスラッシュが含まれる)
// スペースを含むパスの処理
console.log(fileURLToPath('file:///hello%20world'));
// '/hello world' (正しくデコードされる)
|
実践的なユースケース#
プロジェクトルートからの相対パス解決#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import { fileURLToPath } from 'node:url';
import path from 'node:path';
// プロジェクトルートを基準としたパスを解決するヘルパー
function resolveFromRoot(...segments) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, '..');
return path.join(projectRoot, ...segments);
}
// 使用例
const configPath = resolveFromRoot('config', 'app.json');
const dataPath = resolveFromRoot('data', 'users.csv');
|
ファイル拡張子の変更#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import path from 'node:path';
function changeExtension(filePath, newExt) {
const parsed = path.parse(filePath);
return path.format({
dir: parsed.dir,
name: parsed.name,
ext: newExt.startsWith('.') ? newExt : `.${newExt}`
});
}
console.log(changeExtension('/path/to/file.ts', '.js'));
// '/path/to/file.js'
console.log(changeExtension('document.md', 'html'));
// 'document.html'
|
安全なパス結合(パストラバーサル対策)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
import path from 'node:path';
function safeJoin(basePath, userInput) {
// ユーザー入力からの相対パスを解決
const resolvedPath = path.resolve(basePath, userInput);
// 結果がベースパス内に収まっているか確認
const normalizedBase = path.normalize(basePath + path.sep);
if (!resolvedPath.startsWith(normalizedBase)) {
throw new Error('パストラバーサル攻撃を検出しました');
}
return resolvedPath;
}
// 使用例
const uploadsDir = '/var/www/uploads';
console.log(safeJoin(uploadsDir, 'images/photo.jpg'));
// '/var/www/uploads/images/photo.jpg'
try {
safeJoin(uploadsDir, '../../../etc/passwd');
} catch (error) {
console.error(error.message);
// 'パストラバーサル攻撃を検出しました'
}
|
ファイル一覧からの拡張子別分類#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import path from 'node:path';
function groupByExtension(files) {
return files.reduce((acc, file) => {
const ext = path.extname(file).toLowerCase() || '(no extension)';
if (!acc[ext]) {
acc[ext] = [];
}
acc[ext].push(path.basename(file));
return acc;
}, {});
}
const files = [
'/docs/readme.md',
'/src/index.ts',
'/src/utils.ts',
'/public/style.css',
'/public/logo.png',
'/Makefile'
];
console.log(groupByExtension(files));
// {
// '.md': ['readme.md'],
// '.ts': ['index.ts', 'utils.ts'],
// '.css': ['style.css'],
// '.png': ['logo.png'],
// '(no extension)': ['Makefile']
// }
|
APIリファレンス一覧#
| API |
説明 |
path.basename(path, suffix?) |
パスからファイル名を取得 |
path.dirname(path) |
パスからディレクトリ名を取得 |
path.extname(path) |
パスから拡張子を取得 |
path.join(...paths) |
パスセグメントを結合 |
path.resolve(...paths) |
絶対パスを構築 |
path.relative(from, to) |
2つのパス間の相対パスを計算 |
path.parse(path) |
パスをオブジェクトに分解 |
path.format(pathObject) |
オブジェクトからパスを構築 |
path.normalize(path) |
パスを正規化 |
path.isAbsolute(path) |
絶対パスかどうかを判定 |
path.sep |
プラットフォーム固有のパス区切り文字 |
path.delimiter |
環境変数PATHの区切り文字 |
path.posix |
POSIX形式のpath実装 |
path.win32 |
Windows形式のpath実装 |
まとめ#
pathモジュールを使用することで、以下のメリットが得られます。
- OSごとのパス区切り文字の違いを意識せずにコードを書ける
path.posix/path.win32で特定プラットフォーム向けの処理も可能
path.parse()/path.format()でパスの分解・構築が容易
- ES Modulesでは
fileURLToPathと組み合わせて__dirname相当の機能を実現
文字列の直接連結ではなく、常にpathモジュールのAPIを使用することで、クロスプラットフォームで堅牢なファイルパス処理を実装できます。
参考リンク#