背景、
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 noderequire('../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 = 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
选项值。
六、增加命令: 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了!
end
这次就是这样, 心愿与你一起提高。