Shell-Skripting mit Node.js
Sie können die Offline-Version dieses Buches (HTML, PDF, EPUB, MOBI) kaufen und damit die kostenlose Online-Version unterstützen.
(Werbung, bitte nicht blockieren.)

7 Arbeiten mit Dateisystempfaden und Datei-URLs in Node.js



In diesem Kapitel lernen wir, wie man mit Dateisystempfaden und Datei-URLs in Node.js arbeitet.

In diesem Kapitel untersuchen wir pfadbezogene Funktionalität in Node.js.

7.1.1 Die drei Wege, auf die Node.js-Pfad-API zuzugreifen

Das Modul 'node:path' wird oft wie folgt importiert:

import * as path from 'node:path';

In diesem Kapitel wird diese Importanweisung gelegentlich weggelassen. Wir lassen auch den folgenden Import weg:

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

Wir können auf die Pfad-API von Node auf drei Arten zugreifen:

Sehen wir uns an, wie sich die Funktion path.parse(), die Dateisystempfade parst, für die beiden Plattformen unterscheidet:

> 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',
}

Wir parsen einen Windows-Pfad – zuerst korrekt über die path.win32 API, dann über die path.posix API. Wir sehen, dass im letzteren Fall der Pfad nicht korrekt in seine Teile aufgeteilt wird – zum Beispiel sollte der Basisname der Datei file.txt sein (mehr dazu, was die anderen Eigenschaften bedeuten, später).

7.2 Grundlegende Pfadkonzepte und deren API-Unterstützung

7.2.1 Pfadsegmente, Pfadtrennzeichen, Pfadtrennzeichen

Terminologie

Wir können Pfadtrennzeichen und Pfadtrennzeichen (Delimiter) sehen, wenn wir die PATH-Shell-Variable untersuchen – die die Pfade enthält, in denen das Betriebssystem nach ausführbaren Dateien sucht, wenn ein Befehl in einer Shell eingegeben wird.

Dies ist ein Beispiel für einen macOS PATH (Shell-Variable $PATH):

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

Der Trenner für das Aufteilen hat eine Länge von Null, da die Lookbehind-Assertion (?<=:) übereinstimmt, wenn eine gegebene Position einem Doppelpunkt vorangestellt ist, aber nichts erfasst. Daher ist das Pfadtrennzeichen ':' im vorherigen Pfad enthalten.

Dies ist ein Beispiel für einen Windows PATH (Shell-Variable %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 Das aktuelle Arbeitsverzeichnis

Viele Shells haben das Konzept des aktuellen Arbeitsverzeichnisses (CWD) – „das Verzeichnis, in dem ich mich gerade befinde“.

process ist eine globale Node.js-Variable. Sie bietet uns Methoden zum Abrufen und Festlegen des CWD:

Node.js verwendet das CWD, um fehlende Teile zu ergänzen, wenn ein Pfad nicht vollständig qualifiziert (vollständig) ist. Das ermöglicht es uns, teilweise qualifizierte Pfade mit verschiedenen Funktionen zu verwenden – z. B. fs.readFileSync().

7.2.2.1 Das aktuelle Arbeitsverzeichnis unter Unix

Der folgende Code demonstriert process.chdir() und process.cwd() unter Unix:

process.chdir('/home/jane');
assert.equal(
  process.cwd(), '/home/jane'
);
7.2.2.2 Das aktuelle Arbeitsverzeichnis unter Windows

Bisher haben wir das aktuelle Arbeitsverzeichnis unter Unix verwendet. Windows funktioniert anders:

Wir können path.chdir() verwenden, um beides gleichzeitig festzulegen:

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

Wenn wir zu einem Laufwerk zurückkehren, merkt sich Node.js das vorherige aktuelle Verzeichnis dieses Laufwerks.

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

7.2.3 Vollständig vs. teilweise qualifizierte Pfade, Auflösen von Pfaden

7.2.3.1 Vollständig und teilweise qualifizierte Pfade unter Unix

Unix kennt nur zwei Arten von Pfaden:

Verwenden wir path.resolve() (das später detaillierter erklärt wird), um relative Pfade gegen absolute Pfade aufzulösen. Die Ergebnisse sind absolute Pfade:

> 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 Vollständig und teilweise qualifizierte Pfade unter Windows

Windows unterscheidet vier Arten von Pfaden (weitere Informationen finden Sie in der Microsoft-Dokumentation):

Absolute Pfade mit Laufwerksbuchstaben sind vollständig qualifiziert. Alle anderen Pfade sind teilweise qualifiziert.

Auflösen eines absoluten Pfades ohne Laufwerksbuchstaben gegen einen vollständig qualifizierten Pfad full, übernimmt den Laufwerksbuchstaben von full.

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

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

Auflösen eines relativen Pfades ohne Laufwerksbuchstaben gegen einen vollständig qualifizierten Pfad kann als Aktualisierung des letzteren betrachtet werden.

> 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'

Auflösen eines relativen Pfades rel mit einem Laufwerksbuchstaben gegen einen vollständig qualifizierten Pfad full hängt vom Laufwerksbuchstaben von rel ab:

Das sieht folgendermaßen aus:

// 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 Abrufen der Pfade von Standardverzeichnissen über das Modul 'node:os'

Das Modul 'node:os' stellt uns die Pfade von zwei wichtigen Verzeichnissen zur Verfügung:

7.4 Verketten von Pfaden

Es gibt zwei Funktionen zum Verketten von Pfaden:

7.4.1 path.resolve(): Verketten von Pfaden zur Erstellung vollständig qualifizierter Pfade

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

Verkettet die paths und gibt einen vollständig qualifizierten Pfad zurück. Es verwendet den folgenden Algorithmus:

Ohne Argumente gibt path.resolve() den Pfad des aktuellen Arbeitsverzeichnisses zurück:

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

Ein oder mehrere relative Pfade werden zur Auflösung verwendet, beginnend mit dem aktuellen Arbeitsverzeichnis:

> 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'

Jeder vollständig qualifizierte Pfad ersetzt das vorherige Ergebnis.

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

Das ermöglicht es uns, teilweise qualifizierte Pfade gegen vollständig qualifizierte Pfade aufzulösen:

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

7.4.2 path.join(): Verketten von Pfaden unter Beibehaltung relativer Pfade

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

Beginnt mit paths[0] und interpretiert die verbleibenden Pfade als Anweisungen zum Auf- oder Absteigen. Im Gegensatz zu path.resolve() behält diese Funktion teilweise qualifizierte Pfade bei: Wenn paths[0] teilweise qualifiziert ist, ist das Ergebnis teilweise qualifiziert. Wenn es vollständig qualifiziert ist, ist das Ergebnis vollständig qualifiziert.

Beispiele für das Absteigen:

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

Doppelpunkte steigen auf:

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

Einzelpunkte tun nichts:

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

Wenn Argumente nach dem ersten vollständig qualifizierte Pfade sind, werden sie als relative Pfade interpretiert:

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

Verwendung von mehr als zwei Argumenten:

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

7.5 Sicherstellen, dass Pfade normalisiert, vollständig qualifiziert oder relativ sind

7.5.1 path.normalize(): Sicherstellen, dass Pfade normalisiert sind

path.normalize(path: string): string

Unter Unix normalisiert path.normalize():

Zum Beispiel

// 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'
);

Unter Windows normalisiert path.normalize():

Zum Beispiel

// 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'
);

Beachten Sie, dass path.join() mit einem einzigen Argument ebenfalls normalisiert und wie path.normalize() funktioniert:

> 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() (ein Argument): Sicherstellen, dass Pfade normalisiert und vollständig qualifiziert sind

Wir sind bereits auf path.resolve() gestoßen. Mit einem einzigen Argument aufgerufen, normalisiert es Pfade und stellt sicher, dass sie vollständig qualifiziert sind.

Verwendung von path.resolve() unter Unix:

> 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'

Verwendung von path.resolve() unter Windows:

> 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(): Erstellen relativer Pfade

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

Gibt einen relativen Pfad zurück, der uns von sourcePath zu destinationPath bringt:

> 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'

Unter Windows erhalten wir einen vollständig qualifizierten Pfad, wenn sourcePath und destinationPath auf unterschiedlichen Laufwerken liegen:

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

Diese Funktion funktioniert auch mit relativen Pfaden:

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

7.6 Parsen von Pfaden: Extrahieren verschiedener Teile eines Pfades (Dateiname, Erweiterung usw.)

7.6.1 path.parse(): Erstellen eines Objekts mit Pfadteilen

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

Extrahiert verschiedene Teile von path und gibt sie in einem Objekt mit den folgenden Eigenschaften zurück:

Später sehen wir die Funktion path.format(), die das Gegenteil von path.parse() ist: Sie konvertiert ein Objekt mit Pfadteilen in einen Pfad.

7.6.1.1 Beispiel: path.parse() unter Unix

So sieht die Verwendung von path.parse() unter Unix aus:

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

Das folgende Diagramm visualisiert den Umfang der Teile:

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

Zum Beispiel sehen wir, dass .dir der Pfad ohne die Basis ist. Und dass .base gleich .name plus .ext ist.

7.6.1.2 Beispiel: path.parse() unter Windows

So funktioniert path.parse() unter Windows:

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

Dies ist ein Diagramm für das Ergebnis:

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

7.6.2 path.basename(): Extrahieren der Basis eines Pfades

path.basename(path, ext?)

Gibt die Basis von path zurück:

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

Optional kann diese Funktion auch ein Suffix entfernen:

> 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'

Das Entfernen der Erweiterung ist Case-sensitiv – sogar unter 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(): Extrahieren des übergeordneten Verzeichnisses eines Pfades

path.dirname(path)

Gibt das übergeordnete Verzeichnis der Datei oder des Verzeichnisses unter path zurück:

> 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(): Extrahieren der Erweiterung eines Pfades

path.extname(path)

Gibt die Erweiterung von path zurück:

> 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 Kategorisieren von Pfaden

7.7.1 path.isAbsolute(): Ist ein gegebener Pfad absolut?

path.isAbsolute(path: string): boolean

Gibt true zurück, wenn path absolut ist, und false andernfalls.

Die Ergebnisse unter Unix sind unkompliziert:

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

Unter Windows bedeutet „absolut“ nicht unbedingt „vollständig qualifiziert“ (nur der erste Pfad ist vollständig qualifiziert):

> 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(): Erstellen von Pfaden aus Teilen

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

Erstellt einen Pfad aus einem Pfadobjekt:

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

7.8.1 Beispiel: Ändern der Dateinamenerweiterung

Wir können path.format() verwenden, um die Erweiterung eines Pfades zu ändern:

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'
);

Wenn wir die ursprüngliche Dateinamenerweiterung kennen, können wir auch einen regulären Ausdruck verwenden, um die Dateinamenerweiterung zu ändern:

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

7.9 Verwenden derselben Pfade auf verschiedenen Plattformen

Manchmal möchten wir dieselben Pfade auf verschiedenen Plattformen verwenden. Dann stehen wir vor zwei Problemen:

Betrachten wir als Beispiel eine Node.js-App, die auf einem Verzeichnis mit Daten operiert. Nehmen wir an, die App kann mit zwei Arten von Pfaden konfiguriert werden:

Aufgrund der oben genannten Probleme:

7.9.1 Relative plattformunabhängige Pfade

Relative plattformunabhängige Pfade können als Arrays von Pfadsegmenten gespeichert und wie folgt in vollständig qualifizierte plattformspezifische Pfade umgewandelt werden:

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'
);

Um relative plattformspezifische Pfade zu erstellen, können wir verwenden:

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'
);

Die folgende Funktion konvertiert relative plattformspezifische Pfade in plattformunabhängige Pfade:

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;
}

Verwendung von splitRelativePathIntoSegments() unter Unix:

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

Verwendung von splitRelativePathIntoSegments() unter Windows:

> 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 Verwenden einer Bibliothek zum Abgleichen von Pfaden über Globs

Das npm-Modul 'minimatch' ermöglicht es uns, Pfade gegen Muster abzugleichen, die als Glob-Ausdrücke, Glob-Muster oder Globs bezeichnet werden:

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
);

Anwendungsfälle für Globs:

Weitere Glob-Bibliotheken:

7.10.1 Die minimatch-API

Die gesamte API von minimatch ist in der Readme-Datei des Projekts dokumentiert. In diesem Unterabschnitt betrachten wir die wichtigste Funktionalität.

Minimatch kompiliert Globs in JavaScript-RegExp-Objekte und verwendet diese zum Abgleichen.

7.10.1.1 minimatch(): Kompilieren und einmaliges Abgleichen
minimatch(path: string, glob: string, options?: MinimatchOptions): boolean

Gibt true zurück, wenn glob mit path übereinstimmt, und false andernfalls.

Zwei interessante Optionen:

7.10.1.2 new minimatch.Minimatch(): Einmaliges Kompilieren, mehrmaliges Abgleichen

Die Klasse minimatch.Minimatch ermöglicht es uns, den Glob nur einmal in einen regulären Ausdruck zu kompilieren und mehrmals abzugleichen:

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

So wird diese Klasse verwendet:

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 Syntax von Glob-Ausdrücken

Dieser Unterabschnitt behandelt die Grundlagen der Syntax. Aber es gibt weitere Funktionen. Diese sind hier dokumentiert:

7.10.2.1 Abgleichen von Windows-Pfaden

Auch unter Windows werden Glob-Segmente durch Schrägstriche getrennt – sie stimmen aber sowohl mit Backslashes als auch mit Schrägstrichen überein (die unter Windows gültige Pfadtrennzeichen sind).

> minimatch('dir\\sub/file.txt', 'dir/sub/file.txt')
true
7.10.2.2 Minimatch normalisiert Pfade nicht

Minimatch normalisiert Pfade nicht für uns:

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

Daher müssen wir Pfade normalisieren, wenn wir sie nicht selbst erstellen:

> path.normalize('./file.txt')
'file.txt'
7.10.2.3 Muster ohne Wildcard-Symbole: Pfadtrennzeichen müssen übereinstimmen

Muster ohne Wildcard-Symbole (die flexibler abgleichen) müssen exakt übereinstimmen. Insbesondere müssen die Pfadtrennzeichen übereinstimmen:

> 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

Das heißt, wir müssen uns entweder für absolute oder relative Pfade entscheiden.

Mit der Option .matchBase können wir Muster ohne Schrägstriche mit den Basisnamen von Pfaden abgleichen:

> minimatch('/dir/file.txt', 'file.txt', {matchBase: true})
true
7.10.2.4 Das Sternchen (*) gleicht ein einzelnes Segment (oder einen Teil davon) ab

Das Wildcard-Symbol Sternchen (*) gleicht jedes Pfadsegment oder jeden Teil eines Segments ab:

> 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

Das Sternchen gleicht keine „unsichtbaren Dateien“ ab, deren Namen mit Punkten beginnen. Wenn wir diese abgleichen wollen, müssen wir das Sternchen mit einem Punkt versehen:

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

Die Option .dot ermöglicht es uns, dieses Verhalten zu deaktivieren:

> minimatch('.gitignore', '*', {dot: true})
true
> minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt', {dot: true})
true
7.10.2.5 Das doppelte Sternchen (**) gleicht null oder mehr Segmente ab

´**/ gleicht null oder mehr Segmente ab.

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

Wenn wir relative Pfade abgleichen wollen, darf das Muster immer noch nicht mit einem Pfadtrennzeichen beginnen:

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

Das doppelte Sternchen gleicht keine „unsichtbaren“ Pfadsegmente ab, deren Namen mit Punkten beginnen:

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

Wir können dieses Verhalten über die Option .dot deaktivieren:

> minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json', {dot: true})
true
7.10.2.6 Negieren von Globs

Wenn wir einen Glob mit einem Ausrufezeichen beginnen, gleicht er, wenn das Muster nach dem Ausrufezeichen nicht übereinstimmt:

> minimatch('file.txt', '!**/*.txt')
false
> minimatch('file.js', '!**/*.txt')
true
7.10.2.7 Alternative Muster

Komma-getrennte Muster in geschweiften Klammern stimmen überein, wenn eines der Muster übereinstimmt:

> minimatch('file.txt', 'file.{txt,js}')
true
> minimatch('file.js', 'file.{txt,js}')
true
7.10.2.8 Bereiche von ganzen Zahlen

Ein Paar ganzer Zahlen, getrennt durch doppelte Punkte, definiert einen Bereich von ganzen Zahlen und stimmt überein, wenn eines seiner Elemente übereinstimmt:

> 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

Auffüllen mit Nullen wird ebenfalls unterstützt:

> 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 Verwenden von file:-URLs zum Referenzieren von Dateien

Es gibt zwei gängige Arten, in Node.js auf Dateien zu verweisen:

Zum Beispiel

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 Klasse URL

In diesem Abschnitt befassen wir uns genauer mit der Klasse URL. Weitere Informationen zu dieser Klasse:

In diesem Kapitel greifen wir über eine globale Variable auf die Klasse URL zu, da sie auf anderen Webplattformen so verwendet wird. Sie kann aber auch importiert werden:

import {URL} from 'node:url';
7.11.1.1 URIs vs. relative Referenzen

URLs sind eine Teilmenge von URIs. RFC 3986, der Standard für URIs, unterscheidet zwei Arten von URI-Referenzen:

7.11.1.2 Konstruktor von URL

Die Klasse URL kann auf zwei Arten instanziiert werden:

Hier sehen wir die Klasse in Aktion:

// 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 Auflösen relativer Referenzen gegen Instanzen von URL

Lassen Sie uns diese Variante des URL-Konstruktors erneut betrachten:

new URL(uriRef: string, baseUri: string)

Das Argument baseUri wird in eine Zeichenkette umgewandelt. Daher kann jedes Objekt verwendet werden – solange es beim Umwandeln in eine Zeichenkette zu einer gültigen URL wird.

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

Das ermöglicht es uns, relative Referenzen gegen URL-Instanzen aufzulösen:

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

So verwendet, ähnelt der Konstruktor lose path.resolve().

7.11.1.4 Eigenschaften von URL-Instanzen

Instanzen von URL haben die folgenden Eigenschaften:

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 Konvertieren von URLs in Zeichenketten

Es gibt drei gängige Möglichkeiten, URLs in Zeichenketten zu konvertieren:

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'
);

Die Methode .toJSON() ermöglicht es uns, URLs in JSON-Daten zu verwenden.

const jsonStr = JSON.stringify({
  pageUrl: new URL('https://exploringjs.de')
});
assert.equal(
  jsonStr, '{"pageUrl":"https://exploringjs.de"}'
);
7.11.1.6 Abrufen von URL-Eigenschaften

Die Eigenschaften von URL-Instanzen sind keine eigenen Dateneigenschaften, sie werden über Getter und Setter implementiert. Im nächsten Beispiel verwenden wir die Hilfsfunktion pickProps() (deren Code am Ende gezeigt wird), um die von diesen Gettern zurückgegebenen Werte in ein einfaches Objekt zu kopieren:

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;
}

Leider ist der Pfadname eine einzelne atomare Einheit. Das heißt, wir können die Klasse URL nicht verwenden, um auf ihre Teile (Basis, Erweiterung usw.) zuzugreifen.

7.11.1.7 Festlegen von Teilen einer URL

Wir können auch Teile einer URL ändern, indem wir Eigenschaften wie .hostname festlegen:

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

Wir können die Setter verwenden, um URLs aus Teilen zu erstellen (Idee von 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 Verwaltung von Suchparametern über .searchParams

Wir können die Eigenschaft .searchParams verwenden, um die Suchparameter von URLs zu verwalten. Ihr Wert ist eine Instanz von URLSearchParams.

Wir können sie verwenden, um Suchparameter zu lesen:

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

Wir können Suchparameter auch über sie ändern:

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 Konvertierung zwischen URLs und Dateipfaden

Es ist verlockend, zwischen Dateipfaden und URLs manuell zu konvertieren. Zum Beispiel können wir versuchen, eine URL-Instanz myUrl über myUrl.pathname in einen Dateipfad zu konvertieren. Das funktioniert jedoch nicht immer – es ist besser, diese Funktion zu verwenden:

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

Der folgende Code vergleicht die Ergebnisse dieser Funktion mit den Werten von .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\\');

Diese Funktion ist das Gegenteil von url.fileURLToPath():

url.pathToFileURL(path: string): URL

Sie konvertiert path in eine Datei-URL.

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

7.11.3 Anwendungsfall für URLs: Zugriff auf Dateien relativ zum aktuellen Modul

Ein wichtiger Anwendungsfall für URLs ist der Zugriff auf eine Datei, die ein Geschwistermodul des aktuellen Moduls ist:

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

Diese Funktion verwendet import.meta.url, die die URL des aktuellen Moduls enthält (was in Node.js normalerweise eine file:-URL ist).

Die Verwendung von fetch() hätte den vorherigen Code noch plattformunabhängiger gemacht. Allerdings funktioniert fetch() in Node.js 18.9.0 noch nicht für file:-URLs:

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

7.11.4 Anwendungsfall für URLs: Erkennen, ob das aktuelle Modul „main“ (der Einstiegspunkt der App) ist

Ein ESM-Modul kann auf zwei Arten verwendet werden:

  1. Es kann als Bibliothek verwendet werden, von der andere Module Werte importieren können.
  2. Es kann als Skript verwendet werden, das wir über Node.js ausführen – z. B. von der Kommandozeile aus. In diesem Fall wird es als Hauptmodul bezeichnet.

Wenn wir möchten, dass ein Modul auf beide Arten verwendet werden kann, benötigen wir eine Möglichkeit zu prüfen, ob das aktuelle Modul das Hauptmodul ist, da nur dann die Skriptfunktionalität ausgeführt wird. In diesem Kapitel lernen wir, wie wir diese Prüfung durchführen.

7.11.4.1 Bestimmen, ob ein CommonJS-Modul main ist

Mit CommonJS können wir das folgende Muster verwenden, um zu erkennen, ob das aktuelle Modul der Einstiegspunkt war (Quelle: Node.js-Dokumentation):

if (require.main === module) {
  // Main CommonJS module
}
7.11.4.2 Bestimmen, ob ein ESM-Modul main ist

Derzeit haben ESM-Module keine einfache eingebaute Möglichkeit zu prüfen, ob ein Modul main ist. Stattdessen müssen wir die folgende Problemumgehung verwenden (basierend auf einem Tweet von 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
  }
}

Erläuterungen

7.11.5 Pfade vs. file:-URLs

Wenn Shell-Skripte Verweise auf Dateien erhalten oder Verweise auf Dateien exportieren (z. B. indem sie sie auf dem Bildschirm protokollieren), sind es praktisch immer Pfade. Es gibt jedoch zwei Fälle, in denen wir URLs benötigen (wie in den vorherigen Unterabschnitten besprochen):