-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
146 lines (134 loc) · 4.48 KB
/
Copy pathindex.js
File metadata and controls
146 lines (134 loc) · 4.48 KB
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
const DEFAULT_OPTS = {
help: true
};
const parseSignature = require('./parseSignature');
const decodeArgs = require('./decodeArgs');
const usage = require('./usage');
const completions = require('./completions');
/**
* fncli - function-based cli scaffold.
* Parses argv based on the given function signature, and
* then calls the function.
*/
module.exports = function (commands, {argv=process.argv, ...config}={}) {
config = Object.assign({}, DEFAULT_OPTS, config);
try {
return parseAndRun(argv, commands, config);
} catch(e) {
console.error(e);
}
}
function parseAndRun(argv, commands, config) {
const [, arg0, ...args] = argv;
// Shell completion is built in and on by default: the `completions` command
// group is injected as an ordinary sibling command (dispatched by name, no
// special casing) — it prints/installs the shell stub and answers the per-TAB
// runtime (`completions v1 -- …`). A bare function becomes the '' default
// command, so `fncli(fn)` runs fn while `completions` sits beside it. Opt out
// (freeing the name) with {completions: false}.
const cctx = { config: config.completions || {} };
const runnable = buildRunnable(commands, config, cctx);
const opts = parseSignature(runnable);
opts.arg0 = arg0;
if (config.help) {
addHelpOption(opts);
}
cctx.opts = opts;
// The `completions` group and its `v1` runtime hide themselves via a `HIDE`
// synopsis (see completions.js / parseSignature), so nothing to poke here.
const decoded = decodeArgs(opts, args);
const isHelpRequested = config.help && decoded.optionValues.help;
if (isHelpRequested) {
decoded.error = true;
}
if (decoded.error) {
const text = usage({...decoded, isHelp: isHelpRequested});
if (isHelpRequested) {
// Requested help is not an error: print to stdout (pipeable), exit 0.
console.log(text);
} else {
process.exitCode = 2;
console.error(text);
}
} else {
const func = getHandler(runnable, decoded.commandPath);
applyFunc(decoded, func);
}
}
// Assemble the runnable command map: the user's commands plus the built-in
// `completions` group. A bare function becomes the '' default command, so
// `fncli(fn)` === `fncli({ '': fn })` with `completions` as a sibling.
function buildRunnable(commands, config, cctx) {
if (config.completions === false) return commands;
const group = completions.commands(cctx);
if (typeof commands === 'function') {
return { '': commands, completions: group };
}
return Object.assign({}, commands, { completions: group });
}
// Register --help on the top-level descriptor and every (nested) command, so
// `cmd sub --help` decodes after the option context switches to the
// sub-command. User-defined help/h options are left alone.
function addHelpOption(optDesc) {
if (optDesc.commands) {
for (let name in optDesc.commands) {
addHelpOption(optDesc.commands[name].optDesc);
}
}
if (optDesc.options.help) return;
const help = {name: 'help', hasArg: false, synopsis: 'Prints this message'};
if (!optDesc.options.h) {
help.alias = 'h';
optDesc.options.h = help;
}
optDesc.options.help = help;
}
function getHandler(commands, commandPath) {
// Walk the decoded command path down to the handler function.
let handler = commands;
for (let name of commandPath) {
handler = handler[name];
}
if (typeof handler !== 'function') {
throw new Error('invalid type: ' + typeof handler);
}
return handler;
}
module.exports.getHandler = getHandler;
/**
* Apply the decoded argument result, including printing out usage errors.
*
* @param {*} args
* @param {*} func
*/
function applyFunc(decoded, func) {
let result;
try {
result = func.apply(null, decoded.apply);
} catch(e) {
handleHandlerError(decoded, e);
return;
}
if (result && typeof result.then === 'function') {
// Async handler — await the promise but keep error handling consistent.
return result.then(
r => handleReturn(decoded, r),
e => handleHandlerError(decoded, e)
);
}
handleReturn(decoded, result);
}
function handleReturn(decoded, result) {
if (typeof result === 'string' && result.startsWith('error:')) {
process.exitCode = 1;
console.error(usage({...decoded, error: result}));
}
}
function handleHandlerError(decoded, e) {
if (e && typeof e.toString === 'function' && e.toString().startsWith('error:')) {
process.exitCode = 1;
console.error(usage({...decoded, error: e.toString()}));
} else {
throw e;
}
}