Node.js 构建命令行工具:实现 ls 命令的 -a 和 -l 选项

news/2024/11/13 12:48:40/文章来源:https://www.cnblogs.com/vigourice/p/18538586

在日常的前端开发中,我们常常借助各种基于 Node.js 的脚手架工具来加速项目搭建和维护,比如 create-react-app 可以一键初始化一个 React 项目,eslint 则帮助我们保持代码的整洁和一致。而在公司内部,为了更好地满足特定业务的需求,我们往往会构建自己的脚手架工具,如自定义的 React 或 Vue 框架、内部使用的代码检查工具等。本篇文章来和大家分享一下如何用 Node.js 实现一个简单的命令行工具,模仿常用的 ls 命令,包括其 -a-l 参数的功能。

ls 命令概览

首先,让我们快速回顾一下 ls 命令的一些基本用法。

  • ls:列出当前目录下所有的非隐藏文件。
  • ls -a:列出所有文件,包括以点(.)开头的隐藏文件,同时还会显示当前目录(.)和上级目录(..)。
  • ls -l:以长格式列出文件详情,包括文件类型、权限、链接数等。
  • ls -alls -a -l:结合 -a 和 -l 的功能,展示所有文件的详细信息。

简单来说,-a 参数用于显示隐藏文件和当前及上级目录,而 -l 参数则提供了更详细的文件信息。

如下图所示,当在初始化的新 React 项目目录中运行 ls 命令时,会看到如下情况:

ls -l 文件信息详解

当我们加上 -l 参数时,ls 命令会输出更多关于文件的信息:

1、文件类型:取第一个字符,d 代表目录,- 代表文件,l 代表链接。
2、用户操作权限:接下来的9个字符分为三组,分别表示文件所有者、所属组及其他用户的读、写、执行权限。
3、文件链接数:文件或目录的硬链接数。对于普通文件,这个数字通常是1。对于目录,这个数字至少为2,因为每个目录都包含两个特殊的目录 . 和 ..。
4、文件所有者:文件的所有者用户名,
5、文件所属组:文件所属的用户组名。
6、文件大小:文件的大小,以字节为单位。
7、最后修改时间:表示文件最后一次被修改的时间,格式为 月 日 时:分。
8、文件名:文件或目录的名称。

初始化项目

接下来,我们来实际动手实现一个类似的工具。首先,创建一个新的项目文件夹 ice-ls,并运行 npm init -y 来生成 package.json 文件。

然后,在项目根目录下创建一个 bin 文件夹,并在其中添加一个名为 index.js 的文件。这个文件是我们的命令行工具的入口点,文件头部添加 #!/usr/bin/env node 以便可以直接执行。

#!/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

解析参数

命令行工具的一大特点是支持多种参数来改变行为。在我们的例子中,我们需要处理 -a-l 参数。为此,可以在项目中创建一个 parseArgv.js 文件,用于解析命令行参数。

function parseArgv() {const argvList = process.argv.slice(2); // 忽略前两个默认参数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 函数,并根据解析结果来调整文件的输出方式。

#!/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; // 初始化文件系统块的总用量
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 文件同层级创建 getType.js 文件,用于判断文件类型是目录、文件还是链接。我们可以通过 fs 模块获取文件状态信息,其中 mode 属性包含了文件类型和权限的信息。通过与 fs 常量模块按位与来判断文件类型。

Node.js 文件系统模块 fs 中存在一些常量,其中和文件类型有关且常用的是以下三类:

  • S_IFDIR:用于检查一个文件是否是目录,数值为 0o040000(八进制)
  • S_IFREG:用于检查一个文件是否是普通文件,数值为 0o100000(八进制)
  • S_IFLNK:用于检查一个文件是否是符号链接,数值: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 系统中,文件权限分为三类:

  • 所有者(User):文件的拥有者。
  • 组(Group):文件所属的用户组。
  • 其他(Others):除所有者和组以外的其他用户。

每类权限又分为三种:

  • 读权限(Read, r):允许读取文件内容或列出目录内容。
  • 写权限(Write, w):允许修改文件内容或删除、重命名目录中的文件。
  • 执行权限(Execute, x):允许执行文件或进入目录。

其中和以上权限相关的 nodejs 变量为:

  • S_IRUSR:表示文件所有者的读权限(数值:0o400,十进制: 256)
  • S_IWUSR:文件所有者的写权限(数值:0o200,十进制:128)
  • S_IXUSR:文件所有者的执行权限(数值:0o100,十进制:64)
  • S_IRGRP:文件所属组的读权限(数值:0o040,十进制:32)
  • S_IWGRP:文件所属组的写权限(数值:0o020,十进制:16)
  • S_IXGRP:文件所属组的执行权限(数值:0o010,十进制:8)
  • S_IROTH:其他用户的读权限(数值:0o004,十进制:4)
  • S_IWOTH:其他用户的写权限(数值:0o002,十进制:2)
  • S_IXOTH:其他用户的执行权限(数值:0o001,十进制:1)

在 index.js 同层级创建 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";}
});

输出内容如下图所示:

处理文件链接数、总数、文件大小

LinuxUnix 系统中,通过命令行查看文件或目录的详细信息时,权限字符串后面的数字并不直接表示文件数量。例如,bin 文件夹下只有四个文件,但该数字显示为6。实际上,这个数字代表的是文件链接数,即有多少个硬链接指向该目录内的条目。

此外,ls -l 命令的第一行输出中的 total 值,并非指代文件总数,而是文件系统块的总用量。它反映了当前目录下所有文件及其子目录所占用的磁盘块数的总和。

为了方便理解和处理这些数据,我们可以使用 Node.jsfs.stat() 方法来获取文件的状态信息。

const { mode, size } = stat;// 获取文件链接数
const count = stat.nlink.toString().padStart(3, " ");// 获取文件大小
const fileSize = size.toString().padStart(5, " ");// 获取文件系统块的总用量
total += stat.blocks;output += `${type}${auth}${count}${fileName}`;

输出内容如下图所示:

获取用户信息

创建 getFileUser.js 文件,处理用户名称和组名称。虽然直接从文件状态(stat)对象中可以获取到用户ID(uid)和组ID(gid),但是要将这些ID转换成对应的名称需要一些转换工作。

获取用户名称相对简单,可以通过执行命令 id -un <uid> 来实现。而对于组名称的获取,则稍微复杂一些,我们需要先通过 id -G <uid> 命令获取与用户关联的所有组ID列表,然后再使用 id -Gn <uid> 获取这些组的名称列表。最后,通过查找 gid 在所有组ID列表中的位置,来确定组名称。

如下图所示,在我的系统中,uid 是 502,gid 是 20,用户名称是 xingchen,组名称是 staff。

代码实现:

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 中引入刚刚创建的 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}`;

最终输出效果如图所示:

获取修改时间

为了更好地展示文件信息中的时间部分,我们需要将原本的数字形式的时间转换为更易读的格式。这涉及到将月份从数字转换为缩写形式(如将1转换为"Jan"),同时确保日期、小时和分钟等字段在不足两位数时前面补零。

首先,我们在 config.js 文件中定义了一个对象来映射月份的数字与它们对应的英文缩写:

// 定义月份对应关系
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,
};

接下来创建 getFileTime.js 文件,用于从文件状态对象(stat)中提取并格式化修改时间:

function getFileTime(stat) {const { mtimeMs } = stat;const mTime = new Date(mtimeMs);const month = mTime.getMonth() + 1; // 获取月份,注意JavaScript中月份从0开始计数const date = mTime.getDate();// 不足2位在前一位补齐0const 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 中,我们引入了上述两个模块,并使用它们来处理和格式化时间数据:

const { getFileTime } = require("./getFileTime");
const { monthObj } = require("./config");
// ...其他代码...// 获取创建时间
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}${d}${t}${fileName}`;

通过上述步骤,我们成功地实现了对 -l 选项下显示的所有文件信息的功能,实现效果如图所示:

发布

在完成所有功能开发后,我们可以准备将项目发布到 npm 仓库,以便其他人也能使用这个工具。首先,需要移除本地的 npm 链接,这样可以确保发布的版本是最新的,不会受到本地开发环境的影响。执行以下命令即可移除本地链接:

npm unlink

执行该命令后,再次尝试运行 ice-ls 命令,系统将会提示找不到该命令,这是因为本地链接已被移除。接着,登录 npm 账户,使用以下命令进行登录:

npm login

登录后,就可以通过以下命令将包发布到 npm 仓库:

npm publish

实现效果如下图所示:

至此,我们已经成功实现了一个类似于Linux 系统的 ls 命令行工具,它支持 -a-l 选项,能够列出当前目录下的所有文件(包括隐藏文件)以及详细的文件信息。

如果你对前端工程化有兴趣,或者想了解更多相关的内容,欢迎查看我的其他文章,这些内容将持续更新,希望能给你带来更多的灵感和技术分享。

完整代码

以下是 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);// 获取文件链接数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, " ");// 获取创建时间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}${d}${t}${fileName}`;// 除了最后一个元素,都需要换行if (index !== files.length - 1) {output += "\n";}});
}if (!isList) {console.log(output);
} else {console.log(`total ${total}`);console.log(output);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/830839.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

法律行业内部知识库构建:重要性与实施步骤

这里是ai元启航,最近在学习ai相关知识,所以来分享一些这周学习的一些知识点,今天分享的是涉及法律行业的知识库搭建内容。 在当今信息爆炸的时代,法律行业面临着前所未有的挑战。法规的不断更新、案例的日益复杂以及客户需求的多样化,都要求法律从业者能够快速、准确地获取…

视野修炼第109期 | VSCode主题定制

① VS Code 主题定制生成 ② skellyCSS - 轻量级的CSS骨架屏方案 ③ npmpackage.info ④ create-vue 实现性集成 Oxlint ⑤ 英:从 URL 输入到页面渲染的过程 ⑥ 使用 JS 生成随机迷宫 ⑦ gounfaked - 免费的AI图片欢迎来到第 109 期的【视野修炼 - 技术周刊】,下面是本期的精…

网址封装(带苹果免签/安卓apk/苹果ipa)仿第八区H5APP封装打包分发系统源码

此封装系统可以无限封装,也可以给用户开后台让别人无限封装多个“苹果免签”文件 市面上一个苹果免签封装价格70到150rmb之间,自己拥有一个封装系统还是特别划算 分发网站功能如下: 1、苹果免签封装带绿标签名功能(可设置自己的域名显示) 免签封装时候任意网址/网页,不跳…

星际战甲 - 指挥官

001 || 青春无敌美少女头部数据衣服 002 || 御姐头部数据

2024.11.2(MyBatis)

MyBatis免除了几乎所有的jdbc代码以及设置参数和获取结果集的工作

黑马PM- B端产品-CRM产品模式

CRM行业概述CRM产品分类 部署方式行业匹配服务对象功能侧重

2024.11.10 鲜花

Triple 扩展Triple 扩展像神一样呐 愛のネタバレ 「別れ」っぽいな 人生のネタバレ 「死ぬ」っぽいな なにそれ意味深で かっこいいじゃん それっぽい単語集で踊ってんだ 失敬 とぅ とぅる とぅ とぅ とぅる “風” とぅ とぅる とぅ とぅ とぅる “風” とぅ とぅる とぅ とぅ…

Python decimal模块用法

decimal 模块:decimal意思为十进制,这个模块提供了十进制浮点运算支持1.可以传递给Decimal整型或者字符串参数,但不能是浮点数据,因为浮点数据本身就不准确。在Python中,将变量声明为 Decimal 类型通常用于需要高精度和小数运算的场合。Decimal 类型属于 decimal 模块,提…

使用Visual Studio Code 快速新建Net项目

前言最近,总是听大家说Visual Studio Code写后端代码非常好用,蓝后,就自己亲身体验了一下,还是很香的。正文1.首先需要安装Dotnet SDK,我这里安装的8.0版本,如下图:2.安装完DotNet SDK,就可以使用命令创建控制台应用了,如下图:3.新建的控制台应用如下图,有一丢丢的简…

20222402 2024-2025-1《网络与系统攻防技术》实验四实验报告

一、实验内容 本周学习内容 计算机病毒(Virus):通过感染文件(可执行文件、数据文件、电子邮件等)或磁盘引导扇区进行传播,一般需要宿主程序被执行或人为交互才能运行 蠕虫(Worm):一般为不需要宿主的单独文件,通过网络传播,自动复制通常无需人为交互便可感染传播 恶意移动代码…

GeoHash处理经纬度,降维,空间填充曲线

个人博客:无奈何杨(wnhyang) 个人语雀:wnhyang 共享语雀:在线知识共享 Github:wnhyang - Overview参考 https://segmentfault.com/a/1190000042971576 GeoHash原理以及代码实现_geohash编码-CSDN博客 GeoHash代码实现--java_geohash java代码示例-CSDN博客 在线经纬度距离…