用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容

用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命令:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

2: 添加
rs add

???起名随意, 甚至全用汉语更舒服, 这里先演示输入简单命令:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

3: 查看 + 使用'
rs ls

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

??? 命令是可选择的, 这里我先多加几个凑所的命令用来演示:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

??? 可以按上下键移动选择, 回车即可执行命令:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

??? 当然也可以查看命令详情, 只需-a参数:
rs ls -a

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

4: 移除
rs rm

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

5: add有变量的命令 ??? 我们的命令当然不会都是写'死'的模式啦, 比如命令 echo 内容 > a.txt, 这里的意思是我要把内容写入目标文件:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

6: 使用变量 ??? 使用命令时会引导我们填入变量, 所以定义时写汉语就行:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

二、初始化自己的node项目 ??? 接下来一起从零开始做出这个库, 考虑到一些新手同学可能没做过这种全局的node包, 我这里就讲的详细一些。
???初始化项目没啥好说的, 随便起名:
npm init

??? 改造package.json文件:
"bin": { "rs": "./bin/www" },

??? 这里在 bin内指明, 当运行 rs 命令的时候, 访问"./bin/www"
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

#! /usr/bin/env node require('../src/index.js')

  1. #! 这个符号通常在Unix系统的基本中第一行开头中出现,用于指明这个脚本文件的解释程序。
  2. /usr/bin/env 因为可能大家会把node安装到不同的目录下, 这里直接告诉系统可以在PATH目录中查找, 这样就兼容了不同的node安装路径。
  3. node 这个自不必说, 就是去查找咱们的node命令。
三、初始化命令 + 全局安装 ??? 这里讲一下如何将我们的命令挂在到全局, 使你可以在任何地方都能使用全局的rs命令:
// cd 我们的项目npm install . -g

??? 这里比较好理解吧, 相当于直接把项目安装在了全局, 我们平时install xxx -g 是去远端拉取, 这个命令是拉当前目录。
???此时那你向index.js文件内写入console.log('全局执行'), 再全局执行 rs 并看到如下效果就是成功了:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

四、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

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

??? 逐句解释一下代码:
  1. const program = require('commander')这里很明显引入了commander
  2. program.version(packageData.version)此处是定义了当前的版本, 当你输入rs -V时会展示program.version方法获取到的值, 此处直接使用了package.json里面的version字段。
  3. program.command('ls') 定义了名为ls的参数, 当我们输入rs ls时才会触发我们后面的处理方法, 我之所以写成program.command('ls [-type]')是因为加上[-type]commander才会认为ls命令后面可以跟其他参数, 当然你叫[xxxxx]也可以, 让使用者能看懂即可。
  4. .description('description')顾名思义这里是简介描述, 当我们输入rs -h的时候会出现:
    用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
    文章图片
  5. .action方法就是commander检测到当前命令触发时的处理函数, 第一个参数是用户传入的参数, 第二个参数是Command对象, 后续我们会在这里弹出选择列表。
  6. process.argv这里要先知道processnode中的全局变量, 其中argv是启动命令行时的所有参数。
  7. 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) })

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

??? 逐句解释一下代码:
  1. 首先这里是一个asyncawite的模式。
  2. inquirer.prompt参数是一个数组, 因为它可以连续操作, 比如进行两次单选列表操作。
  3. name就是最终的key, 比如namexxxx用户选择了1, 则最终返回结果就是{xxxx:1}
  4. type指定交互类型rawlist单选列表、 input输入、checkbox多选列表等。
  5. message就是提示语, 我们让用户选择之前总要告诉他这里在做啥吧。
  6. choices选项的数组, name选项名, value选项值。
六、添加命令: add ??? 正式开始做第一个命令, 我新建了一个名为env的文件夹, 里面创建record-list.json文件用了存储用户的命令:
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

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

??? 逐句解释一下代码:
  1. 首先我们使用commander定义了add命令;
  2. 当触发add命令时我们使用inquirer定义了两个输入框, 第一个输入命令名称, 第二个输入命令语句。
  3. validate定义了对入参的校验, 注意: 用户不输入值不是undefined而是空字符串, 所以使用了 !== '', 如果校验不通过无法继续操作。
  4. 用户填写完毕就向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)); })

??? 逐句解释一下代码:
  1. choices是定义了一组可选项。
  2. 使用checkbox多选模式, 让用户可以一次删除多个命令。
  3. validate校验了什么都不删的情况, 因为可能使用户忘了点击选取(空格键)。
  4. 使用filter过滤掉名称相同的命令。
  5. 最后更新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 }]) })

??? 逐句解释一下代码:
  1. option('-a detailed')定义了可以接收-a参数, 比如ls -a, 并且如果用户传了-a则会得到返回值{detailed: true}
  2. 如果有-a则将命令本身放在name属性里展示出来。
  3. choices是转换了record-list.json文件里的数据的列表数据。
  4. 如果record-list.json数据是空的, 则提示用户去使用rs add进行添加。
  5. 使用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 [] } }

??? 逐句解释一下代码:
  1. optionsReg正则匹配出所有 '[这种写法]'的变量。
  2. 如果匹配到了变量则返回一个数组, 这个数组的长度是变量的个数, 因为每个变量都要有一次输入的机会。
  3. 没有对重复的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); } });

??? 逐句解释一下代码:
  1. command是要执行的命令。
  2. stdout执行命令的输出, 比如ls就是输出当前目录中的文件信息。
  3. 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) }

??? 逐句解释一下代码:
  1. inquirer执行完会返回一个字典, 比如{[文本]:"xxxxx", [文件名]:"a.txt"}, 因为我们设置了namemessage使用同样的名称。
  2. answerOptions2Command循环执行replace进行变量的替换。
  3. handleExec负责执行语句。
九、让文字变色 (chalk) ??? 功能都完成了, 但是我们的提示文字还是'黑白的', 我们当然希望命令行中多姿多彩一些, 在node中使用:
var red = "\033[31m red \033[0m"; console.log('你好红色:', red)

用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

??? \033c语言中的转义字符这里就不扩了, 反正看到他就是要对屏幕进行操作了, 但是我们可以看出上面的写法很不友好, 肯定要封装一下下, chalk.js就是个不错的已有轮子, 我们下进行安装:
npm install chalk

??? 使用:
const chalk = require('chalk') chalk.red('你好: 红色')

??? 你高兴太早了, 现在是有问题的 !!
用node从零做个管理shell命令的好用工具|用node从零做个管理shell命令的好用工具, 正好温习下commander.js相关内容
文章图片

???其他教程里都没说怎么解决, 其实那你只要把chalk的版本降低到4就ok了!
end
???? 这次就是这样, 希望与你一起进步。

    推荐阅读