开发 Node.js 命令行程序的一些常用工程实践
Posted sheldon
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开发 Node.js 命令行程序的一些常用工程实践相关的知识,希望对你有一定的参考价值。
介绍
node开发命令行程序非常方便,我们常用的webpack,babel,http-server,express-generator, yeoman等等,都是node.js开发出来的命令行工具。
原理
Node.js 的程序一般通过 node 指令进行运行。而在 Unix Like 的系统中,任何一种脚本也可以被系统当做可执行程序执行。只需要按照如下约定,设置脚本的运行时程序即可,例如对于 Node.js 脚本来说,只需要设置他的运行程序是node:
#!/usr/bin/env node
console.log(\'hello world\');
这样,该文件就可以直接在 Linux 系统中输入脚本名进行运行了。Linux 会自动使用 node 来运行该脚本。
假设该脚本文件名为 test, 此时便可在 shell 中直接执行 ./test
, 而不需要使用 node ./test
. (实际上他们是相等的)。
npm 可以自动生成这种 bash 文件
在 npm 包中,有一种通过 -g
全局安装的包,实际上就是将 npm 包内的 bin 入口 js 自动创建了 /usr/local/bin
下面一个软连接,连接到了npm安装的该全局包所在位置。如果你也希望在 /usr/local 下出现你的可执行文件的话,只需要在 npm 包中将我们预先写好的入口js文件设置为package.json
的 bin 字段即可:
"main": "./lib/echo.js", # 入口模块位置
"bin" : {
"node-echo": "./bin/node-echo" # 命令行程序名和主模块位置
}
其中 node-echo
是一个 javascript 编写的Node.js代码文件,里面会按照 Unix 格式在顶部表明它需要被 node 来运行。也正因如此,node-echo
我们就不写扩展名 .js
了。
然后入口文件一般放置在package包目录根目录中的 bin
目录下,如 bin/node-echo
这样写:
#! /usr/bin/env node
require(\'../src/index.js\'); // 也可以写任何你喜欢的逻辑
此时,使用 npm i -g
来安装这个包时, npm则会自动在 /usr/local/bin
下面生成 node-echo
可执行文件,它是一个软连接,会链接到:
yourNpmPrefix/node_modules/yourPackageName/bin/node-echo
命令行程序常用操作
开发命令行程序,有一些基本的套路,比如一般:
- 输入命令本身
xx
或xx -h
来展示程序的基本用法(包括usage, options列表) - 通过输入
-n sheldon
或--name sheldon
的方式获取用户输入的参数配置option - 通过
-r
或--recusive
的方式来打开或关闭某个布尔类型的配置option - 通过在最后加value参数来获取用户输入的操作对象,如
cp -r aFolder bFolder
中的 aFolder bFolder就是value参数。 - 通过
xx yy
的方式来支持子命令(yy是xx的子命令) - 更高阶一点,遇到某些命令后可以交互式的获取用户输入的一些参数配置,如询问用户要初始化的模板名字是什么等等
对于options参数,要记得:如果用户输入 -abc
,commander 会当做 -a -b -c
来解析。所以如果你希望的是 abc
参数的话,请在终端输入 --abc
commander
先来看强大的 commander
const program = require(\'commander\')
program
.version(\'0.1.0\')
.option(\'-p, --peppers\', \'Add peppers\')
.option(\'-P, --pineapple\', \'Add pineapple\')
.option(\'-b, --bbq-sauce\', \'Add bbq sauce\')
.option(\'-c, --cheese [type]\', \'Add the specified type of cheese [marble]\', \'marble\')
.parse(process.argv);
使用 commaner,首先要注册上你希望的option,最后要调用一下 .parse(process.argv)
,他才会对进程的入参进行解析并转换为commander对象上的属性。
如果是布尔类型的 option(即不需要设置value),则只需要:
.option(\'-p, --peppers\', \'Add peppers\') // 其中第一个参数格式是固定的,第二个参数是 --help 时候要显示的提示。
.option(\'--no-sauce\', \'Remove sauce\') // 这是定义了一个 否定用法。即用户如果输入 `--no-sauce`, 则commander拿到的sauce就是false
如果是 value类型的option,则
.option(\'-c, --cheese [type]\', \'Add the specified type of cheese [marble]\', \'marble\') // 其中第一个引号里要固定加上中括号表示的参数(尖括号表示必填),第二个参数是帮助文案,第三个参数是cheese参数的默认值(如果不需要默认值可以不填)
如果希望能在 --help
的时候显示用法,只需:
program.usage(\'[options]\')
这样执行命令时便会打印:
Usage: xxx [options] // 其中 xxx 是你的命令行程序名字,会自动填在前面;后面就是你定义的usage字符串
如果希望拿到用户输入的value参数,则可以调用: program.args
, commander会把解析的value参数以数组形式放置在args属性上。
如果你的命令行程序有子命令,如 lime init [options]
则可以通过command方法添加:
program
.command(\'rm <dir>\')
.option(\'-r, --recursive\', \'Remove recursively\')
.action(function (dir, cmd) {
console.log(\'remove \' + dir + (cmd.recursive ? \' recursively\' : \'\'))
}) // 其中 action回调函数收到的第一个形参为命令中的参数如dir,最后一个形参是命令的Command对象(该对象可以直接访问来获取相关option)
command方法中可以声明命令所需的value参数,如果用尖括号包裹 dir
, 则dir就是必填的;中括号包裹就是选填;也可以声明多个参数。
.command(\'rmdir <dir> [otherDirs]\') // 表明rmdir子命令需要2个参数,第一个必填,第二个选填
.command(\'rmdir <dir> [otherDirs...]\')
.action(function (dir, otherDirs) {
}) // 表明otherDirs是可变参数,会以数据形式传递给 action 的 otherDirs形参。
commander 中还有很多针对参数类型、参数格式的设定的方式,如:
program
.version(\'0.1.0\')
.usage(\'[options] <file ...>\')
.option(\'-i, --integer <n>\', \'An integer argument\', parseInt)
.option(\'-f, --float <n>\', \'A float argument\', parseFloat)
.option(\'-r, --range <a>..<b>\', \'A range\', range)
.option(\'-l, --list <items>\', \'A list\', list) // 输入格式为 -l 1,2,3
.option(\'-o, --optional [value]\', \'An optional value\')
.option(\'-c, --collect [value]\', \'A repeatable value\', collect, [])
.option(\'-v, --verbose\', \'A value that can be increased\', increaseVerbosity, 0)
.parse(process.argv);
或针对选项设定 正则表达式 规则:
program
.version(\'0.1.0\')
.option(\'-s --size <size>\', \'Pizza size\', /^(large|medium|small)$/i, \'medium\')
.option(\'-d --drink [drink]\', \'Drink\', /^(coke|pepsi|izze)$/i) // 输入的-d参数必须满足正则,否则会无法获取到-d的输入参数,变成布尔类型。
.parse(process.argv);
另外一种设置子命令的方式是通过子文件的方式。
大家可以去参考 commander 官方文档
智能帮助: 如果你希望在用户输入 -h 的时候能添加一点自己的东西,可以:
program.on(\'--help\', function(){
console.log(\'\')
console.log(\'Examples:\');
console.log(\' $ custom-help --help\');
console.log(\' $ custom-help -h\');
});
如果你希望在某些场景(例如用户没有输入子命令时),主动打印help信息,则可以:
if (program.args.length < 1) {
// 没有输入子命令,则打印帮助
program.outputHelp((txt) => {
txt += \'\\n\'
txt += \' Examples: \\n\\n\'
txt += \' hello\'
return txt
});
}
yargs获取命令行参数
获取命令行参数,在很多编程语言中都类似。node中 process.argv
表示执行该脚本时传入的参数的数组。其中,前两个参数分别就是node可执行程序的位置和当前脚本的位置,如我的mac上的执行效果如下:
[ \'/usr/local/Cellar/node/8.6.0/bin/node\',
\'/Users/cuiyongjian/Code/lime-cli/lime-cli/bin/lime\' ]
- 然而,在命令行程序中,经常需要用户设置命令的options, 如:
node hello --name=tom
node hello -n tom
这时我们需要获取shell中用户输入的name或n参数,除了自己解析 process.argv
数组外,还可通过第三方包 yargs
来更方便获取这些入参
var argv = require(\'yargs\').argv; // argv拿到的就是
console.log(argv.n) // yargs.argv会采集所有输入转为 key-value 对的形式
若想让n是name的别名,则可以设置 yargs
:
var argv = require(\'yargs\')
.alias(\'n\', \'name\')
.argv;
通过 argv._
可以获取到所有非options的参数,如下面这条命令的 argv._
的结果就是 [ \'A\', \'B\', \'C\' ]
:
hello A -n tom B C
- 使用命令行参数时,还需要经常接收
-h
参数展示帮助信息;对参数必填选填进行校验;布尔性质的option等操作。yargs都具备了这些功能,例如可以设置一个参数的各种特性:
var argv = require(\'yargs\')
.option(\'n\', {
alias : \'name\',
demand: true,
default: \'tom\',
describe: \'your name\',
type: \'string\'
})
.argv;
此时,就为argv设置了一个n参数的详细信息,表示其是必填的,且默认值是 "tom", 描述信息是 "your name"。 在命令行中输入 node yourfile -h
, 则 argv
会给显示出你配置这些参数使用方法:
选项:
--help 显示帮助信息 [布尔]
--version 显示版本号 [布尔]
-n, --name your name [字符串] [必需]
- 当然,这还不够,一个完整的命令行程序应该还要提示用法(Usage), 例子(Example),所以yargs也提供了一个简单的配置说明和示例的API:
var argv = require(\'yargs\')
.option(\'f\', {
alias : \'name\',
demand: true,
default: \'tom\',
describe: \'your name\',
type: \'string\'
})
.usage(\'Usage: hello [options]\')
.example(\'hello -n tom\', \'say hello to Tom\')
.help(\'h\')
.alias(\'h\', \'help\')
.epilog(\'copyright 2015\')
.argv;
Usage: hello [options]
选项:
--version 显示版本号 [布尔]
-f, --name your name [字符串] [必需] [默认值: "tom"]
-h, --help 显示帮助信息 [布尔]
示例:
hello -n tom say hello to Tom
copyright 2015
这就比较完整了。 如果 -n
只是作为一个option开关来用,则只需将其设置为boolean类型即可:
var argv = require(\'yargs\')
.option(\'n\', {
boolean: true
})
.argv;
- 子命令怎么办
我们使用git时,经常会用到类似 git remote ...
这样的命令。其 remote
就是git的一个子命令,子命令有自己的option和输入值,yargs支持设置子命令。 要注意子命令的option配置,要在子命令捕获后的回调函数里进行设置。
require(\'shelljs/global\');
var argv = require(\'yargs\')
.command("morning", "good morning", function (yargs) {
echo("Good Morning");
var argv = yargs.reset()
.option("m", {
alias: "message",
description: "provide any sentence"
})
.help("h")
.alias("h", "help")
.argv;
echo(argv.m);
})
.argv;
使用 inquirer 与用户进行交互
inquirer 可以交互式的提示用户输入某些参数。
inquirer.prompt([
{
type: \'confirm\',
name: \'destOk\',
message: \'确认使用目标文件夹:\' + destFolder
}
]).then(function(answers){
子进程运行其他命令
有时我们在命令行程序里需要调用其他bash命令,这时可以通过node自带的子进程模块,可以执行任意的shell程序,并异步接收结果:
#!/usr/bin/env node
var name = process.argv[2];
var exec = require(\'child_process\').exec;
var child = exec(\'echo hello \' + name, function(err, stdout, stderr) {
if (err) throw err;
console.log(stdout);
});
有个第三方包 shelljs
, 可以利用其API进行各种Unix命令操作, 例如:
shelljs.rm(@params, @destinationFile)
shelljs还有全局API模式,可以将一些linux命令API设置在node全局空间内,如:
require(\'shelljs/global\');
if (!which(\'git\')) {
echo(\'Sorry, this script requires git\');
exit(1);
}
mkdir(\'-p\', \'out/Release\');
cp(\'-R\', \'stuff/*\', \'out/Release\');
最好不要使用全局模式咯,毕竟覆盖掉全局空间内的API是不太好。
字符图片
为了让你的命令行程序 高大上
,你可能需要一点 字符图片
作为logo。可以安装这个node程序:
npm install -g figlet-cli
然后执行:
figlet "LIME TOOL"
生成的字符图片如下:
_ ___ __ __ _____ _____ ___ ___ _
| | |_ _| \\/ | ____| |_ _/ _ \\ / _ \\| |
| | | || |\\/| | _| | || | | | | | | |
| |___ | || | | | |___ | || |_| | |_| | |___
|_____|___|_| |_|_____| |_| \\___/ \\___/|_____|
还可以吧老妹
其他常用工具
colors 一个修改shell中打印字符的颜色的工具。
chalk 也是一个颜色库
ora 一个能在shell内产生动态loading效果的库
rimraf 一个简单的rm删除库
conf 一个可以用来存储配置的库,有了它就不用自己写json来存储一些配置信息啦
cli-table 一个能在命令行打印表格的模块
boxen 可以在终端命令行内生成字符框框,瞬间高大上咯
调试
自己在本地调试node命令行程序时,可以采用 npm link
的方式把当前包 软链
到全局。如果是调试一个本地包也可以把它link到局部。使用方式非常简单,可以参考这篇文章: https://github.com/atian25/bl...
我的示例
- merge-file 可以基于二进制进行2个文件的无脑合并
- lime-cli 青檬脚手架~ 可以用此工具生成各种常用的前端项目脚手架,例如webapp项目、前端库项目等。
以上是关于开发 Node.js 命令行程序的一些常用工程实践的主要内容,如果未能解决你的问题,请参考以下文章
小册基于 hapi 的 Node.js 小程序后端开发实践指南