0%

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
32
33
34
35
36
37
38
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
12
13
14
//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
15
16
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
16
17
18
19
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
15
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
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
14
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
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
23
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,就是这样