开始下面操作前请确保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);