基于Node开发命令行程序

s

## 介绍
node开发命令行程序非常方便,我们常用的webpack,babel,http-server,express-generator, yeoman等等,都是node.js开发出来的命令行工具。


## 原理
node.js的程序一般通过node指令进行运行。而在Unix Like的系统中,任何一种脚本也可以被系统当做可执行程序执行。只需要按照如下约定,设置脚本的运行时程序即可,例如对于Node.js脚本来说,只需要设置他的运行程序是node:

1
2
#!/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安装的该全局包所在位置。这是npm自动完成的行为,我们如果希望开发一个可全局使用的命令行程序,让npm帮助我们生成/usr/local下的可执行文件的话,只需要在npm包种,将我们预先写好的入口文件设置为package.json的bin字段即可:

1
2
3
4
"main": "./lib/echo.js", # 入口模块位置
"bin" : {
"node-echo": "./bin/node-echo" # 命令行程序名和主模块位置
}

然后入口文件一般放置在package包目录根目录中的 bin 目录下,如 bin/node-echo 这样写:

1
2
#! /usr/bin/env node
require('../src/index.js');

此时,使用 npm i -g 来安装这个包时, npm则会自动在 /usr/local/bin 下面生成 node-echo 可执行文件,它是一个软连接,会链接到 yourNpmPrefix/node_modules/yourPackageName/bin/node-echo

命令行程序常用操作

开发命令行程序,有一些基本的套路,比如一般需要展示程序的基本用法,获取用户输入的参数,新建子进程执行其他程序等。

获取命令行参数

获取命令行参数,在很多编程语言中都类似。node中 process.argv 表示执行该脚本时传入的参数的数组。其中,前两个参数分别就是node可执行程序的位置和当前脚本的位置,如我的mac上的执行效果如下:

1
2
[ '/usr/local/Cellar/node/8.6.0/bin/node',
'/Users/cuiyongjian/Code/lime-cli/lime-cli/bin/lime' ]
  1. 然而,在命令行程序中,经常需要用户设置命令的options, 如:
1
2
$ node hello --name=tom
$ node hello -n tom

这时我们需要获取shell中用户输入的name或n参数,除了自己解析 process.argv 外,还可通过第三方包 yargs 来更方便获取。

1
2
var argv = require('yargs').argv;
console.log(argv.n)

若想让n是name的别名,则可以设置 yargs:

1
2
3
var argv = require('yargs')
.alias('n', 'name')
.argv;

通过 argv._ 可以获取到所有非options的参数,如下面这条命令的 argv._ 的结果就是 [ 'A', 'B', 'C' ]:

1
$ hello A -n tom B C

  1. 使用命令行参数时,还需要经常接收 -h 参数展示帮助信息;对参数必填选填进行校验;布尔性质的option等操作。yargs都具备了这些功能,例如可以设置一个参数的各种特性:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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 会给显示出你配置这些参数使用方法:

1
2
3
4
选项:
--help 显示帮助信息 [布尔]
--version 显示版本号 [布尔]
-n, --name your name [字符串] [必需]

  1. 当然,这还不够,一个完整的命令行程序应该还要提示用法(Usage), 例子(Example),所以yargs也提供了一个简单的配置说明和示例的API:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
1
2
3
4
5
6
7
8
9
10
11
Usage: hello [options]
选项:
--version 显示版本号 [布尔]
-f, --name your name [字符串] [必需] [默认值: "tom"]
-h, --help 显示帮助信息 [布尔]
示例:
hello -n tom say hello to Tom
copyright 2015

这就比较完整了。 如果 -n 只是作为一个option开关来用,则只需将其设置为boolean类型即可:

1
2
3
4
5
var argv = require('yargs')
.option('n', {
boolean: true
})
.argv;
  1. 子命令怎么办
    我们使用git时,经常会用到类似 git remote ... 这样的命令。其 remote 就是git的一个子命令,子命令有自己的option和输入值,yargs支持设置子命令。 要注意子命令的option配置,要在子命令捕获后的回调函数里进行设置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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;

子进程

有时我们在命令行程序里需要调用其他bash命令,这时可以通过node自带的子进程模块,可以执行任意的shell程序,并异步接收结果:

1
2
3
4
5
6
7
8
#!/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命令操作, 例如:

1
shelljs.rm(@params, @destinationFile)

shelljs还有全局API模式,可以将一些linux命令API设置在node全局空间内,如:

1
2
3
4
5
6
7
8
9
require('shelljs/global');
if (!which('git')) {
echo('Sorry, this script requires git');
exit(1);
}
mkdir('-p', 'out/Release');
cp('-R', 'stuff/*', 'out/Release');

最好不要使用全局模式咯,毕竟覆盖掉全局空间内的API是不太好。

我的示例

  • merge-file 可以基于二进制进行2个文件的无脑合并
  • lime-cli 青檬脚手架~ 可以用此工具生成各种常用的前端项目脚手架,例如webapp项目、前端库项目等。

实践

首先,你需要构造基本的命令行程序项目骨架。如果你不想自己操作,可以通过lime-cli来自动生成一个nodebin-template类型的项目骨架。此脚手架做了一件非常简单的事情,就是在你的项目中,定义好了一个带有 #! /usr/bin/env node 的文件,并配置好了package.json.

接下来,只需在index.js中书写逻辑代码即可。如果业务比较复杂,可以将代码归并到lib目录中(lib目录已经默认设置为了发布到npm的目录)。

Refer

教你从零开始搭建一款前端脚手架工具