在这篇博文中,我们学习如何在Node.js上使用文件系统路径。
目录。
- Node.js上与路径相关的功能
- 基本的路径概念和它们的API支持
- 通过'node:os'模块获取重要目录的路径
- 串联路径
- 确保路径是规范化的、完全合格的或相对的
- 解析路径:提取路径的各个部分(文件名扩展名等)。
- 对路径进行分类
- path.format():从部分内容中创建路径
- 在不同的平台上使用相同的路径
- 使用一个库,通过globs匹配路径
- 使用文件。URL来引用文件
在这篇博文中,我们探讨了Node.js上的路径相关功能。
- 大多数与路径相关的功能都在模块
'node:path'。 - 全局变量
process有改变当前工作目录的方法(那是什么,很快就会解释)。 - 模块
'node:os'有返回重要目录路径的函数。
访问'node:path' API的三种方式
模块'node:path' 经常被导入,如下所示。
import * as path from 'node:path';
在这篇博文中,这个导入语句偶尔会被省略掉。我们也省略了下面的导入。
import * as assert from 'node:assert/strict';
我们可以通过三种方式访问Node的路径API。
-
我们可以访问特定平台版本的API。
path.posix支持 Unixes,包括 macOS。path.win32支持Windows。
-
path本身总是支持当前的平台。例如,这是一个在macOS上的REPL交互。> path.parse === path.posix.parse true
让我们看看解析文件系统路径的函数path.parse() ,在这两个平台上有什么不同。
> 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 (关于其他属性的含义,稍后再谈)。
基本的路径概念和它们的API支持
路径段、路径分隔符、路径定界符
术语。
-
一个非空的路径由一个或多个路径段组成,通常是目录或文件的名称。
-
路径分隔器用于分隔路径中两个相邻的路径段。
> path.posix.sep '/' > path.win32.sep '\\' -
路径分隔符分隔路径列表中的元素。
> path.posix.delimiter ':' > path.win32.delimiter ';'
如果我们检查一下PATH这个外壳变量,就可以看到路径分隔符和路径定界符--它包含操作系统在外壳中输入命令时寻找可执行文件的路径。
这是一个macOS PATH的例子(shell变量$PATH )。
> process.env.PATH.split(/(?<=:)/)
[
'/opt/homebrew/bin:',
'/opt/homebrew/sbin:',
'/usr/local/bin:',
'/usr/bin:',
'/bin:',
'/usr/sbin:',
'/sbin',
]
分隔符的长度为0,因为lookbehind断言 (?<=:) ,如果给定的位置前面有一个冒号,它就会匹配,但它并没有捕获任何东西。因此,路径分隔符':' ,包括在前面的路径中。
这是一个Windows PATH的例子(shell变量%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\\',
]
当前工作目录
许多shell有当前工作目录(CWD)的概念--"我当前所在的目录"。
- 如果我们使用一个带有部分限定路径的命令,该路径就会针对CWD进行解析。
- 如果我们省略了一个路径,而一个命令希望有一个路径,那么就会使用CWD。
- 在Unixs和Windows上,改变CWD的命令是
cd。
process 是一个全局Node.js变量。它为我们提供了获取和设置CWD的方法。
process.cwd()返回CWD。process.chdir(dirPath)将CWD改为dirPath。- 必须有一个目录在
dirPath。 - 这种改变并不影响shell,只影响当前运行的Node.js进程。
- 必须有一个目录在
Node.js使用CWD来填补缺失的部分,只要路径不完全合格(完整)。这使我们能够在各种功能中使用部分限定的路径,例如:fs.readFileSync() 。
Unix上的当前工作目录
下面的代码演示了Unix上的process.chdir() 和process.cwd() 。
process.chdir('/home/jane');
assert.equal(
process.cwd(), '/home/jane'
);
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'
);
完全限定路径与部分限定路径,解析路径
- 一个完全合格的路径不依赖于任何其他信息,可以按原样使用。
- 部分限定的路径缺少信息。我们需要在使用它之前把它变成一个完全合格的路径。这可以通过将其与完全合格的路径进行解析来实现。
Unix上的完全和部分合格的路径
Unix只知道两种类型的路径。
-
绝对路径是完全合格的,以斜线开头。
/home/john/proj -
相对路径是部分限定的,以文件名或点开始。
. (current directory) .. (parent directory) dir ./dir ../dir ../../dir/subdir
让我们用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'
Windows上的完全和部分限定路径
Windows区分了四种路径(更多信息请参见微软的文档)。
- 有绝对路径和相对路径。
- 这两种路径都可以有一个盘符("卷标")或没有。
带有盘符的绝对路径是完全合格的。所有其他路径都是部分限定的。
对照完全限定的路径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'
针对完全合格的路径full ,解决一个带盘符的相对路径rel ,取决于rel 的盘符。
- 与
full相同的盘符?将rel与full对照解决。 - 与
full不同的盘符?将rel与rel's drive的当前目录进行比对。
这看起来如下。
// 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'
);
通过模块'node:os' 获取重要目录的路径
模块'node:os' 为我们提供了两个重要目录的路径。
-
os.homedir()返回当前用户的主目录的路径 - 例如。> os.homedir() // macOS '/Users/rauschma' > os.homedir() // Windows 'C:\\Users\\axel' -
os.tmpdir()返回操作系统的临时文件目录的路径--例如。> os.tmpdir() // macOS '/var/folders/ph/sz0384m11vxf5byk12fzjms40000gn/T' > os.tmpdir() // Windows 'C:\\Users\\axel\\AppData\\Local\\Temp'
连接路径
有两个函数用于连接路径。
path.resolve()总是返回完全合格的路径path.join()保留相对路径
path.resolve():串联路径以创建完全合格的路径
path.resolve(...paths: Array<string>): string
串联paths ,并返回一个完全合格的路径。它使用以下算法。
- 从当前工作目录开始。
- 将
path[0]与之前的结果进行比对。 - 将
path[1]与之前的结果进行比对。 - 对所有剩余的路径做同样的处理。
- 返回最后的结果。
没有参数,path.resolve() ,返回当前工作目录的路径。
> process.cwd()
'/usr/local'
> path.resolve()
'/usr/local'
一个或多个相对路径被用于解析,从当前工作目录开始。
> 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'
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'
使用两个以上的参数。
> path.posix.join('/usr/local', '../lib', '.', 'log')
'/usr/lib/log'
确保路径是规范化的,完全合格的,或相对的
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'
path.resolve() (一个参数):确保路径被规范化并完全合格
我们已经遇到了 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'
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上,如果sourcePath 和destinationPath 是在不同的驱动器上,我们会得到一个完全合格的路径。
> path.win32.relative('Z:\\tmp\\', 'C:\\Users\\Jane\\')
'C:\\Users\\Jane'
这个函数也适用于相对路径。
> path.posix.relative('proj/my-lib/', 'doc/zsh.txt')
'../../doc/zsh.txt'
path.parse(): 创建一个带有路径部分的对象
type PathObject = {
dir: string,
root: string,
base: string,
name: string,
ext: string,
};
path.parse(path: string): PathObject
提取path 的各个部分,并在一个对象中返回,其属性如下。
.base: 路径的最后一段.ext: 基地的文件名扩展名.name: 不含扩展名的基数。这一部分也被称为路径的茎。
.root: 路径的开头(在第一段之前)。.dir: 基地所在的目录 - 不含基地的路径
稍后,我们将看到函数path.format(),它是path.parse() 的逆运算。它将一个有路径部分的对象转换成一个路径。
例如: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 是.name 加上.ext 。
例子:Windows上的path.parse()
这就是path.parse() 在Windows上的工作原理。
> 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 |
path.basename(path, ext?)
返回path 的基数。
> 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'
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'
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')
''
对路径进行分类
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
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'
例子:改变文件名的扩展名
我们可以用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'
在不同的平台上使用相同的路径
有时我们想在不同的平台上使用相同的路径。那么我们就会面临两个问题。
- 路径分隔符可能不同。
- 文件结构可能不同:主目录和临时文件的目录可能在不同的位置,等等。
作为一个例子,考虑一个Node.js应用程序,它在一个有数据的目录上操作。让我们假设该应用可以配置两种路径。
- 系统中任何地方的全限定路径
- 数据目录内的路径
由于前面提到的问题。
-
我们不能在平台之间重复使用完全限定的路径。
- 有时我们需要绝对路径。这些必须在数据目录的每个 "实例 "中进行配置,并存储在外部(或存储在里面,被版本控制忽略)。这些路径保持不变,不随数据目录移动。
-
我们可以重新使用指向数据目录的路径。这些路径可以存储在配置文件(无论是否在数据目录内)和应用程序代码中的常量中。要做到这一点。
- 我们必须将它们存储为相对路径。
- 我们必须确保每个平台上的路径分隔符是正确的。
下一小节将解释如何实现这两个目标。
独立于平台的相对路径
与平台无关的相对路径可以存储为路径段的数组,并按以下方式转化为完全合格的平台特定路径。
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' ]
使用一个库来通过globs匹配路径
npm模块'minimatch'可以让我们根据被称为glob表达式、glob模式或globs的模式来匹配路径。
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
);
globs的使用情况。
- 指定一个目录中的哪些文件应该被脚本处理。
- 指定哪些文件应该被忽略。
更多的glob库。
- multimatch扩展了minimatch,支持多种模式。
- micromatch是 minimatch 和 multimatch 的替代品,它有类似的 API。
- globby是一个基于fast-glob的库,它增加了便利的功能。
minimatch 的 API
在项目的 readme 文件中记录了 minimatch 的整个 API。在本小节中,我们看一下最重要的功能。
Minimatch 将 globs 编译成 JavaScriptRegExp 对象并使用这些对象进行匹配。
minimatch():编译和匹配一次
minimatch(path: string, glob: string, options?: MinimatchOptions): boolean
如果glob 匹配path ,则返回true ,否则返回false 。
有两个有趣的选项。
-
.dot: boolean(默认: )false
如果 ,通配符,如 和 匹配 "不可见 "的路径段(其名称以点开始)。true***> minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json') false > minimatch('/usr/local/.tmp/data.json', '/usr/**/data.json', {dot: true}) true > minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt') false > minimatch('/tmp/.log/events.txt', '/tmp/*/events.txt', {dot: true}) true -
.matchBase: boolean(默认: )false
如果 ,不含斜线的模式将与路径的基名匹配。true> minimatch('/dir/file.txt', 'file.txt') false > minimatch('/dir/file.txt', 'file.txt', {matchBase: true}) true
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
);
glob表达式的语法
这个小节涵盖了语法的基本内容。但是还有更多的功能。这里记录了这些内容。
- Minimatch的单元测试中有许多关于glob的例子。
- Bash参考手册中有一个关于文件名扩展的章节。
匹配Windows路径
即使在Windows上,glob段也是由斜线分隔的--但是它们同时匹配反斜线和斜线(这在Windows上是合法的路径分隔符)。
> minimatch('dir\\sub/file.txt', 'dir/sub/file.txt')
true
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'
没有通配符的模式:路径分隔符必须排成一行
没有通配符的模式(可以更灵活地匹配)必须完全匹配。特别是路径分隔符必须排成一行。
> 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 ,我们可以用不带斜线的模式与路径的基名相匹配。
> minimatch('/dir/file.txt', 'file.txt', {matchBase: true})
true
星号 (*) 匹配任何(部分)单段。
通配符星号 (*) 匹配任何路径段或一个段的任何部分。
> 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
双星号 (**) 匹配零个或多个片段
**/ 匹配零个或多个段。
> 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
否定globbs
如果我们用感叹号开始一个glob,那么如果感叹号后面的模式不匹配,它就会匹配。
> minimatch('file.txt', '!**/*.txt')
false
> minimatch('file.js', '!**/*.txt')
true
替代模式
在大括号内的逗号分隔的模式,如果其中一个模式匹配,就会匹配。
> minimatch('file.txt', 'file.{txt,js}')
true
> minimatch('file.js', 'file.{txt,js}')
true
整数的范围
一对用双点分隔的整数定义了一个整数范围,如果其中的任何元素匹配,则匹配。
> 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
使用file: URLs来引用文件
在Node.js中,有两种常见的方法来引用文件。
- 字符串中的路径
- 带有协议的
URL的实例file:
比如说
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'
);
类URL
在本节中,我们仔细看看URL 类。关于这个类的更多信息。
- Node.js 文档:"WHATWG URL API "部分
- WHATWG URL标准的"API "部分
在这篇博文中,我们通过一个全局变量访问类URL ,因为它在其他网络平台上是这样使用的。但它也可以被导入。
import {URL} from 'node:url';
URIs vs. 相对引用
URLs是URIs的一个子集。RFC 3986,URI的标准,区分了两种URI-引用。
- 一个URI以一个方案开始,后面是一个冒号分隔符。
- 所有其他URI引用都是相对引用。
URL 的构造函数
URL 类可以通过两种方式进行实例化。
-
new URL(uri: string)uri必须是一个URI。它指定了新实例的URI。 -
new URL(uriRef: string, baseUri: string)baseUri必须是一个URI。如果 是一个相对引用,它将被解析为与 相对应,其结果成为新实例的 URI。uriRefbaseUri如果
uriRef是一个URI,它将完全取代baseUri,成为实例所基于的数据。
在这里我们可以看到这个类的作用。
// 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'
);
解决对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() 宽泛地相似。
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,
}
将URL转换为字符串
有三种常见的方法,我们可以将URL转换为字符串。
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数据中使用URLs。
const jsonStr = JSON.stringify({
pageUrl: new URL('https://2ality.com/p/subscribe.html')
});
assert.equal(
jsonStr, '{"pageUrl":"https://2ality.com/p/subscribe.html"}'
);
获取URL 的属性
URL 实例的属性不是自己的数据属性,它们是通过getters和setters实现的。在下一个例子中,我们使用实用函数pickProps() (其代码在最后显示),将这些getters返回的值复制到一个普通对象中。
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 来访问它的各个部分(基础、扩展等)。
设置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'
);
通过.searchParams 管理搜索参数
我们可以使用属性.searchParams 来管理URLs的搜索参数。它的值是一个 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'
);
在URL和文件路径之间进行转换
在文件路径和URL之间手动转换是很诱人的。例如,我们可以尝试通过myUrl.pathname 将URL 实例myUrl 转换为一个文件路径。然而这并不总是奏效--最好是使用这个函数。
url.fileURLToPath(url: URL | string): string
下面的代码将该函数的结果与.pathname 的值进行比较。
import * as assert from 'assert';
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'
URLs的用例:访问相对于当前模块的文件
URLs的一个重要用例是访问当前模块的兄弟姐妹的文件。
function readData() {
const url = new URL('data.txt', import.meta.url);
return fs.readFileSync(url, {encoding: 'UTF-8'});
}
这个函数使用 import.meta.url它包含了当前模块的URL(在Node.js上通常是一个file: URL)。
使用fetch() ,可以使以前的代码更具有跨平台性。然而,从Node.js 18.5开始,fetch() 还不能用于file: URLs。
> await fetch('file:///tmp/file.txt')
TypeError: fetch failed
cause: Error: not implemented... yet...
URLs的用例:检测当前模块是否作为脚本运行
见博文 "Node.js:检查ESM模块是否为'main'"。
路径 vs.file: URLs
当shell脚本接收对文件的引用或输出对文件的引用时(例如通过在屏幕上记录它们),它们几乎都是路径。然而,有两种情况下我们需要URL(正如前面几个小节所讨论的)。
- 访问相对于当前模块的文件
- 检测当前模块是否以脚本形式运行