UNPKG

3.74 kBJavaScriptView Raw
1const mri = require('mri');
2const $ = require('./utils');
3
4const ALL = '__all__';
5const DEF = '__default__';
6
7class Sade {
8 constructor(name) {
9 this.tree = {};
10 this.name = name;
11 this.ver = '0.0.0';
12 this.default = '';
13 // set internal shapes;
14 this.command(ALL);
15 this.command(`${DEF} <command>`)
16 .option('-v, --version', 'Displays current version');
17 this.curr = ''; // reset
18 }
19
20 command(str, desc, opts) {
21 let cmd=[], usage=[], rgx=/(\[|<)/;
22 // All non-([|<) are commands
23 str.split(/\s+/).forEach(x => {
24 (rgx.test(x.charAt(0)) ? usage : cmd).push(x);
25 });
26
27 // Back to string~!
28 cmd = cmd.join(' ');
29
30 if (cmd in this.tree) {
31 throw new Error(`Command already exists: ${cmd}`);
32 }
33
34 this.curr = cmd;
35 (opts && opts.default) && (this.default=cmd);
36
37 !~cmd.indexOf('__') && usage.unshift(cmd); // re-include `cmd`
38 usage = usage.join(' '); // to string
39
40 this.tree[cmd] = { usage, options:[], alias:{}, default:{}, examples:[] };
41 desc && this.describe(desc);
42
43 return this;
44 }
45
46 describe(str) {
47 this.tree[this.curr || DEF].describe = Array.isArray(str) ? str : $.sentences(str);
48 return this;
49 }
50
51 option(str, desc, val) {
52 let cmd = this.tree[ this.curr || ALL ];
53
54 let [flag, alias] = $.parse(str);
55 (alias && alias.length > 1) && ([flag, alias]=[alias, flag]);
56
57 str = `--${flag}`;
58 if (alias && alias.length > 0) {
59 str = `-${alias}, ${str}`;
60 let old = cmd.alias[alias];
61 cmd.alias[alias] = (old || []).concat(flag);
62 }
63
64 let arr = [str, desc || ''];
65
66 if (val !== void 0) {
67 arr.push(val);
68 cmd.default[flag] = val;
69 }
70
71 cmd.options.push(arr);
72 return this;
73 }
74
75 action(handler) {
76 this.tree[ this.curr || DEF ].handler = handler;
77 return this;
78 }
79
80 example(str) {
81 this.tree[ this.curr || DEF ].examples.push(str);
82 return this;
83 }
84
85 version(str) {
86 this.ver = str;
87 return this;
88 }
89
90 parse(arr, opts={}) {
91 let offset = 2; // argv slicer
92 let alias = { h:'help', v:'version' };
93 let argv = mri(arr.slice(offset), { alias });
94 let bin = this.name;
95
96 // Loop thru possible command(s)
97 let tmp, name='';
98 let i=1, len=argv._.length + 1;
99 for (; i < len; i++) {
100 tmp = argv._.slice(0, i).join(' ');
101 if (this.tree[tmp] !== void 0) {
102 name=tmp; offset=(i + 2); // argv slicer
103 }
104 }
105
106 let cmd = this.tree[name];
107 let isVoid = (cmd === void 0);
108
109 if (isVoid) {
110 if (this.default) {
111 name = this.default;
112 cmd = this.tree[name];
113 arr.unshift(name);
114 offset++;
115 } else if (name) {
116 return $.error(bin, `Invalid command: ${name}`);
117 } //=> else: cmd not specified, wait for now...
118 }
119
120 if (argv.version) {
121 return console.log(`${bin}, ${this.ver}`);
122 }
123
124 if (argv.help) {
125 return this.help(!isVoid && name);
126 }
127
128 if (cmd === void 0) {
129 return $.error(bin, 'No command specified.');
130 }
131
132 let all = this.tree[ALL];
133 // merge all objects :: params > command > all
134 opts.alias = Object.assign(all.alias, cmd.alias, opts.alias);
135 opts.default = Object.assign(all.default, cmd.default, opts.default);
136
137 let vals = mri(arr.slice(offset), opts);
138 let segs = cmd.usage.split(/\s+/);
139 let reqs = segs.filter(x => x.charAt(0)==='<');
140 let args = vals._.splice(0, reqs.length);
141
142 if (args.length < reqs.length) {
143 name && (bin += ` ${name}`); // for help text
144 return $.error(bin, 'Insufficient arguments!');
145 }
146
147 segs.filter(x => x.charAt(0)==='[').forEach(_ => {
148 args.push(vals._.pop()); // adds `undefined` per [slot] if no more
149 });
150
151 args.push(vals); // flags & co are last
152 let handler = cmd.handler;
153 return opts.lazy ? { args, name, handler } : handler.apply(null, args);
154 }
155
156 help(str) {
157 console.log(
158 $.help(this.name, this.tree, str || DEF)
159 );
160 }
161}
162
163module.exports = str => new Sade(str);