本文带你了解创建一个Node-CLI工具所需知识点。
一、命令行参数解析
在NodeJS中可以通过以下代码获取命令行中传递的参数:
process.argv.slice(2)
但是这对于构建一个CLI工具远远不够,首先需要考虑参数输入的各种风格:
- Unix参数风格:前面加-,不过后面跟的是单个字符,例如-abc解析为[‘a’, ‘b’, ‘c’]。
- GNU参数风格:前面加–,例如npm中的命令,npm --save-dev webpack。
- BSD参数风格:前面不加修饰符。
这里可以通过正则表达式对process.argv进行加工:
/**
* 解析Unix、BSD和GNU参数风格
* @param {Array} argv 命令行参数数组
* @returns
*/
function parseArgv (argv) {
const max = argv.length
const result = {
_: []
}
for (let i = 0; i < max; i++) {
const arg = argv[i]
const next = argv[i + 1]
if (/^--.+/.test(arg)) {
// GNU风格
const key = arg.match(/^--(.+)/)[1]
if (next != null && !/^-.+/.test(next)) {
result[key] = next
i++
} else {
result[key] = true
}
} else if (/^-[^-]+/.test(arg)) {
// Unix风格
const items = arg.match(/^-([^-]+)/)[1].split('')
for (let j = 0, max = items.length; j < max; j++) {
const item = items[j]
// 非字母不解析
if (!/[a-zA-Z]/.test(item)) {
continue
}
if (next != null && !/^-.+/.test(next) && j === max - 1) {
result[item] = next
i++
} else {
result[item] = true
}
}
} else {
// BSD风格
result._.push(arg)
}
}
return result
}
通过以上的方法可以得到如下结果:
node example1.js --save-dev -age 20 some
// => 结果
{
_: ['some'],
'save-dev': true,
a: true,
g: true,
e: 20
}
上面这个示例不仅仅为了展示解析的结果,而且还强调了Unix参数风格只解析单个字母,所以这种风格的参数可能表达的意思不太明确并且数量有限,那么就需要在正确的场景中使用这种风格的参数:
npm --save-dev webpack
npm -D webpack
npm中采用Unix参数风格表示简写,这就是一种很恰当的方式,那么前面示例中的-age按照语义应该改为–age更加合理一点。
二、命令行界面
NodeJS中的readline模块提供question和prompt方法构建命令行界面,下面是一个简单的问答式的交互界面:
const readline = require('readline');
const question = ['请输入您的姓名', '请输入您的年龄']
const result = []
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: `?${question[0]} `
});
rl.prompt();
rl.on('line', (line) => {
result.push(line.trim())
const max = result.length
if (max === question.length) {
rl.close()
}
rl.setPrompt(`?${question[max]} `)
rl.prompt();
}).on('close', () => {
console.log(`谢谢参与问答 *** 姓名: ${result[0]} 年龄: ${result[1]}`);
process.exit(0);
});
当然交互界面的元素并不只有这一种,在使用各类CLI工具时,你应该会遇到诸如:单项选择、下载进度条…
下面可以尝试实现一个单项选择交互界面:
const readline = require('readline')
let selected = 0
const choices = ['javascript', 'css', 'html']
let lineCount = 0
const rl = readline.createInterface(process.stdin, process.stdout)
function reader () {
let str = ''
for (let i = 0; i < choices.length; i++) {
lineCount++
str += `${selected === i ? '[X]' : '[ ]'} ${choices[i]}\r\n`
}
process.stdout.write(str)
}
reader()
process.stdin.on('keypress', (s, key) => {
const name = key.name
const max = choices.length
if (name === 'up' && selected > 0) {
selected--
} else if (name === 'down' && selected < max - 1) {
selected++
} else if (name === 'down' && selected === max - 1) {
selected = 0
} else if (name === 'up' && selected === 0) {
selected = max - 1
} else {
return true
}
// 移动光标至起始位置,确保后续输入覆盖当前内容
readline.moveCursor(process.stdout, 0, -lineCount)
lineCount -= choices.length
reader()
})
rl.on('line', () => {
console.log(`you choose ${choices[selected]}`)
process.exit(0)
}).on('close', () => {
rl.close()
})
三、定制样式
为了有效的区别命令行界面中信息的差异性,我们可以为这里输出信息添加适当的样式。
这里介绍一下字符串添加样式的语法:
\x1b[背景颜色编号;字体颜色编号m
每条样式都要以[开头:
// \x1b[0m 清除样式
process.stdout.write('\x1b[44;37m OK \x1b[0m just do it\n')
四、自定义Node命令
接下来就是自定义Node命令,首先需要创建一个命令执行的文件:
// hello.js 首行需要指定脚本的解释程序
#!/usr/bin/env node
console.log('hello')
再利用package.json中的bin配置:
{
"bin": {
"hello": "./hello.js"
},
}
执行npm的link命令:
npm link
# 输入自定义命令
hello
# 输出 hello
五、总结
上面介绍了开发Node-CLI时所需要的一些基本知识,但是对于用过诸如webpack-cli、vue-cli工具的你可能会发现这些优秀的CLI工具还具有:
- git风格的子命令;
- 自动化的帮助信息;
- …
那么下面这些成熟的框架会给你很大的帮助: