使用Node.js實現(xiàn)一個簡單的命令行工具
在日常的前端開發(fā)中,我們常常借助各種基于 Node.js 的腳手架工具來加速項目搭建和維護,比如 create-react-app 可以一鍵初始化一個 React 項目,eslint 則幫助我們保持代碼的整潔和一致。而在公司內部,為了更好地滿足特定業(yè)務的需求,我們往往會構建自己的腳手架工具,如自定義的 React 或 Vue 框架、內部使用的代碼檢查工具等。本篇文章來和大家分享一下如何用 Node.js 實現(xiàn)一個簡單的命令行工具,模仿常用的 ls 命令,包括其 -a 和 -l 參數(shù)的功能。
ls 命令概覽
首先,讓我們快速回顧一下 ls 命令的一些基本用法。
ls:列出當前目錄下所有的非隱藏文件。ls -a:列出所有文件,包括以點(.)開頭的隱藏文件,同時還會顯示當前目錄(.)和上級目錄(..)。ls -l:以長格式列出文件詳情,包括文件類型、權限、鏈接數(shù)等。ls -al或ls -a -l:結合 -a 和 -l 的功能,展示所有文件的詳細信息。
簡單來說,-a 參數(shù)用于顯示隱藏文件和當前及上級目錄,而 -l 參數(shù)則提供了更詳細的文件信息。
如下圖所示,當在初始化的新 React 項目目錄中運行 ls 命令時,會看到如下情況:

ls -l 文件信息詳解
當我們加上 -l 參數(shù)時,ls 命令會輸出更多關于文件的信息:
1、文件類型:取第一個字符,d 代表目錄,- 代表文件,l 代表鏈接。
2、用戶操作權限:接下來的9個字符分為三組,分別表示文件所有者、所屬組及其他用戶的讀、寫、執(zhí)行權限。
3、文件鏈接數(shù):文件或目錄的硬鏈接數(shù)。對于普通文件,這個數(shù)字通常是1。對于目錄,這個數(shù)字至少為2,因為每個目錄都包含兩個特殊的目錄 . 和 ..。
4、文件所有者:文件的所有者用戶名,
5、文件所屬組:文件所屬的用戶組名。
6、文件大小:文件的大小,以字節(jié)為單位。
7、最后修改時間:表示文件最后一次被修改的時間,格式為 月 日 時:分。
8、文件名:文件或目錄的名稱。

初始化項目
接下來,我們來實際動手實現(xiàn)一個類似的工具。首先,創(chuàng)建一個新的項目文件夾 ice-ls,并運行 npm init -y 來生成 package.json 文件。
然后,在項目根目錄下創(chuàng)建一個 bin 文件夾,并在其中添加一個名為 index.js 的文件。這個文件是我們的命令行工具的入口點,文件頭部添加 #!/usr/bin/env node 以便可以直接執(zhí)行。
#!/usr/bin/env node
console.log('hello nodejs')
可以通過 ./bin/index.js 命令來測試這段代碼是否正常工作,會看到 "hello nodejs" 的輸出。

為了讓我們的工具更加易于使用,在 package.json 中配置 bin 字段,這樣通過一個簡短的名字就可以調用。
bin: {
"ice-ls": "./bin/index.js"
}
為了在本地可以調試,使用 npm link 命令將項目鏈接到全局 node_modules 目錄中,這樣就能像使用其他全局命令一樣使用 ice-ls 。

解析參數(shù)
命令行工具的一大特點是支持多種參數(shù)來改變行為。在我們的例子中,我們需要處理 -a 和 -l 參數(shù)。為此,可以在項目中創(chuàng)建一個 parseArgv.js 文件,用于解析命令行參數(shù)。
function parseArgv() {
const argvList = process.argv.slice(2); // 忽略前兩個默認參數(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ù)解析結果來調整文件的輸出方式。
#!/usr/bin/env node
const fs = require("fs");
const { parseArgv } = require("./parseArgv");
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; // 初始化文件系統(tǒng)塊的總用量
if (!isList) {
files.forEach((file) => {
output += `${file} `;
});
} else {
files.forEach((file, index) => {
output += file;
if (index !== files.length - 1) {
output += "\n"; // 如果不是最后一個元素,則換行
}
});
}
if (!isList) {
console.log(output);
} else {
console.log(`total ${total}`);
console.log(output);
}
輸出內容如下圖所示:

處理文件類型及權限
在 index.js 文件同層級創(chuàng)建 getType.js 文件,用于判斷文件類型是目錄、文件還是鏈接。我們可以通過 fs 模塊獲取文件狀態(tài)信息,其中 mode 屬性包含了文件類型和權限的信息。通過與 fs 常量模塊按位與來判斷文件類型。
Node.js 文件系統(tǒng)模塊 fs 中存在一些常量,其中和文件類型有關且常用的是以下三類:
S_IFDIR:用于檢查一個文件是否是目錄,數(shù)值為 0o040000(八進制)S_IFREG:用于檢查一個文件是否是普通文件,數(shù)值為 0o100000(八進制)S_IFLNK:用于檢查一個文件是否是符號鏈接,數(shù)值:0o120000(八進制)
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 '?'; // 若無法識別,則返回問號
}
module.exports = {
getFileType,
};
在 Unix 系統(tǒng)中,文件權限分為三類:
- 所有者(User):文件的擁有者。
- 組(Group):文件所屬的用戶組。
- 其他(Others):除所有者和組以外的其他用戶。
每類權限又分為三種:
- 讀權限(Read, r):允許讀取文件內容或列出目錄內容。
- 寫權限(Write, w):允許修改文件內容或刪除、重命名目錄中的文件。
- 執(zhí)行權限(Execute, x):允許執(zhí)行文件或進入目錄。
其中和以上權限相關的 nodejs 變量為:
S_IRUSR:表示文件所有者的讀權限(數(shù)值:0o400,十進制: 256)S_IWUSR:文件所有者的寫權限(數(shù)值:0o200,十進制:128)S_IXUSR:文件所有者的執(zhí)行權限(數(shù)值:0o100,十進制:64)S_IRGRP:文件所屬組的讀權限(數(shù)值:0o040,十進制:32)S_IWGRP:文件所屬組的寫權限(數(shù)值:0o020,十進制:16)S_IXGRP:文件所屬組的執(zhí)行權限(數(shù)值:0o010,十進制:8)S_IROTH:其他用戶的讀權限(數(shù)值:0o004,十進制:4)S_IWOTH:其他用戶的寫權限(數(shù)值:0o002,十進制:2)S_IXOTH:其他用戶的執(zhí)行權限(數(shù)值:0o001,十進制:1)
在 index.js 同層級創(chuàng)建 getAuth.js 文件來處理文件權限信息:
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 文件中引入這兩個模塊,并使用它們來豐富文件信息的輸出。
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;
// 獲取權限
const type = getFileType(mode);
const auth = getAuth(mode);
// 獲取文件名,增加空格
const fileName = ` ${file}`;
output += `${type}${auth}${fileName}`;
// 除了最后一個元素,都需要換行
if (index !== files.length - 1) {
output += "\n";
}
});
輸出內容如下圖所示:

處理文件鏈接數(shù)、總數(shù)、文件大小
在 Linux 或 Unix 系統(tǒng)中,通過命令行查看文件或目錄的詳細信息時,權限字符串后面的數(shù)字并不直接表示文件數(shù)量。例如,bin 文件夾下只有四個文件,但該數(shù)字顯示為6。實際上,這個數(shù)字代表的是文件鏈接數(shù),即有多少個硬鏈接指向該目錄內的條目。
此外,ls -l 命令的第一行輸出中的 total 值,并非指代文件總數(shù),而是文件系統(tǒng)塊的總用量。它反映了當前目錄下所有文件及其子目錄所占用的磁盤塊數(shù)的總和。
為了方便理解和處理這些數(shù)據(jù),我們可以使用 Node.js 的 fs.stat() 方法來獲取文件的狀態(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}`;
輸出內容如下圖所示:

獲取用戶信息
創(chuàng)建 getFileUser.js 文件,處理用戶名稱和組名稱。雖然直接從文件狀態(tài)(stat)對象中可以獲取到用戶ID(uid)和組ID(gid),但是要將這些ID轉換成對應的名稱需要一些轉換工作。
獲取用戶名稱相對簡單,可以通過執(zhí)行命令 id -un <uid> 來實現(xiàn)。而對于組名稱的獲取,則稍微復雜一些,我們需要先通過 id -G <uid> 命令獲取與用戶關聯(lián)的所有組ID列表,然后再使用 id -Gn <uid> 獲取這些組的名稱列表。最后,通過查找 gid 在所有組ID列表中的位置,來確定組名稱。
如下圖所示,在我的系統(tǒng)中,uid 是 502,gid 是 20,用戶名稱是 xingchen,組名稱是 staff。

代碼實現(xiàn):
const { execSync } = require("child_process");
function getFileUser(stat) {
const { uid, gid } = stat;
// 獲取用戶名
const username = execSync("id -un " + uid)
.toString()
.trim();
// 獲取組名列表及對應關系
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,
};
在項目的主入口文件 index.js 中引入剛剛創(chuàng)建的 getFileUser 模塊,并調用它來獲取文件的用戶信息。
const { getFileUser } = require("./getFileUser");
再調整一下輸出的內容
// 獲取用戶名
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ù)字轉換為縮寫形式(如將1轉換為"Jan"),同時確保日期、小時和分鐘等字段在不足兩位數(shù)時前面補零。
首先,我們在 config.js 文件中定義了一個對象來映射月份的數(shù)字與它們對應的英文縮寫:
// 定義月份對應關系
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,
};
接下來創(chuàng)建 getFileTime.js 文件,用于從文件狀態(tài)對象(stat)中提取并格式化修改時間:
function getFileTime(stat) {
const { mtimeMs } = stat;
const mTime = new Date(mtimeMs);
const month = mTime.getMonth() + 1; // 獲取月份,注意JavaScript中月份從0開始計數(shù)
const date = mTime.getDate();
// 不足2位在前一位補齊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 中,我們引入了上述兩個模塊,并使用它們來處理和格式化時間數(shù)據(jù):
const { getFileTime } = require("./getFileTime");
const { monthObj } = require("./config");
// ...其他代碼...
// 獲取創(chuàng)建時間
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}`;
通過上述步驟,我們成功地實現(xiàn)了對 -l 選項下顯示的所有文件信息的功能,實現(xiàn)效果如圖所示:

發(fā)布
在完成所有功能開發(fā)后,我們可以準備將項目發(fā)布到 npm 倉庫,以便其他人也能使用這個工具。首先,需要移除本地的 npm 鏈接,這樣可以確保發(fā)布的版本是最新的,不會受到本地開發(fā)環(huán)境的影響。執(zhí)行以下命令即可移除本地鏈接:
npm unlink
執(zhí)行該命令后,再次嘗試運行 ice-ls 命令,系統(tǒng)將會提示找不到該命令,這是因為本地鏈接已被移除。接著,登錄 npm 賬戶,使用以下命令進行登錄:
npm login
登錄后,就可以通過以下命令將包發(fā)布到 npm 倉庫:
npm publish
實現(xiàn)效果如下圖所示:

至此,我們已經成功實現(xiàn)了一個類似于Linux 系統(tǒng)的 ls 命令行工具,它支持 -a 和-l 選項,能夠列出當前目錄下的所有文件(包括隱藏文件)以及詳細的文件信息。
如果你對前端工程化有興趣,或者想了解更多相關的內容,歡迎查看我的其他文章,這些內容將持續(xù)更新,希望能給你帶來更多的靈感和技術分享。
完整代碼
以下是 index.js 的完整代碼,其他文件的完整代碼均已在上面分析過程中貼出。
#!/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;
// 獲取權限
const type = getFileType(mode);
const auth = getAuth(mode);
// 獲取文件鏈接數(shù)
const count = stat.nlink.toString().padStart(3, " ");
// 獲取用戶名
const { username, groupName } = getFileUser(stat);
const u = username.padStart(9, " ");
const g = groupName.padStart(7, " ");
// 獲取文件大小
const fileSize = size.toString().padStart(5, " ");
// 獲取創(chuàng)建時間
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}`;
// 除了最后一個元素,都需要換行
if (index !== files.length - 1) {
output += "\n";
}
});
}
if (!isList) {
console.log(output);
} else {
console.log(`total ${total}`);
console.log(output);
}到此這篇關于使用Node.js實現(xiàn)一個簡單的命令行工具的文章就介紹到這了,更多相關Node.js命令行工具內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
NestJS核心概念之Middleware中間件創(chuàng)建使用示例
這篇文章主要為大家介紹了NestJS核心概念之Middleware中間件創(chuàng)建使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08
輕松創(chuàng)建nodejs服務器(2):nodejs服務器的構成分析
這篇文章主要介紹了輕松創(chuàng)建nodejs服務器(2):nodejs服務器的構成分析,本文是對第一節(jié)中簡單服務器的代碼進行分析總結,需要的朋友可以參考下2014-12-12
Node.js如何快速導出多表頭的excel文件實現(xiàn)方法
這篇文章主要為大家介紹了Node.js如何快速導出多表頭的excel文件實現(xiàn)方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06
nodejs子進程child_process和cluster模塊深入解析
本文從node的單線程單進程的理解觸發(fā),介紹了child_process模塊和cluster模塊,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09
nodejs連接mysql數(shù)據(jù)庫簡單封裝示例-mysql模塊
本篇文章主要介紹了nodejs連接mysql數(shù)據(jù)庫簡單封裝(mysql模塊),具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04

