在日常的前端开发中,我们常常借助各种基于 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位在前一位补齐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 中,我们引入了上述两个模块,并使用它们来处理和格式化时间数据:

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);
}

Node.js 构建命令行工具:实现 ls 命令的 -a 和 -l 选项的更多相关文章

  1. svn使用规范、在Windows下使用svn命令行工具、svn命令行的解释

    以前在公司一直使用git,现在公司有用svn,一时间还真的不知道如何下手,在网上搜寻了很多大神和官网文档的指导,总结了下面一份教程,希望能够帮助大家快速上手,如果想更细致的了解相关内容,可以点击每个小 ...

  2. Linux命令行工具之vmstat命令

    原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11484608.html vmstat是一款指定采样周期和次数的功能性监测工具,可以使用它监控进程上下文 ...

  3. Linux命令行工具之pidstat命令

    原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11484624.html pidstat命令就可以帮助我们监测到具体线程的上下文切换 通过pidstat ...

  4. Linux命令行工具之free命令

    原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11524691.html 使用 free 查看整个系统的内存使用情况 Note:不同版本的free输出可 ...

  5. 从零开始打造个人专属命令行工具集——yargs完全指南

    前言 使用命令行程序对程序员来说很常见,就算是前端工程师或者开发gui的,也需要使用命令行来编译程序或者打包程序 熟练使用命令行工具能极大的提高开发效率,linux自带的命令行工具都非常的有用,但是这 ...

  6. 【Mac】Mac OS X 安装GNU命令行工具

    macos的很多用户都是做it相关的人,类unix系统带来了很多方面,尤其是经常和linux打交道的人. 但是作为经常使用linux 命令行的人发现macos中的命令行工具很多都是bsd工具,跟lin ...

  7. 命令行工具--LLDP

    目录 命令行工具--LLDP 一.场景引入 二.什么是LLDP? 三.在CentOS上安装LLDP 四.命令详解 五.脚本 命令行工具--LLDP 一.场景引入 有的时候,我们需要知道服务器上联交换机 ...

  8. 7z命令行工具

    7z (中文)是优秀开源的压缩解压缩软件(wiki: en  中文),有windows版本与linux版本,最新的9.32版本支持的格式包括: 压缩与解压缩均支持:7z, XZ, BZIP2, GZI ...

  9. Mysql 命令行工具

    1.Mysql命令行工具分为两类:服务端命令行工具和客户端命令行工具. 2.服务端工具 mysql_install_db:建库工具 mysqld_safe:Mysql服务的启动工具,mysqld_sa ...

  10. dedecms:织梦文章如何添加“自定义属性”标签(sql命令行工具)

    dede织梦如何添加“自定义属性”标签“症状” 1.进入后台——系统——SQL命令行工具——运行SQL命令行,添加arcatt表字段: insert into`dede_arcatt`(sortid, ...

随机推荐

  1. 【原创】vagrant up 异常报错,出现 There was an error while executing `VBoxManage` 的解决方法

    最近在使用 vagrant homestead 时,不小心在虚拟机上使用了 exit 命令退出虚拟机,导致再使用 vagrant up 时出现以下错误: Bringing machine 'larav ...

  2. 关于封装axios报错Cyclic dependency的问题

    在npm start的时候直接报错Cyclic dependency循环依赖 JS 循环依赖 (require cycle)_腾飞日记-CSDN博客_js循环依赖 可以看上面的博文了解一下这个错大概在 ...

  3. 【Jmeter】之批量处理多接口压力测试

    一.需求前提 1.有以下三个步骤: ①创建单据 ②审核单据 ③确认单据 让三个相关接口进行一连串批量请求操作,直到所有批量数据确认单据成功. 二.测试计划 需要说明的是,因为每个接口可能处理的不太一样 ...

  4. airflow 学习

    入门 Get started developing workflows with Apache Airflow Getting started with Apache Airflow  

  5. 6.24.2 数据库&漏洞口令&应急取证

    windows日志分析神器 logonTracer-外内网日志 github下载:#JPCERTCC/LogonTracer:通过可视化和分析 Windows 事件日志来调查恶意 Windows 登录 ...

  6. ST-SSL: 用于交通流量预测的时空自监督学习《Spatio-Temporal Self-Supervised Learning for Traffic Flow Prediction》(交通流量预测、时空异质性、自监督、数据增强)

    2023年10月23日,继续论文,好困,想发疯. 论文:Spatio-Temporal Self-Supervised Learning for Traffic Flow Prediction Git ...

  7. TypeScript 高级教程 – 把 TypeScript 当编程语言使用 (第二篇)

    前言 上一篇, 我们提到, TypeScript 进阶有 3 个阶段. 第一阶段是 "把 TypeScript 当强类型语言使用", 我们已经介绍完了. 第二阶段是 "把 ...

  8. SpringMVC——SSM整合——表现层数据封装

    表现层数据封装 设置统一数据返回结果类 注意:Result类中的字段并不是固定的,可以根据需要自行增减提供若干个构造方法,方便操作 返回结果类 package com.cqupt.controller ...

  9. 还在苦于密码太弱?教你3招用Linux生成高强度密码

    各位好啊,我是会编程的蜗牛,作为java开发者,我们平常肯定会接触Linux操作系统,其实除了一般的部署应用外,它还可以帮助我们生成密码.解决我们平常自己想各种复杂密码的烦恼,以后我会讲一讲如何安全地 ...

  10. java基础 -网络编程笔记

    666,InetAddress package com.hspedu.api; import java.net.InetAddress; import java.net.UnknownHostExce ...