从零开始的命令行CLI

开始下面操作前请确保node环境已经正确安装,并配置了全局环境变量,代码在windows系统下编写未做系统适配。

初始化项目

npm init -y 初始化package.json

配置 package.json

新增"bin"字段,设置快捷命令和入口文件

{
  "name": "fn-cli",
  "version": "1.0.0",
  "description": "",
  "bin": {
    "fn": "./bin/index.js",
    "fn-cli": "./bin/index2.js",
  },
  // "bin": "/bin/index.js", 以name为key
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

bin可以直接写入口文件的地址,此时快捷命令的名称为name字段的值

bin设置为对象的形式,可以设置多个快捷命令。

新增入口文件

这里配置的bin的入口文件是bin/index.js 所以直接在根目录下新增bin文件夹并在目录下新增index.js

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Nf3MgLM-1646989638186)(https://secure2.wostatic.cn/static/qFkxqbeAjWewueo9cGrS7h/image.png)]

为了执行命令时,可以直接运行对应的js文件脚本,在index.js 入门文件头部添加:

#!/usr/bin/env node 指定该文件是使用系统环境的node环境执行该脚本

添加内容:

#!/usr/bin/env node
console.log('TEST')

配置命令软链接

在根目录下执行 npm link

没有错误日志输出,并且提示 up to date 就表示建立命令软连接成功

直接控制台输入:对应的命令名称就可以执行,输入fn-cli 就可以看到结果TEST输出

一个最简单的CLI就成功了。

在这里插入图片描述

如果你修改了 bin字段的,需要重新建立软链接,建议到node根目录下删除之前建立的软链接文件

在这里插入图片描述

扩展CLI

此时的cli只能简单的执行但一个命令,实现功能单一无法实现丰富的功能。

vue-cli 可以使用 vue create demo 来新建一个项目, vue -V 可以查看当前cli 版本等等

命令参数解析

当前的CLI不管我们在fn-cli后面加上一些什么参数,都是执行同样的操作。

要让CLI具备支持多命令多功能,第一步就是拿到用户输入的参数

修改一下index.js

#!/usr/bin/env node
console.log('fn-cli', process.argv);

执行命令:

可以看出process.argv 是一个数组类型变量,默认长度为2,分别为node环境执行文件位置和当前命令行入口文件位置。从第3个参数就是用户输入的命名

在这里插入图片描述

当我们在命令后面在跟上一下其他参数是可以发现,process.argv 解析参数是以空格为分割的。
在这里插入图片描述

定义CLI规范

目前知道了CLI如何去解析参数,那么要扩展CLI ,首先得定义规范:

process.argv 的第三个参数为:命令名称,从第四个参数开始为:命令的参数

命名参数:

支持—debug=true —name=demo12 来指定参数的键值。

默认命名后面的普通字符串为默认参数值

#!/usr/bin/env node
const argvs = process.argv.slice(2);
// 规范:
// 第三个参数为命名名称,从第四参数开始为命令的参数 (这里的位置以 process.argv为基准)
const command = argvs[0];
const params = argvs.slice(1);

console.log('fn-cli', command, params);

在这里插入图片描述

开始编写命令

统一参数

在上一步中,可以发现参数解析出来是字符串,存在—name=demo12 这种字符形式代表参数键值对,这里需要首先对这种参数类型进行解析,统一成特定的参数类型。

下面为:参数解析方法这个defaultValue参与覆盖操作,普通字符靠后覆盖前面字符串作为默认值;

/**
 * @description: 对参数键值对进行解析,普通字符串作为defaultValue
 * @param {*}
 * @return {*}
 */
const paramsParse = () => {
    const config = params.reduce((pre, val) => {
        const reg = /^\-\-/gm;
        if (val.indexOf('=') === -1) return {
            ...pre,
            defaultValue: val
        };
        if (reg.test(val)) {
            const param = val.replace(reg, '').split('=');
        } else {
            pre[param[0]] = param[1];
        }
        pre[param[0]] = param[1];
        return pre;
    }, {})
    return config;
}
第一个命令

为了方便扩展命令,这里先建立 comand文件夹 并新增一个index.js 作为 command整合入口文件。

bin/command/index.js

const requireContext = require('node-require-context')
const commands = requireContext('./', false, /\.js$/);
const commanders = {};
commands.keys().forEach(moduleId => {
    const moduleName = moduleId.replace(/(\.\/|\.js)/g, '').split('command')[1].slice(1);
    if(moduleName === 'index') return;
    const module = require(`./${moduleName}`);
    commanders[moduleName] = module;
})

module.exports = commanders;

bin/command/version.js

const path = require('path');
const pkgPath = path.join(__dirname, '../../package.json');
const pkg = require(pkgPath);
function version() {
    console.log(pkg.version);
}
module.exports = version;

bin/``command/create.js

const path = require('path');
const fs = require('fs');

function create(params) {
    const fileNameReg = /^.+\.\w{1,}/;
    const name = params.name || params.defaultValue;
    const dirPath = path.join(process.cwd(), name);
    if(!fileNameReg.test(name)) {
        if (fs.existsSync(dirPath) === false) {
            fs.mkdirSync(dirPath);
            console.log(`${name}: 文件夹创建成功!`,)
        } else {
            console.log(`${name}: 文件夹已存在!`,)
        }
    } else {
        try {
            fs.readFileSync(dirPath, 'utf-8');
            console.log(`${name}: 文件已存在!`,)
        }catch(e) {
            fs.writeFileSync(dirPath, '', 'utf8');
            console.log(`${name}: 文件创建成功!`,)
        }
    }
}

module.exports = create;

bin/index.js

#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const commanders = require('./command/index');
const argvs = process.argv.slice(2);
// 规范:
// 第三个参数为命名名称,从第四参数开始为命令的参数 (这里的位置以 process.argv为基准)
const command = argvs[0];
const params = argvs.slice(1);

const paramsParse = () => {
    const config = params.reduce((pre, val) => {
        const reg = /^\-\-/gm;
        if (val.indexOf('=') === -1) return {
            ...pre,
            defaultValue: val
        };
        if (reg.test(val)) {
            const param = val.replace(reg, '').split('=');
        } else {
            const param = val.split('=');
        }
        pre[param[0]] = param[1];
        return pre;
    }, {})
    return config;
}

if (!command) {
    const commanderKeys = Object.keys(commanders).join('、');
    console.log(`Usage: fn <command> [options] \n command support: ${commanderKeys}`);
} else {
    const commanderParams = paramsParse(params)
    if (!commanders[command]) {
        console.log('不支持当前命令');
    } else {
        commanders[command](commanderParams);
    }
}

Yargs快速建立CLI

使用yargs 可以快速建立一个cli的基本框架:自动帮你解析参数和命令

#!/usr/bin/env node
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const pkg = require('../package.json');
const args = hideBin(process.argv);
const argv = process.argv.slice(2);
const context = {
    version: pkg.version,
}
const cli = yargs()
cli
    .usage('Usage: practice-cli [command] <options>') // 用法
    .demandCommand(1, 'A command is required.Pass --help to see all avaiable commands') // 无command提示
    .recommendCommands()
    .fail((err, msg) => {
        console.log('err', err, msg);
    })
    .strict() // 严格模式
    .alias('h', 'help')
    .alias('V', 'version') // 别名
    .wrap(cli.terminalWidth()) // 实现文字两侧顶格显示
    .epilogue('Pratice CLI')
    .options({
        debug: {
            type: 'boolean',
            describe: 'Bootstrap debug mode',
            alias: 'd',
        }
    })
    .group(['debug'], 'Dev Options')
    .command('init [name]', 'Do init a project', (yargs) => {
        yargs.option('name', {
            type: 'string',
            describe: 'Name of project',
            alias: 'n'
        })
    }, (argv) => {
        console.log('argv', argv);
    })
    .parse(argv, context);