Node.jsによるシェルスクリプティング
この本のオフライン版(HTML、PDF、EPUB、MOBI)を購入して、無料のオンライン版をサポートできます。
(広告、ブロックしないでください。)

7章 ファイルシステムパスとファイルURLの操作(Node.js)



この章では、Node.jsにおけるファイルシステムパスとファイルURLの使用方法を学びます。

この章では、Node.jsにおけるパス関連の機能を探ります。

7.1.1 'node:path' APIへのアクセス方法3種類

'node:path'モジュールは、多くの場合、次のようにインポートされます。

import * as path from 'node:path';

この章では、このインポート文は時々省略されます。また、次のインポートも省略します。

import * as assert from 'node:assert/strict';

NodeのパスAPIには3つの方法でアクセスできます。

ファイルシステムパスを解析する関数path.parse()が、2つのプラットフォームでどのように異なるかを見てみましょう。

> path.win32.parse(String.raw`C:\Users\jane\file.txt`)
{
  dir: 'C:\\Users\\jane',
  root: 'C:\\',
  base: 'file.txt',
  name: 'file',
  ext: '.txt',
}
> path.posix.parse(String.raw`C:\Users\jane\file.txt`)
{
  dir: '',
  root: '',
  base: 'C:\\Users\\jane\\file.txt',
  name: 'C:\\Users\\jane\\file',
  ext: '.txt',
}

Windowsのパスを解析します。最初にpath.win32 APIを使用して正しく解析し、次にpath.posix APIを使用して解析します。後者の場合、パスが正しく分割されていないことがわかります。たとえば、ファイルの基本名はfile.txtである必要があります(他のプロパティの意味については後で詳しく説明します)。

7.2 基本的なパス概念とそのAPIサポート

7.2.1 パスセグメント、パスセパレータ、パスデリミタ

用語

PATHシェル変数を調べると、パスセパレータとパスデリミタを確認できます。PATHシェル変数には、シェルでコマンドが入力されたときに、オペレーティングシステムが実行可能ファイルを探すパスが含まれています。

これはmacOSのPATH(シェル変数$PATH)の例です。

> process.env.PATH.split(/(?<=:)/)
[
  '/opt/homebrew/bin:',
  '/opt/homebrew/sbin:',
  '/usr/local/bin:',
  '/usr/bin:',
  '/bin:',
  '/usr/sbin:',
  '/sbin',
]

後方参照アサーション(?<=:)は、指定された位置の前にコロンがある場合に一致しますが、何もキャプチャしません。そのため、パスデリミタ':'は前のパスに含まれています。後方参照アサーションについてはこちら

これはWindowsのPATH(シェル変数%Path%)の例です。

> process.env.Path.split(/(?<=;)/)
[
  'C:\\Windows\\system32;',
  'C:\\Windows;',
  'C:\\Windows\\System32\\Wbem;',
  'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;',
  'C:\\Windows\\System32\\OpenSSH\\;',
  'C:\\ProgramData\\chocolatey\\bin;',
  'C:\\Program Files\\nodejs\\',
]

7.2.2 カレントワーキングディレクトリ

多くのシェルには、*カレントワーキングディレクトリ*(CWD)という概念があります。「現在いるディレクトリ」です。

processはグローバルなNode.js変数です。CWDを取得および設定するためのメソッドを提供します。

Node.jsは、パスが*完全修飾パス*(完全なパス)でない場合、CWDを使用して不足している部分を補います。これにより、様々な関数(例:fs.readFileSync())で部分修飾パスを使用できます。

7.2.2.1 Unixにおけるカレントワーキングディレクトリ

次のコードは、Unixでのprocess.chdir()process.cwd()を示しています。

process.chdir('/home/jane');
assert.equal(
  process.cwd(), '/home/jane'
);
7.2.2.2 Windowsにおけるカレントワーキングディレクトリ

これまで、Unixでのカレントワーキングディレクトリを使用してきました。Windowsは異なります。

path.chdir()を使用して、同時に両方設定できます。

process.chdir('C:\\Windows');
process.chdir('Z:\\tmp');

ドライブを再び参照すると、Node.jsはそのドライブの以前のカレントディレクトリを記憶しています。

assert.equal(
  process.cwd(), 'Z:\\tmp'
);
process.chdir('C:');
assert.equal(
  process.cwd(), 'C:\\Windows'
);

7.2.3 完全修飾パスと部分修飾パス、パスの解決

7.2.3.1 Unixにおける完全修飾パスと部分修飾パス

Unixは2種類のパスのみ認識します。

path.resolve()後述)を使用して、相対パスを絶対パスに対して解決してみましょう。結果は絶対パスになります。

> const abs = '/home/john/proj';

> path.resolve(abs, '.')
'/home/john/proj'
> path.resolve(abs, '..')
'/home/john'
> path.resolve(abs, 'dir')
'/home/john/proj/dir'
> path.resolve(abs, './dir')
'/home/john/proj/dir'
> path.resolve(abs, '../dir')
'/home/john/dir'
> path.resolve(abs, '../../dir/subdir')
'/home/dir/subdir'
7.2.3.2 Windowsにおける完全修飾パスと部分修飾パス

Windowsは4種類のパスを区別します(詳細については、Microsoftのドキュメントを参照してください)。

ドライブ文字を含む絶対パスは完全修飾パスです。その他のパスはすべて部分修飾パスです。

ドライブ文字のない**絶対パスの解決**は、完全修飾パスfullに対して行われ、fullのドライブ文字が取得されます。

> const full = 'C:\\Users\\jane\\proj';

> path.resolve(full, '\\Windows')
'C:\\Windows'

ドライブ文字のない**相対パスの解決**は、完全修飾パスに対して行われ、後者を更新したものと見なすことができます。

> const full = 'C:\\Users\\jane\\proj';

> path.resolve(full, '.')
'C:\\Users\\jane\\proj'
> path.resolve(full, '..')
'C:\\Users\\jane'
> path.resolve(full, 'dir')
'C:\\Users\\jane\\proj\\dir'
> path.resolve(full, '.\\dir')
'C:\\Users\\jane\\proj\\dir'
> path.resolve(full, '..\\dir')
'C:\\Users\\jane\\dir'
> path.resolve(full, '..\\..\\dir')
'C:\\Users\\dir'

ドライブ文字を含む**相対パスrelの解決**は、完全修飾パスfullに対して行われ、relのドライブ文字に依存します。

次のようになります。

// Configure current directories for C: and Z:
process.chdir('C:\\Windows\\System');
process.chdir('Z:\\tmp');

const full = 'C:\\Users\\jane\\proj';

// Same drive letter
assert.equal(
  path.resolve(full, 'C:dir'),
  'C:\\Users\\jane\\proj\\dir'
);
assert.equal(
  path.resolve(full, 'C:'),
  'C:\\Users\\jane\\proj'
);

// Different drive letter
assert.equal(
  path.resolve(full, 'Z:dir'),
  'Z:\\tmp\\dir'
);
assert.equal(
  path.resolve(full, 'Z:'),
  'Z:\\tmp'
);

7.3 'node:os'モジュールを使用した標準ディレクトリのパス取得

'node:os'モジュールは、2つの重要なディレクトリのパスを提供します。

7.4 パスの連結

パスを連結するための関数は2つあります。

7.4.1 path.resolve():完全修飾パスを作成するためのパスの連結

path.resolve(...paths: Array<string>): string

pathsを連結して完全修飾パスを返します。次のアルゴリズムを使用します。

引数なしの場合、path.resolve()はカレントワーキングディレクトリのパスを返します。

> process.cwd()
'/usr/local'
> path.resolve()
'/usr/local'

1つ以上の相対パスが、カレントワーキングディレクトリから始めて解決に使用されます。

> path.resolve('.')
'/usr/local'
> path.resolve('..')
'/usr'
> path.resolve('bin')
'/usr/local/bin'
> path.resolve('./bin', 'sub')
'/usr/local/bin/sub'
> path.resolve('../lib', 'log')
'/usr/lib/log'

完全修飾パスは、前の結果を置き換えます。

> path.resolve('bin', '/home')
'/home'

これにより、部分修飾パスを完全修飾パスに対して解決できます。

> path.resolve('/home/john', 'proj', 'src')
'/home/john/proj/src'

7.4.2 path.join():相対パスを保持したままパスの連結を行う

path.join(...paths: Array<string>): string

paths[0]から開始し、残りのパスを昇順または降順の指示として解釈します。path.resolve()とは異なり、この関数は部分修飾パスを保持します。paths[0]が部分修飾パスであれば、結果は部分修飾パスになります。完全修飾パスであれば、結果は完全修飾パスになります。

降順の例

> path.posix.join('/usr/local', 'sub', 'subsub')
'/usr/local/sub/subsub'
> path.posix.join('relative/dir', 'sub', 'subsub')
'relative/dir/sub/subsub'

ダブルドットは昇順になります。

> path.posix.join('/usr/local', '..')
'/usr'
> path.posix.join('relative/dir', '..')
'relative'

単一のドットは何も行いません。

> path.posix.join('/usr/local', '.')
'/usr/local'
> path.posix.join('relative/dir', '.')
'relative/dir'

最初の引数以降の引数が完全修飾パスである場合、相対パスとして解釈されます。

> path.posix.join('dir', '/tmp')
'dir/tmp'
> path.win32.join('dir', 'C:\\Users')
'dir\\C:\\Users'

3つ以上の引数を使用する場合

> path.posix.join('/usr/local', '../lib', '.', 'log')
'/usr/lib/log'

7.5 パスの正規化、完全修飾、または相対パスの確保

7.5.1 path.normalize():パスの正規化を確保する

path.normalize(path: string): string

Unixでは、path.normalize()

例:

// Fully qualified path
assert.equal(
  path.posix.normalize('/home/./john/lib/../photos///pet'),
  '/home/john/photos/pet'
);

// Partially qualified path
assert.equal(
  path.posix.normalize('./john/lib/../photos///pet'),
  'john/photos/pet'
);

Windowsでは、path.normalize()

例:

// Fully qualified path
assert.equal(
  path.win32.normalize('C:\\Users/jane\\doc\\..\\proj\\\\src'),
  'C:\\Users\\jane\\proj\\src'
);

// Partially qualified path
assert.equal(
  path.win32.normalize('.\\jane\\doc\\..\\proj\\\\src'),
  'jane\\proj\\src'
);

path.join()に単一の引数を指定した場合も正規化され、path.normalize()と同じように動作することに注意してください。

> path.posix.normalize('/home/./john/lib/../photos///pet')
'/home/john/photos/pet'
> path.posix.join('/home/./john/lib/../photos///pet')
'/home/john/photos/pet'

> path.posix.normalize('./john/lib/../photos///pet')
'john/photos/pet'
> path.posix.join('./john/lib/../photos///pet')
'john/photos/pet'

7.5.2 path.resolve()(1つの引数):パスの正規化と完全修飾を確保する

既にpath.resolve()について説明しました。単一の引数で呼び出されると、パスを正規化し、完全修飾されていることを確認します。

Unixでpath.resolve()を使用する

> process.cwd()
'/usr/local'

> path.resolve('/home/./john/lib/../photos///pet')
'/home/john/photos/pet'
> path.resolve('./john/lib/../photos///pet')
'/usr/local/john/photos/pet'

Windowsでpath.resolve()を使用する

> process.cwd()
'C:\\Windows\\System'

> path.resolve('C:\\Users/jane\\doc\\..\\proj\\\\src')
'C:\\Users\\jane\\proj\\src'
> path.resolve('.\\jane\\doc\\..\\proj\\\\src')
'C:\\Windows\\System\\jane\\proj\\src'

7.5.3 path.relative():相対パスの作成

path.relative(sourcePath: string, destinationPath: string): string

sourcePathからdestinationPathへの相対パスを返します。

> path.posix.relative('/home/john/', '/home/john/proj/my-lib/README.md')
'proj/my-lib/README.md'
> path.posix.relative('/tmp/proj/my-lib/', '/tmp/doc/zsh.txt')
'../../doc/zsh.txt'

Windowsでは、sourcePathdestinationPathが異なるドライブにある場合、完全修飾パスが取得されます。

> path.win32.relative('Z:\\tmp\\', 'C:\\Users\\Jane\\')
'C:\\Users\\Jane'

この関数は相対パスでも機能します。

> path.posix.relative('proj/my-lib/', 'doc/zsh.txt')
'../../doc/zsh.txt'

7.6 パスの解析:パスの様々な部分(ファイル名拡張子など)を抽出する

7.6.1 path.parse():パス部分をオブジェクトで作成する

type PathObject = {
  dir: string,
    root: string,
  base: string,
    name: string,
    ext: string,
};
path.parse(path: string): PathObject

pathの様々な部分を抽出し、以下のプロパティを持つオブジェクトで返します。

後で、path.parse()の逆関数であるpath.format()関数について説明します。これは、パス部分を持つオブジェクトをパスに変換します。

7.6.1.1 例:Unixでのpath.parse()

Unixでpath.parse()を使用すると、次のようになります。

> path.posix.parse('/home/jane/file.txt')
{
  dir: '/home/jane',
  root: '/',
  base: 'file.txt',
  name: 'file',
  ext: '.txt',
}

次の図は、各部分の範囲を視覚的に表したものです。

  /      home/jane / file   .txt
| root |           | name | ext  |
| dir              | base        |

例えば、.dirはbaseを除いたパスであり、.base.name.extの組み合わせであることがわかります。

7.6.1.2 例:Windowsでのpath.parse()

Windowsでのpath.parse()の動作は次のとおりです。

> path.win32.parse(String.raw`C:\Users\john\file.txt`)
{
  dir: 'C:\\Users\\john',
  root: 'C:\\',
  base: 'file.txt',
  name: 'file',
  ext: '.txt',
}

これは結果の図です。

  C:\    Users\john \ file   .txt
| root |            | name | ext  |
| dir               | base        |

7.6.2 path.basename():パスのbaseを抽出する

path.basename(path, ext?)

pathのbaseを返します。

> path.basename('/home/jane/file.txt')
'file.txt'

オプションで、この関数はサフィックスも削除できます。

> path.basename('/home/jane/file.txt', '.txt')
'file'
> path.basename('/home/jane/file.txt', 'txt')
'file.'
> path.basename('/home/jane/file.txt', 'xt')
'file.t'

拡張子の削除は大文字と小文字が区別されます - Windowsでも!

> path.win32.basename(String.raw`C:\Users\john\file.txt`, '.txt')
'file'
> path.win32.basename(String.raw`C:\Users\john\file.txt`, '.TXT')
'file.txt'

7.6.3 path.dirname():パスの親ディレクトリを抽出する

path.dirname(path)

pathにあるファイルまたはディレクトリの親ディレクトリを返します。

> path.win32.dirname(String.raw`C:\Users\john\file.txt`)
'C:\\Users\\john'
> path.win32.dirname('C:\\Users\\john\\dir\\')
'C:\\Users\\john'

> path.posix.dirname('/home/jane/file.txt')
'/home/jane'
> path.posix.dirname('/home/jane/dir/')
'/home/jane'

7.6.4 path.extname():パスの拡張子を抽出する

path.extname(path)

pathの拡張子を返します。

> path.extname('/home/jane/file.txt')
'.txt'
> path.extname('/home/jane/file.')
'.'
> path.extname('/home/jane/file')
''
> path.extname('/home/jane/')
''
> path.extname('/home/jane')
''

7.7 パスの分類

7.7.1 path.isAbsolute():指定されたパスは絶対パスか?

path.isAbsolute(path: string): boolean

pathが絶対パスの場合はtrue、それ以外の場合はfalseを返します。

Unixでの結果は簡単です。

> path.posix.isAbsolute('/home/john')
true
> path.posix.isAbsolute('john')
false

Windowsでは、「絶対」は必ずしも「完全修飾」を意味しません(最初のパスのみが完全修飾されます)。

> path.win32.isAbsolute('C:\\Users\\jane')
true
> path.win32.isAbsolute('\\Users\\jane')
true
> path.win32.isAbsolute('C:jane')
false
> path.win32.isAbsolute('jane')
false

7.8 path.format():部分からパスを作成する

type PathObject = {
  dir: string,
    root: string,
  base: string,
    name: string,
    ext: string,
};
path.format(pathObject: PathObject): string

パスオブジェクトからパスを作成します。

> path.format({dir: '/home/jane', base: 'file.txt'})
'/home/jane/file.txt'

7.8.1 例:ファイル名拡張子の変更

path.format()を使用して、パスの拡張子を変更できます。

function changeFilenameExtension(pathStr, newExtension) {
  if (!newExtension.startsWith('.')) {
    throw new Error(
      'Extension must start with a dot: '
      + JSON.stringify(newExtension)
    );
  }
  const parts = path.parse(pathStr);
  return path.format({
    ...parts,
    base: undefined, // prevent .base from overriding .name and .ext
    ext: newExtension,
  });
}

assert.equal(
  changeFilenameExtension('/tmp/file.md', '.html'),
  '/tmp/file.html'
);
assert.equal(
  changeFilenameExtension('/tmp/file', '.html'),
  '/tmp/file.html'
);
assert.equal(
  changeFilenameExtension('/tmp/file/', '.html'),
  '/tmp/file.html'
);

元のファイル名拡張子がわかっている場合は、正規表現を使用してファイル名拡張子を変更することもできます。

> '/tmp/file.md'.replace(/\.md$/i, '.html')
'/tmp/file.html'
> '/tmp/file.MD'.replace(/\.md$/i, '.html')
'/tmp/file.html'

7.9 異なるプラットフォームで同じパスを使用する

異なるプラットフォームで同じパスを使用したい場合があります。その場合、2つの問題があります。

例として、データを含むディレクトリで動作するNode.jsアプリを考えてみましょう。アプリは2種類のパスで構成できると仮定します。

前述の問題のため

7.9.1 プラットフォームに依存しない相対パス

プラットフォームに依存しない相対パスは、パスセグメントの配列として保存し、次のようにプラットフォーム固有の完全修飾パスに変換できます。

const universalRelativePath = ['static', 'img', 'logo.jpg'];

const dataDirUnix = '/home/john/data-dir';
assert.equal(
  path.posix.resolve(dataDirUnix, ...universalRelativePath),
  '/home/john/data-dir/static/img/logo.jpg'
);

const dataDirWindows = 'C:\\Users\\jane\\data-dir';
assert.equal(
  path.win32.resolve(dataDirWindows, ...universalRelativePath),
  'C:\\Users\\jane\\data-dir\\static\\img\\logo.jpg'
);

プラットフォーム固有の相対パスを作成するには、以下を使用できます。

const dataDir = '/home/john/data-dir';
const pathInDataDir = '/home/john/data-dir/static/img/logo.jpg';
assert.equal(
  path.relative(dataDir, pathInDataDir),
  'static/img/logo.jpg'
);

次の関数は、プラットフォーム固有の相対パスをプラットフォームに依存しないパスに変換します。

import * as path from 'node:path';

function splitRelativePathIntoSegments(relPath) {
  if (path.isAbsolute(relPath)) {
    throw new Error('Path isn’t relative: ' + relPath);
  }
  relPath = path.normalize(relPath);
  const result = [];
  while (true) {
    const base = path.basename(relPath);
    if (base.length === 0) break;
    result.unshift(base);
    const dir = path.dirname(relPath);
    if (dir === '.') break;
    relPath = dir;
  }
  return result;
}

UnixでsplitRelativePathIntoSegments()を使用する

> splitRelativePathIntoSegments('static/img/logo.jpg')
[ 'static', 'img', 'logo.jpg' ]
> splitRelativePathIntoSegments('file.txt')
[ 'file.txt' ]

WindowsでsplitRelativePathIntoSegments()を使用する

> splitRelativePathIntoSegments('static/img/logo.jpg')
[ 'static', 'img', 'logo.jpg' ]
> splitRelativePathIntoSegments('C:static/img/logo.jpg')
[ 'static', 'img', 'logo.jpg' ]

> splitRelativePathIntoSegments('file.txt')
[ 'file.txt' ]
> splitRelativePathIntoSegments('C:file.txt')
[ 'file.txt' ]

7.10 *glob* を介してパスを照合するライブラリの使用

npmモジュール'minimatch'を使用すると、*glob式*、*globパターン*、または*glob*と呼ばれるパターンに対してパスを照合できます。

import minimatch from 'minimatch';
assert.equal(
  minimatch('/dir/sub/file.txt', '/dir/sub/*.txt'), true
);
assert.equal(
  minimatch('/dir/sub/file.txt', '/**/file.txt'), true
);

globの使用例

その他のglobライブラリ

7.10.1 minimatch API

minimatchのAPI全体は、プロジェクトのREADMEファイルに記載されています。このセクションでは、最も重要な機能を見ていきます。

MinimatchはglobをJavaScriptのRegExpオブジェクトにコンパイルし、それを使用して照合します。

7.10.1.1 minimatch():一度コンパイルして照合する
minimatch(path: string, glob: string, options?: MinimatchOptions): boolean

globpathと一致する場合はtrue、それ以外の場合はfalseを返します。

2つの興味深いオプション

7.10.1.2 new minimatch.Minimatch():一度コンパイルして複数回照合する

minimatch.Minimatchクラスを使用すると、globを正規表現に一度だけコンパイルして、複数回照合できます。

new Minimatch(pattern: string, options?: MinimatchOptions)

このクラスは次のように使用します。

import minimatch from 'minimatch';
const {Minimatch} = minimatch;
const glob = new Minimatch('/dir/sub/*.txt');
assert.equal(
  glob.match('/dir/sub/file.txt'), true
);
assert.equal(
  glob.match('/dir/sub/notes.txt'), true
);

7.10.2 glob式の構文

このセクションでは、構文の基本事項について説明します。しかし、さらに多くの機能があります。これらはここに記載されています。

7.10.2.1 Windowsパスの照合

Windowsでも、globセグメントはスラッシュで区切られますが、バックスラッシュとスラッシュの両方と一致します(Windowsでは有効なパス区切り文字です)。

> minimatch('dir\\sub/file.txt', 'dir/sub/file.txt')
true
7.10.2.2 Minimatchはパスを正規化しません

Minimatchはパスを自動的に正規化しません。

> minimatch('./file.txt', './file.txt')
true
> minimatch('./file.txt', 'file.txt')
false
> minimatch('file.txt', './file.txt')
false

したがって、自分で作成しない場合は、パスを正規化する必要があります。

> path.normalize('./file.txt')
'file.txt'
7.10.2.3 ワイルドカード記号のないパターン:パス区切り文字は一致する必要があります

ワイルドカード記号(より柔軟に一致する)のないパターンは、正確に一致する必要があります。特に、パス区切り文字は一致する必要があります。

> minimatch('/dir/file.txt', '/dir/file.txt')
true
> minimatch('dir/file.txt', 'dir/file.txt')
true
> minimatch('/dir/file.txt', 'dir/file.txt')
false

> minimatch('/dir/file.txt', 'file.txt')
false

つまり、絶対パスか相対パスのどちらかを選択する必要があります。

.matchBaseオプションを使用すると、スラッシュのないパターンをパスのbasenameと照合できます。

> minimatch('/dir/file.txt', 'file.txt', {matchBase: true})
true
7.10.2.4 アスタリスク(*)は、単一のセグメントの任意の部分と一致します

ワイルドカード記号であるアスタリスク(*)は、任意のパスセグメントまたはセグメントの任意の部分と一致します。

> minimatch('/dir/file.txt', '/*/file.txt')
true
> minimatch('/tmp/file.txt', '/*/file.txt')
true

> minimatch('/dir/file.txt', '/dir/*.txt')
true
> minimatch('/dir/data.txt', '/dir/*.txt')
true

アスタリスクは、名前がドットで始まる「非表示ファイル」とは一致しません。それらを一致させたい場合は、アスタリスクの前にドットを付ける必要があります。

> minimatch('file.txt', '*')
true
> minimatch('.gitignore', '*')
false
> minimatch('.gitignore', '.*')
true
> minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt')
false

.dotオプションを使用して、この動作をオフにできます。

> minimatch('.gitignore', '*', {dot: true})
true
> minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt', {dot: true})
true
7.10.2.5 ダブルアスタリスク(**)は、0個以上のセグメントと一致します

**/は、0個以上のセグメントと一致します。

> minimatch('/file.txt', '/**/file.txt')
true
> minimatch('/dir/file.txt', '/**/file.txt')
true
> minimatch('/dir/sub/file.txt', '/**/file.txt')
true

相対パスと一致させたい場合でも、パターンはパス区切り文字で始まらないようにする必要があります。

> minimatch('file.txt', '/**/file.txt')
false

ダブルアスタリスクは、名前がドットで始まる「非表示」のパスセグメントとは一致しません。

> minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json')
false

.dotオプションを使用して、この動作をオフにできます。

> minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json', {dot: true})
true
7.10.2.6 globの否定

globを感嘆符で始めると、感嘆符の後のパターンと一致しない場合に一致します。

> minimatch('file.txt', '!**/*.txt')
false
> minimatch('file.js', '!**/*.txt')
true
7.10.2.7 代替パターン

中括弧内のコンマ区切りのパターンは、いずれかのパターンと一致する場合に一致します。

> minimatch('file.txt', 'file.{txt,js}')
true
> minimatch('file.js', 'file.{txt,js}')
true
7.10.2.8 整数の範囲

ダブルドットで区切られた整数のペアは、整数の範囲を定義し、その要素のいずれかと一致する場合に一致します。

> minimatch('file1.txt', 'file{1..3}.txt')
true
> minimatch('file2.txt', 'file{1..3}.txt')
true
> minimatch('file3.txt', 'file{1..3}.txt')
true
> minimatch('file4.txt', 'file{1..3}.txt')
false

ゼロパディングもサポートされています。

> minimatch('file1.txt', 'file{01..12}.txt')
false
> minimatch('file01.txt', 'file{01..12}.txt')
true
> minimatch('file02.txt', 'file{01..12}.txt')
true
> minimatch('file12.txt', 'file{01..15}.txt')
true

7.11 ファイルを参照するためのfile: URLの使用

Node.jsでファイルを参照する一般的な方法は2つあります。

例:

assert.equal(
  fs.readFileSync(
    '/tmp/data.txt', {encoding: 'utf-8'}),
  'Content'
);
assert.equal(
  fs.readFileSync(
    new URL('file:///tmp/data.txt'), {encoding: 'utf-8'}),
  'Content'
);

7.11.1 URLクラス

このセクションでは、URLクラスを詳しく見ていきます。このクラスの詳細については、

この章では、他のWebプラットフォームで使用されている方法と同じように、グローバル変数経由でURLクラスにアクセスします。しかし、インポートすることもできます。

import {URL} from 'node:url';
7.11.1.1 URIと相対参照

URLはURIのサブセットです。URIの標準であるRFC 3986では、2種類のURI参照が区別されています。

7.11.1.2 URLのコンストラクタ

URLクラスは2つの方法でインスタンス化できます。

ここでは、クラスの動作を確認できます。

// If there is only one argument, it must be a proper URI
assert.equal(
  new URL('https://example.com/public/page.html').toString(),
  'https://example.com/public/page.html'
);
assert.throws(
  () => new URL('../book/toc.html'),
  /^TypeError \[ERR_INVALID_URL\]: Invalid URL$/
);

// Resolve a relative reference against a base URI 
assert.equal(
  new URL(
    '../book/toc.html',
    'https://example.com/public/page.html'
  ).toString(),
  'https://example.com/book/toc.html'
);
7.11.1.3 URLのインスタンスに対する相対参照の解決

URLコンストラクタのこのバリアントを再検討しましょう。

new URL(uriRef: string, baseUri: string)

引数baseUriは文字列に変換されます。したがって、文字列に変換したときに有効なURLになる限り、任意のオブジェクトを使用できます。

const obj = { toString() {return 'https://example.com'} };
assert.equal(
  new URL('index.html', obj).href,
  'https://example.com/index.html'
);

これにより、URLインスタンスに対して相対参照を解決できます。

const url = new URL('https://example.com/dir/file1.html');
assert.equal(
  new URL('../file2.html', url).href,
  'https://example.com/file2.html'
);

このように使用すると、コンストラクタはpath.resolve()と大まかに似ています。

7.11.1.4 URLインスタンスのプロパティ

URLのインスタンスには、以下のプロパティがあります。

type URL = {
  protocol: string,
  username: string,
  password: string,
  hostname: string,
  port: string,
  host: string,
  readonly origin: string,
  
  pathname: string,
  
  search: string,
  readonly searchParams: URLSearchParams,
  hash: string,

  href: string,
  toString(): string,
  toJSON(): string,
}
7.11.1.5 URLを文字列に変換する

URLを文字列に変換する一般的な方法は3つあります。

const url = new URL('https://example.com/about.html');

assert.equal(
  url.toString(),
  'https://example.com/about.html'
);
assert.equal(
  url.href,
  'https://example.com/about.html'
);
assert.equal(
  url.toJSON(),
  'https://example.com/about.html'
);

.toJSON()メソッドを使用すると、JSONデータでURLを使用できます。

const jsonStr = JSON.stringify({
  pageUrl: new URL('https://exploringjs.dokyumento.jp')
});
assert.equal(
  jsonStr, '{"pageUrl":"https://exploringjs.dokyumento.jp"}'
);
7.11.1.6 URLプロパティを取得する

URLインスタンスのプロパティは、独自のデータプロパティではなく、ゲッターとセッターによって実装されています。次の例では、ユーティリティ関数pickProps()(コードは最後に示されています)を使用して、これらのゲッターによって返される値をプレーンオブジェクトにコピーします。

const props = pickProps(
  new URL('https://jane:pw@example.com:80/news.html?date=today#misc'),
  'protocol', 'username', 'password', 'hostname', 'port', 'host',
  'origin', 'pathname', 'search', 'hash', 'href'
);
assert.deepEqual(
  props,
  {
    protocol: 'https:',
    username: 'jane',
    password: 'pw',
    hostname: 'example.com',
    port: '80',
    host: 'example.com:80',
    origin: 'https://example.com:80',
    pathname: '/news.html',
    search: '?date=today',
    hash: '#misc',
    href: 'https://jane:pw@example.com:80/news.html?date=today#misc'
  }
);
function pickProps(input, ...keys) {
  const output = {};
  for (const key of keys) {
    output[key] = input[key];
  }
  return output;
}

残念ながら、パス名は単一の原子単位です。つまり、URLクラスを使用してその部分(ベース、拡張子など)にアクセスすることはできません。

7.11.1.7 URLの一部を設定する

.hostnameなどのプロパティを設定することで、URLの一部を変更することもできます。

const url = new URL('https://example.com');
url.hostname = '2ality.com';
assert.equal(
  url.href, 'https://2ality.com/'
);

セッターを使用して、部分的なURLを作成できます(Haroen Viaeneによるアイデア)。

// Object.assign() invokes setters when transferring property values
const urlFromParts = (parts) => Object.assign(
  new URL('https://example.com'), // minimal dummy URL
  parts // assigned to the dummy
);

const url = urlFromParts({
  protocol: 'https:',
  hostname: '2ality.com',
  pathname: '/p/about.html',
});
assert.equal(
  url.href, 'https://2ality.com/p/about.html'
);
7.11.1.8 .searchParamsによる検索パラメータの管理

.searchParamsプロパティを使用して、URLの検索パラメータを管理できます。その値は、URLSearchParamsのインスタンスです。

これを使用して、検索パラメータを読み取ることができます。

const url = new URL('https://example.com/?topic=js');
assert.equal(
  url.searchParams.get('topic'), 'js'
);
assert.equal(
  url.searchParams.has('topic'), true
);

これを使用して、検索パラメータを変更することもできます。

url.searchParams.append('page', '5');
assert.equal(
  url.href, 'https://example.com/?topic=js&page=5'
);

url.searchParams.set('topic', 'css');
assert.equal(
  url.href, 'https://example.com/?topic=css&page=5'
);

7.11.2 URLとファイルパスの間の変換

ファイルパスとURLを手動で変換しようとすることがありますが、例えば、URLインスタンスmyUrlmyUrl.pathnameを介してファイルパスに変換しようとすることができます。しかし、これは必ずしも機能するわけではありません。 この関数を使用する方が良いでしょう。

url.fileURLToPath(url: URL | string): string

次のコードは、その関数の結果と.pathnameの値を比較しています。

import * as url from 'node:url';

//::::: Unix :::::

const url1 = new URL('file:///tmp/with%20space.txt');
assert.equal(
  url1.pathname, '/tmp/with%20space.txt');
assert.equal(
  url.fileURLToPath(url1), '/tmp/with space.txt');

const url2 = new URL('file:///home/thor/Mj%C3%B6lnir.txt');
assert.equal(
  url2.pathname, '/home/thor/Mj%C3%B6lnir.txt');
assert.equal(
  url.fileURLToPath(url2), '/home/thor/Mjölnir.txt');

//::::: Windows :::::

const url3 = new URL('file:///C:/dir/');
assert.equal(
  url3.pathname, '/C:/dir/');
assert.equal(
  url.fileURLToPath(url3), 'C:\\dir\\');

この関数url.fileURLToPath()の逆関数です。

url.pathToFileURL(path: string): URL

これはpathをファイルURLに変換します。

> url.pathToFileURL('/home/john/Work Files').href
'file:///home/john/Work%20Files'

7.11.3 URLのユースケース:現在のモジュールを基準としたファイルへのアクセス

URLの重要なユースケースの1つは、現在のモジュールの兄弟であるファイルにアクセスすることです。

function readData() {
  const url = new URL('data.txt', import.meta.url);
  return fs.readFileSync(url, {encoding: 'UTF-8'});
}

この関数は、現在のモジュールのURL(通常はNode.jsではfile: URL)を含むimport.meta.urlを使用します。

fetch()を使用すると、前のコードはさらにクロスプラットフォームになります。しかし、Node.js 18.9.0時点では、fetch()file: URLではまだ動作しません。

> await fetch('file:///tmp/file.txt')
TypeError: fetch failed
  cause: Error: not implemented... yet...

7.11.4 URLのユースケース:現在のモジュールが「メイン」(アプリのエントリポイント)かどうかを検出する

ESMモジュールは2つの方法で使用できます。

  1. 他のモジュールが値をインポートできるライブラリとして使用できます。
  2. Node.jsを介して実行するスクリプト(例:コマンドラインから)として使用できます。この場合、メインモジュールと呼ばれます。

モジュールを両方の方法で使用する場合、現在のモジュールがメインモジュールかどうかを確認する必要があります。スクリプト機能を実行するのは、その場合のみだからです。この章では、そのチェックを実行する方法を学習します。

7.11.4.1 CommonJSモジュールがメインかどうかを判断する

CommonJSでは、次のパターンを使用して、現在のモジュールがエントリポイントであったかどうかを検出できます(ソース:Node.jsドキュメント)。

if (require.main === module) {
  // Main CommonJS module
}
7.11.4.2 ESMモジュールがメインかどうかを判断する

現時点では、ESMモジュールには、モジュールがメインかどうかを確認する簡単な組み込み方法がありません。代わりに、次の回避策を使用する必要があります(Rich Harrisによるツイートに基づく)。

import * as url from 'node:url';

if (import.meta.url.startsWith('file:')) { // (A)
  const modulePath = url.fileURLToPath(import.meta.url);
  if (process.argv[1] === modulePath) { // (B)
    // Main ESM module
  }
}

説明

7.11.5 パスとfile: URL

シェルスクリプトがファイルへの参照を受け取ったり、ファイルへの参照をエクスポートしたりする場合(例:画面にログ出力する場合)、それらは事実上常にパスです。ただし、前のセクションで説明したように、URLが必要になるケースが2つあります。