用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片
背景、
【用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.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 commander
???
commander
的可以帮我们非常规范的处理用户的命令, 比如用户在命令行输入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 = https://www.it610.com/article/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 inquirer
???
inquirer
可以帮我们生成各种命令行问答功能, 就像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
选项值。
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
文件。
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
生成单选列表。
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
, 所以不可以重名, 重名的话回会导致只处理第一个变量。
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
了。
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
负责执行语句。
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了!end
???? 这次就是这样, 希望与你一起进步。
推荐阅读
- 搭建个人知识付费应用系统-(5)Header、Footer|搭建个人知识付费应用系统-(5)Header、Footer 样式组件
- Jira使用浅谈篇一
- 硬件|【PCB专题】从嘉立创免费打样PCB流程
- Arduino+ESP32专题|【软件使用】新增了文件,但更新到Tortoise SVN里却没有增加
- 物流|【零售流通周报】 每日优鲜被强制执行127万元;今年全球电子商务将首次出现萎缩;考拉海购业务团队从400人收缩至不足20人;格力电器试图进军外卖
- 20180505
- 开发工具|win10 DockerDesktop使用K8s
- docker|mac安装docker-desktop,国内启用k8s
- 配置|Docker Desktop上启用k8s流程
- 运维|最新DockerDesktop下使用k8s