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 接受緩衝區時,它也會接受 Uint8Array。因此,由於 Uint8Arrays 是跨平台的,而緩衝區不是,因此前者較佳。
緩衝區可以執行 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 上使用跨平台的網路串流,而不是原生串流。說明請見 §10「在 Node.js 上使用網路串流」。
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 串流轉換成網頁串流(ReadableStream
的執行個體)。ChunksToLinesStream
在 §10.7.1「範例:將任意區塊串流轉換成行串流」 中說明。區塊 是串流產生的資料片段。如果我們有一個區塊為任意長度字串的串流,並將其透過 ChunksToLinesStream 傳遞,那麼我們就會取得一個區塊為行的串流。網頁串流是 非同步可疊代 的,這就是我們可以使用 for-await-of
迴圈疊代行的原因。
如果我們對文字行沒有興趣,那麼我們不需要 ChunksToLinesStream
,可以疊代 webReadableStream
並取得任意長度的區塊。
更多資訊
網頁串流在 §10「在 Node.js 上使用網頁串流」 中說明。
行終止符號在 §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 串流轉換為網頁串流(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 上執行(支援有限,需要協助)。這是其自述檔案中的範例
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 Epoch 以來的毫秒數stats.atimeNs
:自 POSIX Epoch 以來的奈秒數(需要選項 .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
,目標不會變更。