Node.js 实现 Shell 命令管理工具及 commander.js 学习
本文详细介绍如何使用 Node.js 开发一款 Shell 命令管理工具。通过集成 commander.js 处理命令解析,利用 inquirer.js 实现交互式问答,并借助 chalk.js 美化终端输出。文章涵盖全局命令注册、命令的增删改查及变量替换执行等核心功能,帮助开发者掌握命令行工具的开发流程。
开发背景
GitHub 仓库地址:https://github.com/lulu-up/record-shell
你是否曾遇到过忘记某个shell命令具体拼写,或者觉得输入一长串命令过于繁琐的情况?例如,我的mac笔记本电脑的tachbar偶尔会出现“卡死”现象,此时我需要输入 killall ControlStrip 命令来重启tachbar。正如你所见,这个命令确实很难打。
另外,在创建react项目时,我每次都需要输入npx create-react-app 项目名 --template typescript。在日常公司开发中,我习惯为每个新需求单独clone项目并创建新的分支进行开发。此时,我需要去gitlab上复制项目地址,然后在本地执行git clone xxxxxxxxxx 新的项目名。从理论上讲,这些操作显得非常重复且低效。
首先,本次教程将带你使用node一起动手制作一款记录shell命令的小插件。当然,网上已有类似的插件,但我这次开发了一个最简单直接的版本,旨在让自己使用更顺手,同时也借此机会复习一遍命令行相关知识。
一、用法演示
我们先来看看这个“库”是否真的方便:
1: 安装
npm install record-shell -g安装完成后,你的全局环境中会新增 rs 命令:

2: 添加
rs add命令名称可以随意起,甚至使用全中文会更舒适。这里先演示输入简单命令:

3: 查看 + 使用
rs ls
命令是可选的,这里我先多添加几个凑数的命令来演示:

你可以使用键盘上下键移动光标进行选择,按回车键即可执行命令:

当然,你也可以查看命令的详细信息,只需使用-a参数:
rs ls -a
4: 移除
rs rm

5: add 命令支持变量
我们的命令当然不会都是写“死”的模式。例如命令 echo 内容 > a.txt,这里的意思是将内容写入目标文件:

6: 使用变量
在执行命令时,系统会引导我们填入变量,因此在定义时使用中文即可:



二、初始化自己的 node 项目
接下来,我们将从零开始构建这个库。考虑到部分新手可能未开发过这种全局的node包,我将进行较为详细的讲解。
初始化项目非常简单,随意命名即可:
npm init改造package.json文件:
"bin": {
"rs": "./bin/www"
},这里在 bin 字段内指明,当运行 rs 命令时,访问"./bin/www"。

#! /usr/bin/env node
require('../src/index.js')#!这个符号通常出现在 Unix 系统脚本的第一行开头,用于指明该脚本文件的解释程序。/usr/bin/env因为大家可能会将node安装到不同的目录下,这里直接告诉系统可以在 PATH 目录中查找,从而兼容不同的node安装路径。node这个自不必多说,就是去查找咱们的node命令。
三、初始化命令 + 全局安装
这里讲解如何将我们的命令挂载到全局,使你可以在任何地方使用全局的rs命令:
// cd 我们的项目
npm install . -g这里比较好理解吧,相当于直接把项目安装在了全局。我们平时install xxx -g 是去远端拉取,而这个命令是拉取当前目录。
此时,向index.js文件内写入console.log('全局执行'),再全局执行 rs 并看到如下效果就是成功了:

四、commander.js (node 命令行解决方案)
先安装再聊:
npm install commandercommander 可以帮我们非常规范地处理用户的命令。例如,用户在命令行输入rs ls -a,在原生node的情况下,我可以先将输入的args进行拆解,拆解出 ls 与 -a,然后再写一堆if判断。如果是ls并且后面有-a则如何去做,但显然这样写不规范,代码也难以维护。commander 就是来帮我们规范这些写法的:
将下面的代码放进 index.js文件中:
const fs = require("fs");
const path = require("path");
const program = require('commander');
const packagePath = path.join(__dirname, "../package.json")
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
program.version(packageData.version)
program
.command('ls [-type]')
.description('description')
.action((value) => {
console.log('你输入的是:', value)
})
program.parse(process.argv)在命令行输入:
rs ls 123456
逐句解释一下代码:
const program = require('commander')这里很明显引入了commander。program.version(packageData.version)此处是定义了当前库的版本。当你输入rs -V时会展示program.version方法获取到的值,此处直接使用了package.json里面的version字段。program.command('ls')定义了名为ls的参数。当我们输入rs ls时才会触发我们后面的处理方法。我之所以写成program.command('ls [-type]')是因为加上[-type]后commander才会认为ls命令后面可以跟其他参数。当然你叫[xxxxx]也可以,让使用者能看懂即可。
.description('description')顾名思义这里是简介描述。当我们输入rs -h的时候会出现:

.action方法就是commander检测到当前命令触发时的处理函数。第一个参数是用户传入的参数,第二个参数是Command对象,后续我们会在这里弹出选择列表。process.argv这里要先知道process是node中的全局变量,其中argv是启动命令行时的所有参数。program.parse(process.argv)看完上面这里就好理解了,将命令行参数传递给commander开始执行。
番外
如果你配置program.option('ls', 'ls的介绍'),则当用户输入rs -h时会出现。但我感觉加了有点乱,咱们的插件追求简单所以就没加。
五、inquirer.js (node 命令行交互插件)
npm install inquirerinquirer 可以帮我们生成各种命令行问答功能,就像vue-cli差不多的效果。大家可以输入下面代码试一试“单选模式”:
program
.command('ls [-type]')
.description('description')
.action(async (value) => {
const answer = await inquirer.prompt([{
name: "key",
type: "rawlist",
message: "message1",
choices: [
{
name: 'name1',
value: 'value1'
},
{
name: 'name2',
value: 'value2'
}
]
}])
console.log(answer)
})

逐句解释一下代码:
- 首先这里是一个
async与awite的模式。 inquirer.prompt参数是一个数组,因为它可以连续操作,比如进行两次单选列表操作。name就是最终的key。比如name为xxxx用户选择了1,则最终返回结果就是{xxxx:1}。type指定交互类型rawlist单选列表、input输入、checkbox多选列表等。message就是提示语,我们让用户选择之前总要告诉他这里在做什么吧。choices选项的数组,name选项名,value选项值。
六、添加命令: add
正式开始做第一个命令。我新建了一个名为env的文件夹,里面创建record-list.json文件用来存储用户的命令:

add命令无非就是往record-list.json文件里面增加内容:
program
.command('add')
.description('添加命令')
.action(async () => {
const answer = await inquirer.prompt([{
name: "name",
type: "input",
message: "命令名称:",
validate: ((name) => {
if (name !== '') return true
})
}, {
name: "command",
type: "input",
message: "命令语句, 可采用[var]的形式传入变量:",
validate: ((command) => {
if (command !== '') return true
})
}])
let shellList = getShellList();
shellList = shellList.filter((item) => item.name !== answer.name);
shellList.push({
"name": answer.name,
"command": answer.command
})
fs.writeFileSync(dataPath, JSON.stringify(shellList));
})逐句解释一下代码:
- 首先我们使用
commander定义了add命令; - 当触发
add命令时,我们使用inquirer定义了两个输入框。第一个输入命令名称,第二个输入命令语句。 validate定义了对入参的校验。注意:用户不输入值不是undefined而是空字符串,所以使用了!== ''。如果校验不通过,无法继续操作。- 用户填写完毕后,向
record-list.json添加数据。同时,如果是重名的命令就进行替换。
名称可能会重复,但是不要紧,因为它的使用场景决定了它不需要做过多的限制。
七、移除命令: rm
这里的原理就是拉取record-list.json数据进行删减,然后更新record-list.json:
program
.command('rm')
.description('移除命令')
.action(async () => {
let shellList = getShellList();
const choices = shellList.map((item) => ({
key: item.name,
name: item.name,
value: item.name,
}));
const answer = await inquirer.prompt([{
name: "names",
type: "checkbox",
message: `请'选择'要删除的记录`,
choices,
validate: ((_choices) => {
if (_choices.length) return true
})
}])
shellList = shellList.filter((item) => {
return !answer.names.includes(item.name)
})
fs.writeFileSync(dataPath, JSON.stringify(shellList));
})逐句解释一下代码:
choices是定义了一组可选项。- 使用
checkbox多选模式,让用户可以一次删除多个命令。 validate校验了什么都不删的情况,因为可能用户忘了点击选取(空格键)。- 使用
filter过滤掉名称相同的命令。 - 最后更新
record-list.json文件。
八、查看+使用: ls
这里内容稍微多一点,毕竟一个命令负责两个能力。这里的核心原理是拉取record-list.json文件的内容展示成单选列表,然后根据用户选取的值进行命令的执行,最后返回执行结果;
1: 查看 ls,支持传参 -a
program
.command('ls')
.alias('l')
.description('命令列表')
.option('-a detailed')
.action(async (_, options) => {
const shellList = getShellList();
const choices = shellList.map(item => ({
key: item.name,
name: `${item.name}${options.detailed ? ': ' + item.command : ''}`,
value: item.command
}));
if (choices.length === 0) {
console.log(`
您当前没有录入命令, 可使用'rs add' 进行添加
`)
return
}
const answer = await inquirer.prompt([{
name: "key",
type: "rawlist",
message: "选择要执行的命令",
choices
}])
})逐句解释一下代码:
option('-a detailed')定义了可以接收-a参数,比如ls -a。并且如果用户传了-a则会得到返回值{detailed: true}。- 如果有
-a则将命令本身放在name属性里展示出来。 choices是转换了record-list.json文件里的数据的列表数据。- 如果
record-list.json数据是空的,则提示用户去使用rs add进行添加。 - 使用
inquirer生成单选列表。
2: 判断命令语句中是否有变量
由于允许用户输入的命令内带变量,比如前面演示过的 echo [内容] > [文件名],那我就要判断当前用户选中的命令内是否有变量:
const optionsReg = /\[.*?\]/g;
function getShellOptions(command) {
const arr = command.match(optionsReg) || [];
if (arr.length) {
return arr.map((message) => ({
name: message,
type: "input",
message,
}));
} else {
return []
}
}逐句解释一下代码:
optionsReg正则匹配出所有 '[这种写法]'的变量。- 如果匹配到了变量则返回一个数组,这个数组的长度是变量的个数,因为每个变量都要有一次输入的机会。
- 没有对重复的
name进行特殊处理,并且name会变成返回值的key,所以不可以重名。重名的话会导致只处理第一个变量。
3: 无变量 -> 执行
这里有一个新的概念:
const child_process = require('child_process');child_process可以生成node的“子进程”。child_process.exec方法是启动了一个系统 shell 来解析参数,因此可以是非常复杂的命令,包括管道和重定向。
child_process.exec(command, function (error, stdout) {
console.log(`${stdout}`)
if (error !== null) {
console.log('error: ' + error);
}
});逐句解释一下代码:
command是要执行的命令。stdout执行命令的输出,比如ls就是输出当前目录中的文件信息。error这里也很重要,如果报错了要让用户知道报错信息,所以也console了。
4: 有变量 -> 执行
核心原理是解析“变量”后对命令语句进行替换,然后正常执行就 ok:
function answerOptions2Command(command, answerMap) {
for (let key in answerMap) {
command = command.replace(`[${key}]`, answerMap[key])
}
return command;
}
function handleExec(command) {
child_process.exec(command, function (error, stdout) {
console.log(`${stdout}`)
if (error !== null) {
console.log('error: ' + error);
}
});
}
if (shellOptions.length) {
const answerMap = await inquirer.prompt(shellOptions)
const command = answerOptions2Command(answer.key, answerMap)
handleExec(command)
} else {
handleExec(answer.key)
}逐句解释一下代码:
inquirer执行完会返回一个字典,比如{[文本]:"xxxxx", [文件名]:"a.txt"}。因为我们设置了name与message使用同样的名称。answerOptions2Command循环执行replace进行变量的替换。handleExec负责执行语句。
九、让文字变色 (chalk)
功能都完成了,但是我们的提示文字还是“黑白的”。我们当然希望命令行中多姿多彩一些,在node中使用:
var red = "\033[31m red \033[0m";
console.log('你好红色:', red)
\033是c语言中的转义字符这里就不扩了,反正看到他就是要对屏幕进行操作了。但是我们可以看出上面的写法很不友好,肯定要封装一下下,chalk.js 就是个不错的已有轮子,我们下进行安装:
npm install chalk使用:
const chalk = require('chalk')
chalk.red('你好: 红色')你高兴太早了,现在是有问题的 !!

其他教程里都没说怎么解决,其实你只要把chalk的版本降低到4就 ok 了!
以上就是 node 实现 shell 命令管理工具及 commander.js 学习的详细内容,更多关于 node shell 命令管理的资料请关注本站其它相关文章!
Node.js 安装配置与第一个 HTTP 服务器编写
2026-11-05 11:41:40
怎么使用 nvm 管理并平滑升级 Node.js 版本
2026-11-04 23:15:07
Node.js 怎么使用 zlib 模块解压 gzip 和 deflate
2026-11-03 23:40:50
直接覆盖安装还是用版本管理器?Node.js 升级方式对比
2026-11-03 22:59:07
Express.js 是什么:Node.js Web 框架核心概念与适用场景
2026-11-03 20:00:12
