使用Node.js實(shí)現(xiàn)一個(gè)簡(jiǎn)單的命令行工具
在日常的前端開(kāi)發(fā)中,我們常常借助各種基于 Node.js 的腳手架工具來(lái)加速項(xiàng)目搭建和維護(hù),比如 create-react-app
可以一鍵初始化一個(gè) React
項(xiàng)目,eslint
則幫助我們保持代碼的整潔和一致。而在公司內(nèi)部,為了更好地滿(mǎn)足特定業(yè)務(wù)的需求,我們往往會(huì)構(gòu)建自己的腳手架工具,如自定義的 React 或 Vue 框架、內(nèi)部使用的代碼檢查工具等。本篇文章來(lái)和大家分享一下如何用 Node.js
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的命令行工具,模仿常用的 ls
命令,包括其 -a
和 -l
參數(shù)的功能。
ls 命令概覽
首先,讓我們快速回顧一下 ls
命令的一些基本用法。
ls
:列出當(dāng)前目錄下所有的非隱藏文件。ls -a
:列出所有文件,包括以點(diǎn)(.)開(kāi)頭的隱藏文件,同時(shí)還會(huì)顯示當(dāng)前目錄(.)和上級(jí)目錄(..)。ls -l
:以長(zhǎng)格式列出文件詳情,包括文件類(lèi)型、權(quán)限、鏈接數(shù)等。ls -al
或ls -a -l
:結(jié)合 -a 和 -l 的功能,展示所有文件的詳細(xì)信息。
簡(jiǎn)單來(lái)說(shuō),-a
參數(shù)用于顯示隱藏文件和當(dāng)前及上級(jí)目錄,而 -l
參數(shù)則提供了更詳細(xì)的文件信息。
如下圖所示,當(dāng)在初始化的新 React 項(xiàng)目目錄中運(yùn)行 ls
命令時(shí),會(huì)看到如下情況:
ls -l 文件信息詳解
當(dāng)我們加上 -l 參數(shù)時(shí),ls 命令會(huì)輸出更多關(guān)于文件的信息:
1、文件類(lèi)型:取第一個(gè)字符,d 代表目錄,- 代表文件,l 代表鏈接。
2、用戶(hù)操作權(quán)限:接下來(lái)的9個(gè)字符分為三組,分別表示文件所有者、所屬組及其他用戶(hù)的讀、寫(xiě)、執(zhí)行權(quán)限。
3、文件鏈接數(shù):文件或目錄的硬鏈接數(shù)。對(duì)于普通文件,這個(gè)數(shù)字通常是1。對(duì)于目錄,這個(gè)數(shù)字至少為2,因?yàn)槊總€(gè)目錄都包含兩個(gè)特殊的目錄 . 和 ..。
4、文件所有者:文件的所有者用戶(hù)名,
5、文件所屬組:文件所屬的用戶(hù)組名。
6、文件大小:文件的大小,以字節(jié)為單位。
7、最后修改時(shí)間:表示文件最后一次被修改的時(shí)間,格式為 月 日 時(shí):分。
8、文件名:文件或目錄的名稱(chēng)。
初始化項(xiàng)目
接下來(lái),我們來(lái)實(shí)際動(dòng)手實(shí)現(xiàn)一個(gè)類(lèi)似的工具。首先,創(chuàng)建一個(gè)新的項(xiàng)目文件夾 ice-ls
,并運(yùn)行 npm init -y
來(lái)生成 package.json
文件。
然后,在項(xiàng)目根目錄下創(chuàng)建一個(gè) bin
文件夾,并在其中添加一個(gè)名為 index.js
的文件。這個(gè)文件是我們的命令行工具的入口點(diǎn),文件頭部添加 #!/usr/bin/env node
以便可以直接執(zhí)行。
#!/usr/bin/env node console.log('hello nodejs')
可以通過(guò) ./bin/index.js
命令來(lái)測(cè)試這段代碼是否正常工作,會(huì)看到 "hello nodejs" 的輸出。
為了讓我們的工具更加易于使用,在 package.json
中配置 bin
字段,這樣通過(guò)一個(gè)簡(jiǎn)短的名字就可以調(diào)用。
bin: { "ice-ls": "./bin/index.js" }
為了在本地可以調(diào)試,使用 npm link
命令將項(xiàng)目鏈接到全局 node_modules
目錄中,這樣就能像使用其他全局命令一樣使用 ice-ls
。
解析參數(shù)
命令行工具的一大特點(diǎn)是支持多種參數(shù)來(lái)改變行為。在我們的例子中,我們需要處理 -a
和 -l
參數(shù)。為此,可以在項(xiàng)目中創(chuàng)建一個(gè) parseArgv.js
文件,用于解析命令行參數(shù)。
function parseArgv() { const argvList = process.argv.slice(2); // 忽略前兩個(gè)默認(rèn)參數(shù) let isAll = false; let isList = false; argvList.forEach((item) => { if (item.includes("a")) { isAll = true; } if (item.includes("l")) { isList = true; } }); return { isAll, isList, }; } module.exports = { parseArgv, };
接著,我們需要在 bin/index.js
文件中引入 parseArgv
函數(shù),并根據(jù)解析結(jié)果來(lái)調(diào)整文件的輸出方式。
#!/usr/bin/env node const fs = require("fs"); const { parseArgv } = require("./parseArgv"); const dir = process.cwd(); // 獲取當(dāng)前工作目錄 let files = fs.readdirSync(dir); // 讀取目錄內(nèi)容 let output = ""; const { isAll, isList } = parseArgv(); if (isAll) { files = [".", ".."].concat(files); // 添加 . 和 .. } else { files = files.filter((item) => item.indexOf(".") !== 0); // 過(guò)濾掉隱藏文件 } let total = 0; // 初始化文件系統(tǒng)塊的總用量 if (!isList) { files.forEach((file) => { output += `${file} `; }); } else { files.forEach((file, index) => { output += file; if (index !== files.length - 1) { output += "\n"; // 如果不是最后一個(gè)元素,則換行 } }); } if (!isList) { console.log(output); } else { console.log(`total ${total}`); console.log(output); }
輸出內(nèi)容如下圖所示:
處理文件類(lèi)型及權(quán)限
在 index.js 文件同層級(jí)創(chuàng)建 getType.js
文件,用于判斷文件類(lèi)型是目錄、文件還是鏈接。我們可以通過(guò) fs
模塊獲取文件狀態(tài)信息,其中 mode
屬性包含了文件類(lèi)型和權(quán)限的信息。通過(guò)與 fs
常量模塊按位與來(lái)判斷文件類(lèi)型。
Node.js
文件系統(tǒng)模塊 fs
中存在一些常量,其中和文件類(lèi)型有關(guān)且常用的是以下三類(lèi):
S_IFDIR
:用于檢查一個(gè)文件是否是目錄,數(shù)值為 0o040000(八進(jìn)制)S_IFREG
:用于檢查一個(gè)文件是否是普通文件,數(shù)值為 0o100000(八進(jìn)制)S_IFLNK
:用于檢查一個(gè)文件是否是符號(hào)鏈接,數(shù)值:0o120000(八進(jìn)制)
const fs = require("fs"); function getFileType(mode) { const S_IFDIR = fs.constants.S_IFDIR; const S_IFREG = fs.constants.S_IFREG; const S_IFLINK = fs.constants.S_IFLINK; if (mode & S_IFDIR) return "d"; if (mode & S_IFREG) return "-"; if (mode & S_IFLINK) return "l"; return '?'; // 若無(wú)法識(shí)別,則返回問(wèn)號(hào) } module.exports = { getFileType, };
在 Unix 系統(tǒng)中,文件權(quán)限分為三類(lèi):
- 所有者(User):文件的擁有者。
- 組(Group):文件所屬的用戶(hù)組。
- 其他(Others):除所有者和組以外的其他用戶(hù)。
每類(lèi)權(quán)限又分為三種:
- 讀權(quán)限(Read, r):允許讀取文件內(nèi)容或列出目錄內(nèi)容。
- 寫(xiě)權(quán)限(Write, w):允許修改文件內(nèi)容或刪除、重命名目錄中的文件。
- 執(zhí)行權(quán)限(Execute, x):允許執(zhí)行文件或進(jìn)入目錄。
其中和以上權(quán)限相關(guān)的 nodejs 變量為:
S_IRUSR
:表示文件所有者的讀權(quán)限(數(shù)值:0o400,十進(jìn)制: 256)S_IWUSR
:文件所有者的寫(xiě)權(quán)限(數(shù)值:0o200,十進(jìn)制:128)S_IXUSR
:文件所有者的執(zhí)行權(quán)限(數(shù)值:0o100,十進(jìn)制:64)S_IRGRP
:文件所屬組的讀權(quán)限(數(shù)值:0o040,十進(jìn)制:32)S_IWGRP
:文件所屬組的寫(xiě)權(quán)限(數(shù)值:0o020,十進(jìn)制:16)S_IXGRP
:文件所屬組的執(zhí)行權(quán)限(數(shù)值:0o010,十進(jìn)制:8)S_IROTH
:其他用戶(hù)的讀權(quán)限(數(shù)值:0o004,十進(jìn)制:4)S_IWOTH
:其他用戶(hù)的寫(xiě)權(quán)限(數(shù)值:0o002,十進(jìn)制:2)S_IXOTH
:其他用戶(hù)的執(zhí)行權(quán)限(數(shù)值:0o001,十進(jìn)制:1)
在 index.js 同層級(jí)創(chuàng)建 getAuth.js 文件來(lái)處理文件權(quán)限信息:
const fs = require("fs"); function getAuth(mode) { const S_IRUSR = mode & fs.constants.S_IRUSR ? "r" : "-"; const S_IWUSR = mode & fs.constants.S_IWUSR ? "w" : "-"; const S_IXUSR = mode & fs.constants.S_IXUSR ? "x" : "-"; const S_IRGRP = mode & fs.constants.S_IRGRP ? "r" : "-"; const S_IWGRP = mode & fs.constants.S_IWGRP ? "w" : "-"; const S_IXGRP = mode & fs.constants.S_IXGRP ? "x" : "-"; const S_IROTH = mode & fs.constants.S_IROTH ? "r" : "-"; const S_IWOTH = mode & fs.constants.S_IWOTH ? "w" : "-"; const S_IXOTH = mode & fs.constants.S_IXOTH ? "x" : "-"; return ( S_IRUSR + S_IWUSR + S_IXUSR + S_IRGRP + S_IWGRP + S_IXGRP + S_IROTH + S_IWOTH + S_IXOTH ); } module.exports = { getAuth, };
在 bin/index.js 文件中引入這兩個(gè)模塊,并使用它們來(lái)豐富文件信息的輸出。
const path = require("path"); const { getAuth } = require("./getAuth"); const { getFileType } = require("./getFileType"); files.forEach((file, index) => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); const { mode } = stat; // 獲取權(quán)限 const type = getFileType(mode); const auth = getAuth(mode); // 獲取文件名,增加空格 const fileName = ` ${file}`; output += `${type}${auth}${fileName}`; // 除了最后一個(gè)元素,都需要換行 if (index !== files.length - 1) { output += "\n"; } });
輸出內(nèi)容如下圖所示:
處理文件鏈接數(shù)、總數(shù)、文件大小
在 Linux
或 Unix
系統(tǒng)中,通過(guò)命令行查看文件或目錄的詳細(xì)信息時(shí),權(quán)限字符串后面的數(shù)字并不直接表示文件數(shù)量。例如,bin
文件夾下只有四個(gè)文件,但該數(shù)字顯示為6。實(shí)際上,這個(gè)數(shù)字代表的是文件鏈接數(shù),即有多少個(gè)硬鏈接指向該目錄內(nèi)的條目。
此外,ls -l
命令的第一行輸出中的 total
值,并非指代文件總數(shù),而是文件系統(tǒng)塊的總用量。它反映了當(dāng)前目錄下所有文件及其子目錄所占用的磁盤(pán)塊數(shù)的總和。
為了方便理解和處理這些數(shù)據(jù),我們可以使用 Node.js
的 fs.stat()
方法來(lái)獲取文件的狀態(tài)信息。
const { mode, size } = stat; // 獲取文件鏈接數(shù) const count = stat.nlink.toString().padStart(3, " "); // 獲取文件大小 const fileSize = size.toString().padStart(5, " "); // 獲取文件系統(tǒng)塊的總用量 total += stat.blocks; output += `${type}${auth}${count}${fileName}`;
輸出內(nèi)容如下圖所示:
獲取用戶(hù)信息
創(chuàng)建 getFileUser.js
文件,處理用戶(hù)名稱(chēng)和組名稱(chēng)。雖然直接從文件狀態(tài)(stat
)對(duì)象中可以獲取到用戶(hù)ID(uid
)和組ID(gid
),但是要將這些ID轉(zhuǎn)換成對(duì)應(yīng)的名稱(chēng)需要一些轉(zhuǎn)換工作。
獲取用戶(hù)名稱(chēng)相對(duì)簡(jiǎn)單,可以通過(guò)執(zhí)行命令 id -un <uid>
來(lái)實(shí)現(xiàn)。而對(duì)于組名稱(chēng)的獲取,則稍微復(fù)雜一些,我們需要先通過(guò) id -G <uid>
命令獲取與用戶(hù)關(guān)聯(lián)的所有組ID列表,然后再使用 id -Gn <uid>
獲取這些組的名稱(chēng)列表。最后,通過(guò)查找 gid 在所有組ID列表中的位置,來(lái)確定組名稱(chēng)。
如下圖所示,在我的系統(tǒng)中,uid 是 502,gid 是 20,用戶(hù)名稱(chēng)是 xingchen,組名稱(chēng)是 staff。
代碼實(shí)現(xiàn):
const { execSync } = require("child_process"); function getFileUser(stat) { const { uid, gid } = stat; // 獲取用戶(hù)名 const username = execSync("id -un " + uid) .toString() .trim(); // 獲取組名列表及對(duì)應(yīng)關(guān)系 const groupIds = execSync("id -G " + uid) .toString() .trim() .split(" "); const groupIdsName = execSync("id -Gn " + uid) .toString() .trim() .split(" "); const index = groupIds.findIndex((id) => +id === +gid); const groupName = groupIdsName[index]; return { username, groupName, }; } module.exports = { getFileUser, };
在項(xiàng)目的主入口文件 index.js
中引入剛剛創(chuàng)建的 getFileUser
模塊,并調(diào)用它來(lái)獲取文件的用戶(hù)信息。
const { getFileUser } = require("./getFileUser");
再調(diào)整一下輸出的內(nèi)容
// 獲取用戶(hù)名 const { username, groupName } = getFileUser(stat); const u = username.padStart(9, " "); const g = groupName.padStart(7, " "); output += `${type}${auth}${count}${u}${g}${fileSize}${fileName}`;
最終輸出效果如圖所示:
獲取修改時(shí)間
為了更好地展示文件信息中的時(shí)間部分,我們需要將原本的數(shù)字形式的時(shí)間轉(zhuǎn)換為更易讀的格式。這涉及到將月份從數(shù)字轉(zhuǎn)換為縮寫(xiě)形式(如將1轉(zhuǎn)換為"Jan"),同時(shí)確保日期、小時(shí)和分鐘等字段在不足兩位數(shù)時(shí)前面補(bǔ)零。
首先,我們?cè)?nbsp;config.js
文件中定義了一個(gè)對(duì)象來(lái)映射月份的數(shù)字與它們對(duì)應(yīng)的英文縮寫(xiě):
// 定義月份對(duì)應(yīng)關(guān)系 const monthObj = { 1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr", 5: "May", 6: "Jun", 7: "Jul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Nov", 12: "Dec", }; module.exports = { monthObj, };
接下來(lái)創(chuàng)建 getFileTime.js
文件,用于從文件狀態(tài)對(duì)象(stat
)中提取并格式化修改時(shí)間:
function getFileTime(stat) { const { mtimeMs } = stat; const mTime = new Date(mtimeMs); const month = mTime.getMonth() + 1; // 獲取月份,注意JavaScript中月份從0開(kāi)始計(jì)數(shù) const date = mTime.getDate(); // 不足2位在前一位補(bǔ)齊0 const hour = mTime.getHours().toString().padStart(2, 0); const minute = mTime.getMinutes().toString().padStart(2, 0); return { month, date, hour, minute, }; } module.exports = { getFileTime, };
在主文件 index.js
中,我們引入了上述兩個(gè)模塊,并使用它們來(lái)處理和格式化時(shí)間數(shù)據(jù):
const { getFileTime } = require("./getFileTime"); const { monthObj } = require("./config"); // ...其他代碼... // 獲取創(chuàng)建時(shí)間 const { month, date, hour, minute } = getFileTime(stat); const m = monthObj[month].toString().padStart(4, " "); const d = date.toString().padStart(3, " "); const t = ` ${hour}:${minute}`; output += `${type}${auth}${count}${u}${g}${fileSize}${m}$vvxyksv9kd${t}${fileName}`;
通過(guò)上述步驟,我們成功地實(shí)現(xiàn)了對(duì) -l
選項(xiàng)下顯示的所有文件信息的功能,實(shí)現(xiàn)效果如圖所示:
發(fā)布
在完成所有功能開(kāi)發(fā)后,我們可以準(zhǔn)備將項(xiàng)目發(fā)布到 npm
倉(cāng)庫(kù),以便其他人也能使用這個(gè)工具。首先,需要移除本地的 npm
鏈接,這樣可以確保發(fā)布的版本是最新的,不會(huì)受到本地開(kāi)發(fā)環(huán)境的影響。執(zhí)行以下命令即可移除本地鏈接:
npm unlink
執(zhí)行該命令后,再次嘗試運(yùn)行 ice-ls
命令,系統(tǒng)將會(huì)提示找不到該命令,這是因?yàn)楸镜劓溄右驯灰瞥?。接著,登?nbsp;npm
賬戶(hù),使用以下命令進(jìn)行登錄:
npm login
登錄后,就可以通過(guò)以下命令將包發(fā)布到 npm
倉(cāng)庫(kù):
npm publish
實(shí)現(xiàn)效果如下圖所示:
至此,我們已經(jīng)成功實(shí)現(xiàn)了一個(gè)類(lèi)似于Linux
系統(tǒng)的 ls
命令行工具,它支持 -a
和-l
選項(xiàng),能夠列出當(dāng)前目錄下的所有文件(包括隱藏文件)以及詳細(xì)的文件信息。
如果你對(duì)前端工程化有興趣,或者想了解更多相關(guān)的內(nèi)容,歡迎查看我的其他文章,這些內(nèi)容將持續(xù)更新,希望能給你帶來(lái)更多的靈感和技術(shù)分享。
完整代碼
以下是 index.js 的完整代碼,其他文件的完整代碼均已在上面分析過(guò)程中貼出。
#!/usr/bin/env node const fs = require("fs"); const path = require("path"); const { parseArgv } = require("./parseArgv"); const { getAuth } = require("./getAuth"); const { getFileType } = require("./getFileType"); const { getFileUser } = require("./getFileUser"); const { getFileTime } = require("./getFileTime"); const { monthObj } = require("./config"); const dir = process.cwd(); let files = fs.readdirSync(dir); let output = ""; const { isAll, isList } = parseArgv(); if (isAll) { files = [".", ".."].concat(files); } else { files = files.filter((item) => item.indexOf(".") !== 0); } let total = 0; if (!isList) { files.forEach((file) => { output += `${file} `; }); } else { files.forEach((file, index) => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); const { mode, size } = stat; // 獲取權(quán)限 const type = getFileType(mode); const auth = getAuth(mode); // 獲取文件鏈接數(shù) const count = stat.nlink.toString().padStart(3, " "); // 獲取用戶(hù)名 const { username, groupName } = getFileUser(stat); const u = username.padStart(9, " "); const g = groupName.padStart(7, " "); // 獲取文件大小 const fileSize = size.toString().padStart(5, " "); // 獲取創(chuàng)建時(shí)間 const { month, date, hour, minute } = getFileTime(stat); const m = monthObj[month].toString().padStart(4, " "); const d = date.toString().padStart(3, " "); const t = ` ${hour}:${minute}`; // 獲取文件名 const fileName = ` ${file}`; total += stat.blocks; output += `${type}${auth}${count}${u}${g}${fileSize}${m}$vvxyksv9kd${t}${fileName}`; // 除了最后一個(gè)元素,都需要換行 if (index !== files.length - 1) { output += "\n"; } }); } if (!isList) { console.log(output); } else { console.log(`total ${total}`); console.log(output); }
到此這篇關(guān)于使用Node.js實(shí)現(xiàn)一個(gè)簡(jiǎn)單的命令行工具的文章就介紹到這了,更多相關(guān)Node.js命令行工具內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
node前端模板引擎Jade之標(biāo)簽的基本寫(xiě)法
這篇文章主要介紹了node前端模板引擎Jade之標(biāo)簽的基本寫(xiě)法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05利用pm2部署多個(gè)node.js項(xiàng)目的配置教程
目前似乎最常見(jiàn)的線(xiàn)上部署nodejs項(xiàng)目的有forever,pm2這兩種,而下面這篇文章主要給大家介紹了關(guān)于利用pm2部署多個(gè)node.js項(xiàng)目的配置教程,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-10-10- 這篇文章主要介紹了node中Stream的詳細(xì)介紹,流是一個(gè)數(shù)據(jù)傳輸手段,是端到端信息交換的一種方式,而且是有順序的,是逐塊讀取數(shù)據(jù)、處理內(nèi)容,用于順序讀取輸入或?qū)懭胼敵?/div> 2022-09-09
NestJS核心概念之Middleware中間件創(chuàng)建使用示例
這篇文章主要為大家介紹了NestJS核心概念之Middleware中間件創(chuàng)建使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08輕松創(chuàng)建nodejs服務(wù)器(2):nodejs服務(wù)器的構(gòu)成分析
這篇文章主要介紹了輕松創(chuàng)建nodejs服務(wù)器(2):nodejs服務(wù)器的構(gòu)成分析,本文是對(duì)第一節(jié)中簡(jiǎn)單服務(wù)器的代碼進(jìn)行分析總結(jié),需要的朋友可以參考下2014-12-12Node.js如何快速導(dǎo)出多表頭的excel文件實(shí)現(xiàn)方法
這篇文章主要為大家介紹了Node.js如何快速導(dǎo)出多表頭的excel文件實(shí)現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06nodejs子進(jìn)程child_process和cluster模塊深入解析
本文從node的單線(xiàn)程單進(jìn)程的理解觸發(fā),介紹了child_process模塊和cluster模塊,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09nodejs連接mysql數(shù)據(jù)庫(kù)簡(jiǎn)單封裝示例-mysql模塊
本篇文章主要介紹了nodejs連接mysql數(shù)據(jù)庫(kù)簡(jiǎn)單封裝(mysql模塊),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04最新評(píng)論