关于前端:用node从零做个管理shell命令的好用工具-正好温习下commanderjs相关内容

23次阅读

共计 7281 个字符,预计需要花费 19 分钟才能阅读完成。

背景、

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')
  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 并看到如下成果就是胜利了:

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

    逐句解释一下代码:

  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 的时候会呈现:
  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)
    })

    逐句解释一下代码:

  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 文件用了存储用户的命令:

    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)

    \033 c 语言 中的 转义字符 这里就不扩了, 反正看到他就是要对屏幕进行操作了, 然而咱们能够看出下面的写法很不敌对, 必定要封装一下下, chalk.js就是个不错的已有轮子, 咱们下进行装置:

npm install chalk

    应用:

const chalk = require('chalk') 

chalk.red('你好: 红色')

    你快乐太早了, 当初是有问题的 !!

   其余教程里都没说怎么解决, 其实那你只有把 chalk 的版本升高到 4 就 ok 了!

end

     这次就是这样, 心愿与你一起提高。

正文完
 0