好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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 = 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了!

以上就是node实现shell命令管理工具及commander.js学习的详细内容,更多关于node shell命令管理的资料请关注其它相关文章!

查看更多关于node实现shell命令管理工具及commander.js学习的详细内容...

  阅读:31次