/
arg-parser.js
162 lines (142 loc) · 6.25 KB
/
arg-parser.js
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
const commander = require('commander');
const logger = require('./logger');
const { commands } = require('./cli-flags');
const runHelp = require('../groups/runHelp');
const { defaultCommands } = require('./commands');
/**
* Creates Argument parser corresponding to the supplied options
* parse the args and return the result
*
* @param {object[]} options Array of objects with details about flags
* @param {string[]} args process.argv or it's subset
* @param {boolean} argsOnly false if all of process.argv has been provided, true if
* args is only a subset of process.argv that removes the first couple elements
*/
const argParser = (options, args, argsOnly = false, name = '') => {
const parser = new commander.Command();
// Set parser name
parser.name(name);
parser.storeOptionsAsProperties(false);
commands.reduce((parserInstance, cmd) => {
parser
.command(cmd.name)
.alias(cmd.alias)
.description(cmd.description)
.usage(cmd.usage)
.allowUnknownOption(true)
.action(async () => {
const cliArgs = args.slice(args.indexOf(cmd.name) + 1 || args.indexOf(cmd.alias) + 1);
return await require('./resolve-command')(defaultCommands[cmd.name], ...cliArgs);
});
return parser;
}, parser);
// Prevent default behavior
parser.on('command:*', () => {});
// Use customized help output if available
parser.on('option:help', () => {
runHelp(args);
process.exit(0);
});
// Allow execution if unknown arguments are present
parser.allowUnknownOption(true);
// Register options on the parser
options.reduce((parserInstance, option) => {
let optionType = option.type;
let isStringOrBool = false;
if (Array.isArray(optionType)) {
// filter out duplicate types
optionType = optionType.filter((type, index) => {
return optionType.indexOf(type) === index;
});
// the only multi type currently supported is String and Boolean,
// if there is a case where a different multi type is needed it
// must be added here
if (optionType.length === 0) {
// if no type is provided in the array fall back to Boolean
optionType = Boolean;
} else if (optionType.length === 1 || optionType.length > 2) {
// treat arrays with 1 or > 2 args as a single type
optionType = optionType[0];
} else {
// only String and Boolean multi type is supported
if (optionType.includes(Boolean) && optionType.includes(String)) {
isStringOrBool = true;
} else {
optionType = optionType[0];
}
}
}
const flags = option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`;
let flagsWithType = flags;
if (isStringOrBool) {
// commander recognizes [value] as an optional placeholder,
// making this flag work either as a string or a boolean
flagsWithType = `${flags} [value]`;
} else if (optionType !== Boolean) {
// <value> is a required placeholder for any non-Boolean types
flagsWithType = `${flags} <value>`;
}
if (isStringOrBool || optionType === Boolean || optionType === String) {
if (option.multiple) {
// a multiple argument parsing function
const multiArg = (value, previous = []) => previous.concat([value]);
parserInstance.option(flagsWithType, option.description, multiArg, option.defaultValue).action(() => {});
} else {
// Prevent default behavior for standalone options
parserInstance.option(flagsWithType, option.description, option.defaultValue).action(() => {});
}
} else if (optionType === Number) {
// this will parse the flag as a number
parserInstance.option(flagsWithType, option.description, Number, option.defaultValue);
} else {
// in this case the type is a parsing function
parserInstance.option(flagsWithType, option.description, optionType, option.defaultValue).action(() => {});
}
if (option.negative) {
// commander requires explicitly adding the negated version of boolean flags
const negatedFlag = `--no-${option.name}`;
parserInstance.option(negatedFlag, `negates ${option.name}`).action(() => {});
}
return parserInstance;
}, parser);
// if we are parsing a subset of process.argv that includes
// only the arguments themselves (e.g. ['--option', 'value'])
// then we need from: 'user' passed into commander parse
// otherwise we are parsing a full process.argv
// (e.g. ['node', '/path/to/...', '--option', 'value'])
const parseOptions = argsOnly ? { from: 'user' } : {};
const result = parser.parse(args, parseOptions);
const opts = result.opts();
const unknownArgs = result.args;
args.forEach((arg) => {
const flagName = arg.slice(5);
const option = options.find((opt) => opt.name === flagName);
const flag = `--${flagName}`;
const flagUsed = args.includes(flag) && !unknownArgs.includes(flag);
let alias = '';
let aliasUsed = false;
if (option && option.alias) {
alias = `-${option.alias}`;
aliasUsed = args.includes(alias) && !unknownArgs.includes(alias);
}
// this is a negated flag that is not an unknown flag, but the flag
// it is negating was also provided
if (arg.startsWith('--no-') && (flagUsed || aliasUsed) && !unknownArgs.includes(arg)) {
logger.warn(
`You provided both ${
flagUsed ? flag : alias
} and ${arg}. We will use only the last of these flags that you provided in your CLI arguments`,
);
}
});
Object.keys(opts).forEach((key) => {
if (opts[key] === undefined) {
delete opts[key];
}
});
return {
unknownArgs,
opts,
};
};
module.exports = argParser;