pro-node 之 CLI & commander,minimist源码解析

#CLI 即 命令行接口
command line interface

node程序接受的参数存放在process.argv 和 execArgv中
示例中$表示在系统的Console中运行,>表示在nodejs的repl中运行

$ node example arg1 arg2 arg3

结果

1
2
argv = [node,example,arg1,arg2,arg3]
execArgv = [arg1,arg2,arg3]

就是execArg比较方便,不过在debug的时候,使用

$ node --debug-brk example arg1 arg2 arg3

结果

1
2
argv = [node,example,arg1,arg2,arg3]
execArgv = ["--debug-brk",arg1,arg2,arg3]

显然这个–debug-brk不是我们想要的,因而直接使用process.argv.slice(2)即可

接收到的值都是string类型,使用parseInt/parseFloat来解析数值

#std in/out/err 标准流
process.stdin stdout stderr属性表示该process的标准流

##process.stdin

1
2
3
4
5
6
7
> typeof process.stdin
'object'

> process.stdin.constructor
{ [Function: ReadStream]
super_: { [Function: Socket] super_: { [Function: Duplex] super_: [Object] } }
}

可以看出这个stdin是一个ReadStream类型实例,继承关系为 ReadStream : Socket : Duplex
所谓可读stream,是程序可以从其他地方读取数据,在程序执行时
stdin默认是被pause暂停了,要不然程序一运行就要你输东西,不执行…

从命令行窗口读取值

1
2
3
4
5
6
process.stdin.once('data', function(data) {
process.stdin.pause();
console.log(data);
});
process.stdout.write(question);
process.stdin.resume();

封装了一个ask函数

1
2
3
4
5
6
7
8
function ask(question, done) {
process.stdin.once('data', function(data) {
process.stdin.pause();
done(data.toString());
});
process.stdout.write(question);
process.stdin.resume();
}

调用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var m = require('magicdawn');

function ask(question, done) {
process.stdin.once('data', function(data) {
process.stdin.pause();
done(data.toString());
});
process.stdout.write(question);
process.stdin.resume();
}

m.async.series([
function(next) {
var self = this;
ask("What's your name : ", function(name) {
self.name = name;
//console.log(name);
next();
});
},
function(next) {
var self = this;
ask("And age : ", function(age) {
self.age = parseInt(age);
next();
});
}
], function(err, results) {
console.log("Got it,your name is %s , %s years old , and %s a adult !",
this.name, this.age, this.age >= 18 ? 'you' : 'not');
});

测试,注意从窗口读取的值后面有一个\r\n,可以trim掉
这个async.series是我前面有关async流程控制博文中实现的this传值那个series

1
2
3
4
What's your name : zhangsan
And age : 18
Got it,your name is zhangsan
, 18 years old , and you a adult !

##process.stdout

1
2
3
4
5
6
> typeof process.stdout
'object'
> process.stdout.constructor
{ [Function: WriteStream]
super_: { [Function: Socket] super_: { [Function: Duplex] super_: [Object] } }
}

WriteStream : Socket : Duplex
可写stream,程序可以输出数据,使用process.stdout.write(data,encoding,cb)
data可以是string 或 Buffer

###console.log
console.log就是调用了process.stdout.write,源码:

1
2
3
Console.prototype.log = function() {
this._stdout.write(util.format.apply(this, arguments) + '\n');
};

####util.format格式说明符
|格式|说明|
|- |-|
|%s |string|
|%d |int数据 或 float数据|
|%j |json数据|
|%% |转义%,表示一个%|

####util.log
带上当前时间,真正的log

1
2
> util.log('abc')
8 Aug 11:49:06 - abc

####util.inspect
console.log -> util.format -> util.inspect
一个object,toString可能是"[Object]",没有价值,使用inspect来显式它的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var o = {
level0: 'level0',
o1: {
level1: "level1",
o2: {
level2: 'level2',
o3: {
level3: 'level3',
o4: {
level4: "level4"
}
}
}
}
};

console.log(o)其实调用的是util.inspect(o),默认深度2,就是能看到”level2”,但是不能看到”level3”,使用util.inspect(o,{ depth : 3})指定depth配置深度,使用null就不限深度.

##process.stderr

1
2
3
4
5
6
> typeof process.stderr
'object'
> process.stderr.constructor
{ [Function: WriteStream]
super_: { [Function: Socket] super_: { [Function: Duplex] super_: [Object] } }
}

console.warn = console.error的,而他们又是调用的process.stderr.write的,见nodejs源码

同stdout也是WriteStream,不过stdout可以被重定向

example.js:

1
2
process.stdout.write('hello in stdout ...' + '\n');
process.stderr.write('hello in stderr ...' + '\n');

执行1

1
$ node example.js

结果1

1
2
hello in stdout ...
hello in stderr ...

执行2

1
$ node example.js > out.txt

结果2

1
2
hello in stderr ...
//同时out.txt出现了 hello in stdout ...

这个就是stdout重定向,默认它是指向console窗口的,isTTY属性为true,重定向之后不为true


process.env表示环境变量,可以更改这个变量,例如process.env.PATH可以更改,但不会影响操作系统的PATH变量
使用process.env.DEVELOPMENT表示开发环境这种做法也有较多库采用,Express就是

##关于encoding

使用process.setEncoding(xxx)解决,前面child_process又说中文乱码,用iconv-lite解决的做法,使用encoding为binary

#minimist使用及源码解析

##minimist基本用法
示例1 : 来自minimist’s readme

1
2
var argv = require('minimist')(process.argv.slice(2));
console.dir(argv);

1
2
$ node example/parse.js -a beep -b boop
{ _: [], a: 'beep', b: 'boop' }
1
2
3
4
5
6
7
8
9
$ node example/parse.js -x 3 -y 4 -n5 -abc --beep=boop foo bar baz
{ _: [ 'foo', 'bar', 'baz' ],
x: 3,
y: 4,
n: 5,
a: true,
b: true,
c: true,
beep: 'boop' }

就是

  • -f表示短选项,–flag表示成长选项
  • 赋值,-f a,–fall b,就表示f=a;fall=b
  • _包括不含任何flag的arg,包括command

hexo s -g,s表示serve就是一个command,包含在_中,-g会生成g=true
hexo -g s,则会生成 g = 's';

##minimist的高级选项

###option.string
例如

1
2
3
4
5
6
> var parse = require('minimist')
undefined
> parse(['--age','18'])
{ _: [], age: 18 }
> parse(['--age','18'],{ string : 'age' })
{ _: [], age: '18' }

默认minimist将看起来像数值的,转化为Number类型,通过指定string : “age”,指定age应该是string类型,多个选项都是,用数组表示

###option.boolean
指定某些值应该被解释为bool类型

1
2
3
4
> parse(['--watch','generate']);
{ _: [], watch: 'generate' }
> parse(['--watch','generate'] , { boolean : 'watch' });
{ _: [ 'generate' ], watch: true }

指定了boolean:’watch’,那么generate就不是watch的值了,存放在_数组里.
一个值用string,多个值用string数组,还可以设置boolean :true,这个是设置所有的不是-p=10 --process=10这种flag,为bool类型

###option.alias
就是alias

1
2
3
4
5
6
7
8
9
10
11
12
13
> parse(['-h','--generate'],{
alias : {
h : ['help','hlp'],
g : 'generate'
}
})

{ _: [],
h: true,
help: true,
hlp: true,
generate: true,
g: true }

###option.default
设置初始值,默认值,不多说

###option['--']
option['--']设置为true,则在--后面的参数,统统放到argv['--']里面,不再解析

1
2
3
4
5
6
> parse(['-abc','10','--','-g'],{ '--' : true })
{ _: [],
a: true,
b: true,
c: 10,
'--': [ '-g' ] }

##minimist源码解析
首先处理boolean option参数,使用[].concat(opts['boolean'])消除单个string值与string数组之间的差别

1
2
3
4
5
6
7
8
9
10
11
//boolean = true
if (typeof opts['boolean'] === 'boolean' && opts['boolean']) {
flags.allBools = true;
}
//boolean = 'arg1';
//boolean = [arg1,arg2]
else {
[].concat(opts['boolean']).filter(Boolean).forEach(function(key) {
flags.bools[key] = true;
});
}

接着处理alias,代码里面有我的注释,假设alias一项是help : [h,he,hp],没加引号,不要在意,把alias里的每一项都变成key

PS : 本来我想可以最后处理alias的,但是自己实现的时候有个问题,在parse的时候,需要知道是否是boolean的,最后做alias工作的话,达不到这个要求,作罢!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var aliases = {};
Object.keys(opts.alias || {}).forEach(function(key) {
//help : [h,he,hp]
//help : h
//key : help
aliases[key] = [].concat(opts.alias[key]);//wrap single value to array
aliases[key].forEach(function(x) {
aliases[x] = [key].concat(aliases[key].filter(function(y) {
return x !== y;
}));
//aliases[h] = [help, he,hp]
//aliases[hp] = [help,h,he]
});
});

接着是option string,处理默认值,处理option['--']选项,把--的index找出,划分前后
再就是主for循环了…for args
setArg函数内部做了alias操作,简单看为设置argv[key]=value吧

--abcd=xxx 的情况,正则匹配出来

1
2
3
4
5
6
7
if (/^--.+=/.test(arg)) {
// Using [\s\S] instead of . because js doesn't support the
// 'dotall' regex modifier. See:
// http://stackoverflow.com/a/1068308/13216
var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
setArg(m[1], m[2]);
}

--no-abcd的情况,设置abcd=false

1
2
3
4
else if (/^--no-.+/.test(arg)) {
var key = arg.match(/^--no-(.+)/)[1];
setArg(key, false);
}

再就是--process这种
取后面一个值,存在 and 不是以-开头 and 当前flag不是boolean的 and boolean没有被设置成true,即代码里面的allTrue and 它的alias不是boolean的,就采用后面这个值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
else if (/^--.+/.test(arg)) {
var key = arg.match(/^--(.+)/)[1];
var next = args[i + 1];
if (next !== undefined && !/^-/.test(next) && !flags.bools[key] && !flags.allBools && (aliases[key] ? !flags.bools[aliases[key]] : true)) {
setArg(key, next);
i++;
}
else if (/^(true|false)$/.test(next)) {
setArg(key, next === 'true');
i++;
}
else {
setArg(key, flags.strings[key] ? '' : true);
}
}

再就是-p 10这种,代码比较长,实现的就是这种,还有-abc 9

1
2
3
4
5
{
a : true,
b : true,
c: 9
}

这种诡异的效果,正则+判断,代码太长,逻辑很复杂,不贴了

再就是普通的command了,push到argv._里

1
2
3
4
5
else {
argv._.push(
flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
);
}

isNumber函数,可以练习下zhengze

1
2
3
4
5
function isNumber(x) {
if (typeof x === 'number') return true;
if (/^0x[0-9a-f]+$/i.test(x)) return true;
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x);
}

#commander.js使用及源码解析

##使用
官方例子1 : 解析命令行参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var program = require('commander');

program
.version('0.0.1')
.option('-p, --peppers', 'Add peppers')
.option('-P, --pineapple', 'Add pineapple')
.option('-b, --bbq', 'Add bbq sauce')
.option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
.parse(process.argv);

console.log('you ordered a pizza with:');
if (program.peppers) console.log(' - peppers');
if (program.pineapple) console.log(' - pineapple');
if (program.bbq) console.log(' - bbq');
console.log(' - %s cheese', program.cheese);

  • 就是将-s , --long这种解析到program上,放到program.long上,我写的-s表示short,简短形式

  • 使用verison添加版本信息,使用option添加选项,使用command添加子命令,如git remote这个remote就是子命令

  • 最后使用parse方法来解析命令行

  • 自动添加的-h , --help参数,以及help子命令

##commander模型
commander的模型基础,根据我的理解哈:
首先command就是一个命令,如git命令行工具,可以有子命令如git add,这个add子命令有一些选项git add --all添加所有,这个--all就是选项,按照约定,--all双线的是长名称all

##源码解析
开头,program = require('commander'),这个program什么东西,看

1
2
exports = module.exports = new Command;
// var p = require('commander');其实是new Command()

其实是new Command(),TJ(不是tai jian 啦)大神写代码真是能省就省

然后exports一些东西

1
2
exports.Command = Command;
exports.Option = Option;

Option构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Option(flags, description) { // "-a , --a [abcd] <abcd>"
this.flags = flags;
this.required = ~flags.indexOf('<'); // ~-1 = 0
this.optional = ~flags.indexOf('[');
this.bool = !~flags.indexOf('-no-');// !~-1 = !0 = true
flags = flags.split(/[ ,|]+/); // 空格 逗号 或符号

//长度大于一,即是 , | 空格分开
//第二个不是 < [ 的
//则第一个就是short,就是说,可以不要short,flag = '--abcd [abcd]'
if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
this.long = flags.shift();
this.description = description || '';
}

Option的两个实例方法name 和 is,

name方法 : 根据long获取option的名字

1
2
3
4
5
Option.prototype.name = function(){
return this.long
.replace('--', '') //去 --
.replace('no-', ''); //取no-
};

is方法 : 检查这个option是否与arg匹配

1
2
3
4
Option.prototype.is = function(arg){
return arg == this.short
|| arg == this.long;
};

Command的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
function Command(name) {
this.commands = [];//子命令
this.options = [];
this._execs = [];//子命令对应为true
this._args = [];//存放 <required> [optional]信息
this._name = name;
}
Command.prototype.__proto__ = EventEmitter.prototype;
//又见新的变种
//1. EventEmitter.call + util.inherit : prototype = obj.create(event.prototype,{ constr})
//2. prototype = new EventEmitter(); reset constructor
//3. 这种,设置prototype.__proto__

接下来是一堆Command的实例方法

Command实例的option方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Command.prototype.option = function(flags, description, fn, defaultValue){
var self = this,
option = new Option(flags, description),
oname = option.name(), // option的long 去--,去no-
name = camelcase(oname);

// default as 3rd arg
if ('function' != typeof fn) defaultValue = fn, fn = null;

// preassign default value only for --no-*, [optional], or <required>
if (false == option.bool || option.optional || option.required) { //程序猿真是操心太多,明明required,管他作甚
// when --no-* we make sure default is true
if (false == option.bool) defaultValue = true;
// preassign only if we have a default
if (undefined !== defaultValue) self[name] = defaultValue;
}

// register the option
this.options.push(option);

// when it's passed assign the value
// and conditionally invoke the callback
//cmd.on('option-name',)通过事件为option赋值
this.on(oname, function(val) {
// coercion
if (null !== val && fn) val = fn(val, undefined === self[name] ? defaultValue : self[name]);

// unassigned or bool
if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
// if no value, bool true, and we have a default, then use it!
if (null == val) {
self[name] = option.bool ? defaultValue || true : false;
}
else {
self[name] = val;
}
}
else if (null !== val) {
// reassign
self[name] = val;
}
});

return this;
};

也就是添加option赋值,同样子command也是这样的,通过action添加..

command方法,添加子命令

1
2
3
4
5
6
7
8
9
10
11
12
13
Command.prototype.command = function(name, desc){ //name = "exec <cmd>"
var args = name.split(/ +/); // args = ['exec','<cmd>']
var cmd = new Command(args.shift()); // exec子命令

if (desc) cmd.description(desc);
if (desc) this.executables = true;
if (desc) this._execs[cmd._name] = true;
this.commands.push(cmd);// 一个Command的command()添加子命令,commands存放子命令
cmd.parseExpectedArgs(args);//...
cmd.parent = this;
if (desc) return this; //指定了desc,return 当前(作为父命令)
return cmd; //没有指定,返回新创建的子命令
};

action方法为子命令添加处理函数,假设命令git remote,那么就是
git.on('remote',handler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Command.prototype.action = function(fn) {
var self = this;

//通过事件实现,git remote -> git.on('remote')
this.parent.on(this._name, function(args, unknown) {
// Parse any so-far unknown options
unknown = unknown || [];
var parsed = self.parseOptions(unknown);

// Output help if necessary
outputHelpIfNecessary(self, parsed.unknown);

// If there are still any unknown options, then we simply
// die, unless someone asked for help, in which case we give it
// to them, and then we die.
if (parsed.unknown.length > 0) {
self.unknownOption(parsed.unknown[0]);
}

// Leftover arguments need to be pushed back. Fixes issue #56
if (parsed.args.length) args = parsed.args.concat(args);

self._args.forEach(function(arg, i) {
if (arg.required && null == args[i]) {
self.missingArgument(arg.name);
}
});

// Always append ourselves to the end of the arguments,
// to make sure we match the number of arguments the user
// expects
if (self._args.length) {
args[self._args.length] = self;
}
else {
args.push(self);
}

fn.apply(this, args);
});
return this;
};

来看parse方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Command.prototype.parse = function(argv){
// implicit help
if (this.executables) this.addImplicitHelpCommand();

// store raw args
this.rawArgs = argv;

// guess name
this._name = this._name || basename(argv[1], '.js');

// process argv
var parsed = this.parseOptions(this.normalize(argv.slice(2)));
var args = this.args = parsed.args;

var result = this.parseArgs(this.args, parsed.unknown);

// executable sub-commands
var name = result.args[0];
if (this._execs[name]) return this.executeSubCommand(argv, args, parsed.unknown);

return result;
};

解析的实际操作,在parseOption里面,一个大for循环…
这个拿到子命令,执行,返回result = this command,就是这样