mkdir
、mkdir -p
)rm
、rm -r
)rmdir
)本章包含
鉴于本书的重点是 shell 脚本,我们只处理文本数据。
fs.open()
等)fs.openSync(path, flags?, mode?)
为给定路径的文件打开一个新的文件描述符并返回它。fs.closeSync(fd)
关闭文件描述符。fs.fchmodSync(fd, mode)
fs.fchownSync(fd, uid, gid)
fs.fdatasyncSync(fd)
fs.fstatSync(fd, options?)
fs.fsyncSync(fd)
fs.ftruncateSync(fd, len?)
fs.futimesSync(fd, atime, mtime)
FileHandle
,它基于文件描述符。实例通过 fsPromises.open()
创建。各种操作通过方法(而不是函数)提供fileHandle.close()
fileHandle.chmod(mode)
fileHandle.chown(uid, gid)
请注意,我们在本章中没有使用 (3) - (1) 和 (2) 就足以满足我们的目的。
名称以“l”开头的函数通常对符号链接进行操作
fs.lchmodSync()
、fs.lchmod()
、fsPromises.lchmod()
fs.lchownSync()
、fs.lchown()
、fsPromises.lchown()
fs.lutimesSync()
、fs.lutimes()
、fsPromises.lutimes()
名称以“f”开头的函数通常管理文件描述符
fs.fchmodSync()
、fs.fchmod()
fs.fchownSync()
、fs.fchown()
fs.fstatSync()
、fs.fstat()
几个类在 Node 的文件系统 API 中扮演着重要的角色。
每当 Node.js 函数接受字符串中的文件系统路径(行 A)时,它通常也接受 URL
的实例(行 B)
.equal(
assert.readFileSync(
fs'/tmp/text-file.txt', {encoding: 'utf-8'}), // (A)
'Text content'
;
).equal(
assert.readFileSync(
fsnew URL('file:///tmp/text-file.txt'), {encoding: 'utf-8'}), // (B)
'Text content'
; )
手动在路径和 file:
URL 之间转换看似容易,但却有许多令人惊讶的陷阱:百分号编码或解码、Windows 驱动器号等。相反,最好使用以下两个函数
我们在本章中没有使用文件 URL。它们的用例在 §7.11.1 “类 URL
” 中有描述。
类 Buffer
表示 Node.js 上的固定长度字节序列。它是 Uint8Array
的子类(TypedArray)。缓冲区主要用于处理二进制文件,因此在本书中不太受关注。
每当 Node.js 接受 Buffer 时,它也接受 Uint8Array。因此,鉴于 Uint8Arrays 是跨平台的而 Buffer 不是,前者更可取。
缓冲区可以做 Uint8Arrays 做不到的一件事:以各种编码对文本进行编码和解码。如果我们需要在 Uint8Arrays 中编码或解码 UTF-8,我们可以使用类 TextEncoder
或类 TextDecoder
。这些类在大多数 JavaScript 平台上都可用
> new TextEncoder().encode('café')Uint8Array.of(99, 97, 102, 195, 169)
> new TextDecoder().decode(Uint8Array.of(99, 97, 102, 195, 169))'café'
一些函数接受或返回原生 Node.js 流
stream.Readable
是 Node 用于可读流的类。模块 node:fs
使用 fs.ReadStream
,它是其子类。stream.Writable
是 Node 用于可写流的类。模块 node:fs
使用 fs.WriteStream
,它是其子类。现在,我们可以在 Node.js 上使用跨平台的*Web 流*,而不是原生流。如何操作在 §10 “在 Node.js 上使用 Web 流” 中有解释。
fs.readFileSync(filePath, options?)
将 filePath
处的文件读取到单个字符串中
.equal(
assert.readFileSync('text-file.txt', {encoding: 'utf-8'}),
fs'there\r\nare\nmultiple\nlines'
; )
这种方法的优缺点(与使用流相比)
接下来,我们将研究如何将读取的字符串拆分为行。
以下代码将字符串拆分为行,同时删除行终止符。它适用于 Unix 和 Windows 行终止符
const RE_SPLIT_EOL = /\r?\n/;
function splitLines(str) {
return str.split(RE_SPLIT_EOL);
}.deepEqual(
assertsplitLines('there\r\nare\nmultiple\nlines'),
'there', 'are', 'multiple', 'lines']
[; )
“EOL”代表“行尾”。我们接受 Unix 行终止符 ('\n'
) 和 Windows 行终止符 ('\r\n'
,如上一个示例中的第一个)。有关更多信息,请参阅 §8.3 “跨平台处理行终止符”。
以下代码将字符串拆分为行,同时包含行终止符。它适用于 Unix 和 Windows 行终止符(“EOL”代表“行尾”)
const RE_SPLIT_AFTER_EOL = /(?<=\r?\n)/; // (A)
function splitLinesWithEols(str) {
return str.split(RE_SPLIT_AFTER_EOL);
}
.deepEqual(
assertsplitLinesWithEols('there\r\nare\nmultiple\nlines'),
'there\r\n', 'are\n', 'multiple\n', 'lines']
[;
).deepEqual(
assertsplitLinesWithEols('first\n\nthird'),
'first\n', '\n', 'third']
[;
).deepEqual(
assertsplitLinesWithEols('EOL at the end\n'),
'EOL at the end\n']
[;
).deepEqual(
assertsplitLinesWithEols(''),
'']
[; )
行 A 包含一个带有 后视断言 的正则表达式。它匹配模式 \r?\n
匹配之前的那些位置,但它不捕获任何内容。因此,它不会删除输入字符串拆分成的字符串片段之间的任何内容。
在不支持后视断言的引擎上(请参阅此表),我们可以使用以下解决方案
function splitLinesWithEols(str) {
if (str.length === 0) return [''];
const lines = [];
let prevEnd = 0;
while (prevEnd < str.length) {
// Searching for '\n' means we’ll also find '\r\n'
const newlineIndex = str.indexOf('\n', prevEnd);
// If there is a newline, it’s included in the line
const end = newlineIndex < 0 ? str.length : newlineIndex+1;
.push(str.slice(prevEnd, end));
lines= end;
prevEnd
}return lines;
}
此解决方案很简单,但更冗长。
在这两个版本的 splitLinesWithEols()
中,我们再次接受 Unix 行终止符 ('\n'
) 和 Windows 行终止符 ('\r\n'
)。有关更多信息,请参阅 §8.3 “跨平台处理行终止符”。
我们也可以通过流读取文本文件
import {Readable} from 'node:stream';
const nodeReadable = fs.createReadStream(
'text-file.txt', {encoding: 'utf-8'});
const webReadableStream = Readable.toWeb(nodeReadable);
const lineStream = webReadableStream.pipeThrough(
new ChunksToLinesStream());
for await (const line of lineStream) {
console.log(line);
}
// Output:
// 'there\r\n'
// 'are\n'
// 'multiple\n'
// 'lines'
我们使用了以下外部功能
fs.createReadStream(filePath, options?)
创建一个 Node.js 流(stream.Readable
的实例)。stream.Readable.toWeb(streamReadable)
将可读 Node.js 流转换为 Web 流(ReadableStream
的实例)。ChunksToLinesStream
在 §10.7.1 “示例:将任意块的流转换为行的流” 中有解释。*块*是流产生的数据片段。如果我们有一个块是任意长度字符串的流,并将其通过 ChunksToLinesStream 传输,那么我们将获得一个块是行的流。Web 流是 异步可迭代的,这就是为什么我们可以使用 for-await-of
循环来迭代行。
如果我们对文本行不感兴趣,那么我们不需要 ChunksToLinesStream
,可以迭代 webReadableStream
并获取任意长度的块。
更多信息
Web 流在 §10 “在 Node.js 上使用 Web 流” 中有介绍。
行终止符在 §8.3 “跨平台处理行终止符” 中有介绍。
这种方法的优缺点(与读取单个字符串相比)
fs.writeFileSync(filePath, str, options?)
将 str
写入 filePath
处的文件。如果该路径下已存在文件,则会被覆盖。
以下代码展示了如何使用此函数
.writeFileSync(
fs'new-file.txt',
'First line\nSecond line\n',
encoding: 'utf-8'}
{; )
有关行终止符的信息,请参阅 §8.3 “跨平台处理行终止符”。
优缺点(与使用流相比)
以下代码将一行文本追加到现有文件
.appendFileSync(
fs'existing-file.txt',
'Appended line\n',
encoding: 'utf-8'}
{; )
我们也可以使用 fs.writeFileSync()
来执行此任务
.writeFileSync(
fs'existing-file.txt',
'Appended line\n',
encoding: 'utf-8', flag: 'a'}
{; )
这段代码与我们用来覆盖现有内容的代码几乎相同(有关详细信息,请参阅上一节)。唯一的区别是我们添加了选项 .flag
:值 'a'
表示我们追加数据。其他可能的值(例如,如果文件尚不存在则抛出错误)在Node.js 文档中进行了解释。
注意:在某些函数中,此选项名为 .flag
,而在其他函数中则名为 .flags
。
以下代码使用流将多个字符串写入文件
import {Writable} from 'node:stream';
const nodeWritable = fs.createWriteStream(
'new-file.txt', {encoding: 'utf-8'});
const webWritableStream = Writable.toWeb(nodeWritable);
const writer = webWritableStream.getWriter();
try {
await writer.write('First line\n');
await writer.write('Second line\n');
await writer.close();
finally {
} .releaseLock()
writer }
我们使用了以下函数
fs.createWriteStream(path, options?)
创建一个 Node.js 流(stream.Writable
的实例)。stream.Writable.toWeb(streamWritable)
将可写 Node.js 流转换为 Web 流(WritableStream
的实例)。更多信息
优缺点(与写入单个字符串相比)
以下代码使用流将文本追加到现有文件
import {Writable} from 'node:stream';
const nodeWritable = fs.createWriteStream(
'existing-file.txt', {encoding: 'utf-8', flags: 'a'});
const webWritableStream = Writable.toWeb(nodeWritable);
const writer = webWritableStream.getWriter();
try {
await writer.write('First appended line\n');
await writer.write('Second appended line\n');
await writer.close();
finally {
} .releaseLock()
writer }
这段代码与我们用来覆盖现有内容的代码几乎相同(有关详细信息,请参阅上一节)。唯一的区别是我们添加了选项 .flags
:值 'a'
表示我们追加数据。其他可能的值(例如,如果文件尚不存在则抛出错误)在Node.js 文档中进行了解释。
注意:在某些函数中,此选项名为 .flag
,而在其他函数中则名为 .flags
。
遗憾的是,并非所有平台都使用相同的行终止符来标记行尾 (EOL)
'\r\n'
。'\n'
。为了以适用于所有平台的方式处理 EOL,我们可以使用几种策略。
读取文本时,最好识别两种 EOL。
将文本拆分为多行时,这会是什么样子?我们可以在末尾包含 EOL(任何一种格式)。如果我们修改这些行并将它们写入文件,这将使我们能够尽可能少地进行更改。
使用 EOL 处理行时,有时删除它们很有用 - 例如,通过以下函数
const RE_EOL_REMOVE = /\r?\n$/;
function removeEol(line) {
const match = RE_EOL_REMOVE.exec(line);
if (!match) return line;
return line.slice(0, match.index);
}
.equal(
assertremoveEol('Windows EOL\r\n'),
'Windows EOL'
;
).equal(
assertremoveEol('Unix EOL\n'),
'Unix EOL'
;
).equal(
assertremoveEol('No EOL'),
'No EOL'
; )
在写入行终止符时,我们有两个选择
'node:os'
中的常量 EOL
包含当前平台的 EOL。以下函数遍历目录并列出其所有后代(其子目录、子目录的子目录等)
import * as path from 'node:path';
function* traverseDirectory(dirPath) {
const dirEntries = fs.readdirSync(dirPath, {withFileTypes: true});
// Sort the entries to keep things more deterministic
.sort(
dirEntries, b) => a.name.localeCompare(b.name, 'en')
(a;
)for (const dirEntry of dirEntries) {
const fileName = dirEntry.name;
const pathName = path.join(dirPath, fileName);
yield pathName;
if (dirEntry.isDirectory()) {
yield* traverseDirectory(pathName);
}
} }
我们使用了此功能
fs.readdirSync(thePath, options?)
返回 thePath
处目录的子目录。.withFileTypes
为 true
,则该函数返回目录条目,即 fs.Dirent
的实例。它们具有以下属性:dirent.name
dirent.isDirectory()
dirent.isFile()
dirent.isSymbolicLink()
.withFileTypes
为 false
或缺失,则该函数返回带有文件名的字符串。以下代码显示了 traverseDirectory()
的实际应用
for (const filePath of traverseDirectory('dir')) {
console.log(filePath);
}
// Output:
// 'dir/dir-file.txt'
// 'dir/subdir'
// 'dir/subdir/subdir-file1.txt'
// 'dir/subdir/subdir-file2.csv'
mkdir
、mkdir -p
)我们可以使用以下函数来创建目录
.mkdirSync(thePath, options?): undefined | string fs
options.recursive
确定函数如何在 thePath
处创建目录
.recursive
缺失或为 false
,则 mkdirSync()
返回 undefined
,并且在以下情况下会抛出异常:thePath
处已存在目录(或文件)。thePath
的父目录不存在。.recursive
为 true
thePath
处已存在目录,则可以。thePath
的祖先目录。mkdirSync()
返回第一个新创建目录的路径。这是 mkdirSync()
的实际应用
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
];
).mkdirSync('dir/sub/subsub', {recursive: true});
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/sub',
'dir/sub/subsub',
]; )
函数 traverseDirectory(dirPath)
列出 dirPath
处目录的所有后代。
如果我们想按需设置嵌套文件结构,则在创建新文件时,我们不能总是确定祖先目录是否存在。那么以下函数会有所帮助
import * as path from 'node:path';
function ensureParentDirectory(filePath) {
const parentDir = path.dirname(filePath);
if (!fs.existsSync(parentDir)) {
.mkdirSync(parentDir, {recursive: true});
fs
} }
在这里,我们可以看到 ensureParentDirectory()
的实际应用(A 行)
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
];
)const filePath = 'dir/sub/subsub/new-file.txt';
ensureParentDirectory(filePath); // (A)
.writeFileSync(filePath, 'content', {encoding: 'utf-8'});
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/sub',
'dir/sub/subsub',
'dir/sub/subsub/new-file.txt',
]; )
fs.mkdtempSync(pathPrefix, options?)
创建一个临时目录:它将 6 个随机字符附加到 pathPrefix
,在新路径处创建一个目录并返回该路径。
pathPrefix
不应以大写字母“X”结尾,因为某些平台会将尾随的 X 替换为随机字符。
如果我们想在特定于操作系统的全局临时目录中创建临时目录,则可以使用函数 os.tmpdir()
import * as os from 'node:os';
import * as path from 'node:path';
const pathPrefix = path.resolve(os.tmpdir(), 'my-app');
// e.g. '/var/folders/ph/sz0384m11vxf/T/my-app'
const tmpPath = fs.mkdtempSync(pathPrefix);
// e.g. '/var/folders/ph/sz0384m11vxf/T/my-app1QXOXP'
重要的是要注意,临时目录不会在 Node.js 脚本终止时自动删除。我们必须自己删除它,或者依赖操作系统定期清理其全局临时目录(它可能会也可能不会这样做)。
fs.cpSync(srcPath, destPath, options?)
:将文件或目录从 srcPath
复制到 destPath
。有趣的选项:
.recursive
(默认值:false
):仅当此选项为 true
时才复制目录(包括空目录)。.force
(默认值:true
):如果为 true
,则覆盖现有文件。如果为 false
,则保留现有文件。.errorOnExist
设置为 true
会导致抛出错误。.filter
是一个函数,允许我们控制复制哪些文件。.preserveTimestamps
(默认值:false
):如果为 true
,则 destPath
中的副本将获得与 srcPath
中的原始文件相同的时间戳。这是该函数的实际应用
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir-orig',
'dir-orig/some-file.txt',
];
).cpSync('dir-orig', 'dir-copy', {recursive: true});
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir-copy',
'dir-copy/some-file.txt',
'dir-orig',
'dir-orig/some-file.txt',
]; )
函数 traverseDirectory(dirPath)
列出 dirPath
处目录的所有后代。
fs.renameSync(oldPath, newPath)
将文件或目录从 oldPath
重命名或移动到 newPath
。
让我们使用此函数重命名目录
.deepEqual(
assertArray.from(traverseDirectory('.')),
['old-dir-name',
'old-dir-name/some-file.txt',
];
).renameSync('old-dir-name', 'new-dir-name');
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['new-dir-name',
'new-dir-name/some-file.txt',
]; )
在这里,我们使用该函数移动文件
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/subdir',
'dir/subdir/some-file.txt',
];
).renameSync('dir/subdir/some-file.txt', 'some-file.txt');
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/subdir',
'some-file.txt',
]; )
函数 traverseDirectory(dirPath)
列出 dirPath
处目录的所有后代。
rm
、rm -r
)fs.rmSync(thePath, options?)
删除 thePath
处的文件或目录。有趣的选项:
.recursive
(默认值:false
):仅当此选项为 true
时才删除目录(包括空目录)。.force
(默认值:false
):如果为 false
,则如果 thePath
处没有文件或目录,则会抛出异常。让我们使用 fs.rmSync()
删除文件
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/some-file.txt',
];
).rmSync('dir/some-file.txt');
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
]; )
在这里,我们使用 fs.rmSync()
递归删除非空目录。
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/subdir',
'dir/subdir/some-file.txt',
];
).rmSync('dir/subdir', {recursive: true});
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
]; )
函数 traverseDirectory(dirPath)
列出 dirPath
处目录的所有后代。
rmdir
)fs.rmdirSync(thePath, options?)
删除空目录(如果目录不为空,则会抛出异常)。
以下代码显示了此函数的工作原理
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/subdir',
];
).rmdirSync('dir/subdir');
fs.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
]; )
函数 traverseDirectory(dirPath)
列出 dirPath
处目录的所有后代。
将输出保存到目录 dir
的脚本通常需要在启动之前清除 dir
:删除 dir
中的每个文件,使其为空。以下函数可以做到这一点。
import * as path from 'node:path';
function clearDirectory(dirPath) {
for (const fileName of fs.readdirSync(dirPath)) {
const pathName = path.join(dirPath, fileName);
.rmSync(pathName, {recursive: true});
fs
} }
我们使用了两个文件系统函数
fs.readdirSync(dirPath)
返回 dirPath
处目录的所有子目录的名称。这在§8.4.1 “遍历目录”中进行了说明。fs.rmSync(pathName, options?)
删除文件和目录(包括非空目录)。这在§8.6.1 “删除文件和任意目录(shell:rm
、rm -r
)”中进行了说明。这是使用 clearDirectory()
的示例
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/dir-file.txt',
'dir/subdir',
'dir/subdir/subdir-file.txt'
];
)clearDirectory('dir');
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
]; )
库 trash
将文件和文件夹移动到回收站。它适用于 macOS、Windows 和 Linux(在 Linux 上,支持有限,需要帮助)。这是其自述文件中的一个示例
import trash from 'trash';
await trash(['*.png', '!rainbow.png']);
trash()
接受字符串数组或字符串作为其第一个参数。任何字符串都可以是 glob 模式(带有星号和其他元字符)。
fs.existsSync(thePath)
如果 thePath
处存在文件或目录,则返回 true
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/some-file.txt',
];
).equal(
assert.existsSync('dir'), true
fs;
).equal(
assert.existsSync('dir/some-file.txt'), true
fs;
).equal(
assert.existsSync('dir/non-existent-file.txt'), false
fs; )
函数 traverseDirectory(dirPath)
列出 dirPath
处目录的所有后代。
fs.statSync(thePath, options?)
返回 fs.Stats
的实例,其中包含有关 thePath
处的文件或目录的信息。
有趣的 options
.throwIfNoEntry
(默认值:true
):如果 path
处没有实体,会发生什么情况?true
,则会抛出异常。false
,则返回 undefined
。.bigint
(默认值:false
):如果为 true
,则此函数对数值(例如时间戳,请参见下文)使用 bigint。fs.Stats
实例的属性
stats.isFile()
stats.isDirectory()
stats.isSymbolicLink()
stats.size
是以字节为单位的大小stats.atime
:上次访问时间stats.mtime
:上次修改时间stats.birthtime
:创建时间atime
stats.atime
:Date
的实例stats.atimeMS
:自 POSIX 纪元以来的毫秒数stats.atimeNs
:自 POSIX 纪元以来的纳秒数(需要选项 .bigint
)在以下示例中,我们使用 fs.statSync()
来实现函数 isDirectory()
function isDirectory(thePath) {
const stats = fs.statSync(thePath, {throwIfNoEntry: false});
return stats !== undefined && stats.isDirectory();
}
.deepEqual(
assertArray.from(traverseDirectory('.')),
['dir',
'dir/some-file.txt',
];
)
.equal(
assertisDirectory('dir'), true
;
).equal(
assertisDirectory('dir/some-file.txt'), false
;
).equal(
assertisDirectory('non-existent-dir'), false
; )
函数 traverseDirectory(dirPath)
列出 dirPath
处目录的所有后代。
让我们简要介绍一下用于更改文件属性的函数
fs.chmodSync(path, mode)
更改文件的权限。fs.chownSync(path, uid, gid)
更改文件的所有者和组。fs.utimesSync(path, atime, mtime)
更改文件的时间戳atime
:上次访问时间mtime
:上次修改时间用于处理硬链接的函数
fs.linkSync(existingPath, newPath)
创建硬链接。fs.unlinkSync(path)
删除硬链接以及它指向的文件(如果它是该文件的最后一个硬链接)。用于处理符号链接的函数
fs.symlinkSync(target, path, type?)
创建从 path
到 target
的符号链接。fs.readlinkSync(path, options?)
返回 path
处符号链接的目标。以下函数对符号链接进行操作,而不会取消引用它们(请注意名称前缀“l”)
fs.lchmodSync(path, mode)
更改 path
处符号链接的权限。fs.lchownSync(path, uid, gid)
更改 path
处符号链接的用户和组。fs.lutimesSync(path, atime, mtime)
更改 path
处符号链接的时间戳。fs.lstatSync(path, options?)
返回 path
处符号链接的状态(时间戳等)。其他实用函数
fs.realpathSync(path, options?)
通过解析点 (.
)、双点 (..
) 和符号链接来计算规范路径名。影响符号链接处理方式的函数选项
fs.cpSync(src, dest, options?)
:
.dereference
(默认值:false
):如果为 true
,则复制符号链接指向的文件,而不是符号链接本身。.verbatimSymlinks
(默认值:false
):如果为 false
,则复制的符号链接的目标将被更新,以便它仍然指向相同的位置。如果为 true
,则目标不会更改。