1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const FAILURE = 1;
|
4 | const POSITIONAL_ARGS_KEY = 'args';
|
5 | const ERROR_PREFFIX = 'ERROR#GETOPT: ';
|
6 | function throwError(message) {
|
7 | throw new Error(ERROR_PREFFIX + message);
|
8 | }
|
9 | function getShorteners(options) {
|
10 | const initialValue = {};
|
11 | return Object.entries(options).reduce((accum, [key, value]) => {
|
12 | if (typeof value === 'object' && value.key)
|
13 | accum[value.key] = key;
|
14 | return accum;
|
15 | }, initialValue);
|
16 | }
|
17 | function parseOption(config, arg) {
|
18 | if (!/^--?[a-zA-Z]/.test(arg))
|
19 | return null;
|
20 | if (arg.startsWith('--')) {
|
21 | return [arg.replace(/^--/, '')];
|
22 | }
|
23 | const shorteners = getShorteners(config);
|
24 | return arg
|
25 | .replace(/^-/, '')
|
26 | .split('')
|
27 | .map(letter => shorteners[letter] || letter);
|
28 | }
|
29 | function getStateAndReset(state) {
|
30 | const partial = { [state.activeOption]: state.optionArgs };
|
31 | Object.assign(state, {
|
32 | activeOption: '',
|
33 | remainingArgs: 0,
|
34 | optionArgs: [],
|
35 | isMultiple: false,
|
36 | });
|
37 | return partial;
|
38 | }
|
39 | function postprocess(input) {
|
40 | const initialValue = {};
|
41 | return Object.entries(input).reduce((accum, [key, value]) => {
|
42 | if (Array.isArray(value) && value.length === 1 && key !== POSITIONAL_ARGS_KEY)
|
43 | accum[key] = value[0];
|
44 | else
|
45 | accum[key] = [...value];
|
46 | return accum;
|
47 | }, initialValue);
|
48 | }
|
49 | function checkRequiredParams(config, input) {
|
50 | if (config._meta_ && typeof config._meta_ === 'object') {
|
51 | const { args = 0, minArgs = 0, maxArgs = 0 } = config._meta_;
|
52 | let providedArgs = 0;
|
53 | let error = null;
|
54 | if (Array.isArray(input[POSITIONAL_ARGS_KEY]) && input[POSITIONAL_ARGS_KEY].length > 0) {
|
55 | providedArgs = input[POSITIONAL_ARGS_KEY].length;
|
56 | }
|
57 | if (args && providedArgs !== args) {
|
58 | error = `${args} positional arguments are required, but ${providedArgs} were provided`;
|
59 | }
|
60 | if (minArgs && providedArgs < minArgs) {
|
61 | error = `At least ${minArgs} positional arguments are required, but ${providedArgs} were provided`;
|
62 | }
|
63 | if (maxArgs && providedArgs > maxArgs) {
|
64 | error = `Max allowed positional arguments is ${maxArgs}, but ${providedArgs} were provided`;
|
65 | }
|
66 | if (error)
|
67 | throwError(error);
|
68 | }
|
69 | Object.entries(config).forEach(([key, value]) => {
|
70 | if (!value || typeof value !== 'object')
|
71 | return;
|
72 | if ((value.mandatory || value.required) && !input[key]) {
|
73 | throwError(`Missing option: "--${key}"`);
|
74 | }
|
75 | if (value.args && value.args !== '*') {
|
76 | const expectedArgsCount = parseInt(String(value.args));
|
77 | const argsCount = input[key] ? input[key].length : 0;
|
78 | if (expectedArgsCount > 0 && expectedArgsCount !== argsCount) {
|
79 | throwError(`Option "--${key}" requires ${expectedArgsCount} arguments, but ${argsCount} were provided`);
|
80 | }
|
81 | }
|
82 | });
|
83 | }
|
84 | function applyDefaults(config, result) {
|
85 | Object.entries(config).forEach(([key, value]) => {
|
86 | if (!value || typeof value !== 'object')
|
87 | return;
|
88 | if ('default' in value && !(key in result)) {
|
89 | const values = Array.isArray(value.default) ? value.default : [value.default];
|
90 | result[key] = values.map(v => (typeof v === 'boolean' ? v : String(v)));
|
91 | }
|
92 | });
|
93 | }
|
94 | function getopt(config = {}, command) {
|
95 | const rawArgs = command.slice(2);
|
96 | const result = {};
|
97 | const args = [];
|
98 | const state = {
|
99 | activeOption: '',
|
100 | remainingArgs: 0,
|
101 | optionArgs: [],
|
102 | isMultiple: false,
|
103 | };
|
104 | rawArgs.forEach(arg => {
|
105 | const parsedOption = parseOption(config, arg);
|
106 | if (!parsedOption) {
|
107 | if (state.activeOption) {
|
108 | state.optionArgs.push(arg);
|
109 | state.remainingArgs--;
|
110 | if (!state.remainingArgs || state.isMultiple) {
|
111 | const isMultiple = state.isMultiple;
|
112 | const partial = getStateAndReset(state);
|
113 | Object.entries(partial).forEach(([key, value]) => {
|
114 | if (isMultiple && result[key])
|
115 | partial[key] = result[key].concat(value);
|
116 | });
|
117 | Object.assign(result, partial);
|
118 | }
|
119 | }
|
120 | else {
|
121 | args.push(arg);
|
122 | }
|
123 | return;
|
124 | }
|
125 | parsedOption.forEach(option => {
|
126 | if (['h', 'help'].includes(option))
|
127 | throwError('');
|
128 | let subconfig = config[option];
|
129 | if (!subconfig) {
|
130 | throwError(`Unknown option: "${arg}"`);
|
131 | return;
|
132 | }
|
133 | if (typeof subconfig === 'boolean')
|
134 | subconfig = {};
|
135 | const isMultiple = !!subconfig.multiple;
|
136 | if (result[option] && !isMultiple)
|
137 | throwError(`Option "--${option}" provided many times`);
|
138 | let expectedArgsCount = subconfig.args;
|
139 | if (expectedArgsCount === '*')
|
140 | expectedArgsCount = Infinity;
|
141 | if (state.activeOption) {
|
142 | const partial = getStateAndReset(state);
|
143 | Object.entries(partial).forEach(([key, value]) => {
|
144 | if (!result[key])
|
145 | return;
|
146 | if (isMultiple)
|
147 | partial[key] = result[key].concat(value);
|
148 | });
|
149 | Object.assign(result, partial);
|
150 | }
|
151 | if (!expectedArgsCount && !isMultiple) {
|
152 | result[option] = [true];
|
153 | return;
|
154 | }
|
155 | Object.assign(state, {
|
156 | activeOption: option,
|
157 | remainingArgs: expectedArgsCount || 0,
|
158 | optionArgs: [],
|
159 | isMultiple,
|
160 | });
|
161 | if (!isMultiple)
|
162 | result[option] = [true];
|
163 | });
|
164 | });
|
165 | if (args.length)
|
166 | result[POSITIONAL_ARGS_KEY] = args;
|
167 | applyDefaults(config, result);
|
168 | checkRequiredParams(config, result);
|
169 | return postprocess(result);
|
170 | }
|
171 | function getHelpMessage(config, programName) {
|
172 | const strLines = [
|
173 | 'USAGE: node ' + programName + ' [OPTION1] [OPTION2]... arg1 arg2...',
|
174 | 'The following options are supported:',
|
175 | ];
|
176 | const lines = [];
|
177 | Object.entries(config).forEach(([key, value]) => {
|
178 | if (key === '_meta_')
|
179 | return;
|
180 | if (typeof value !== 'object' || !value) {
|
181 | lines.push([' --' + key, '']);
|
182 | return;
|
183 | }
|
184 | let ops = ' ';
|
185 | if (value.multiple)
|
186 | value.args = 1;
|
187 | const argsCount = value.args || 0;
|
188 | if (value.args === '*') {
|
189 | ops += '<ARG1>...<ARGN>';
|
190 | }
|
191 | else {
|
192 | for (let i = 0; i < argsCount; i++) {
|
193 | ops += '<ARG' + (i + 1) + '> ';
|
194 | }
|
195 | }
|
196 | lines.push([
|
197 | ' ' + (value.key ? '-' + value.key + ', --' : '--') + key + ops,
|
198 | (value.description || '') +
|
199 | (value.mandatory || value.required ? ' (required)' : '') +
|
200 | (value.multiple ? ' (multiple)' : '') +
|
201 | (value.default ? ' ("' + value.default + '" by default)' : ''),
|
202 | ]);
|
203 | });
|
204 | const maxLength = lines.reduce((prev, current) => Math.max(current[0].length, prev), 0);
|
205 | const plainLines = lines.map(line => {
|
206 | const key = line[0];
|
207 | const message = line[1];
|
208 | const padding = new Array(maxLength - key.length + 1).join(' ');
|
209 | return (key + padding + '\t' + message).trimRight();
|
210 | });
|
211 | return strLines.concat(plainLines).join('\n');
|
212 | }
|
213 | function preprocessCommand(command) {
|
214 | const parsed = [];
|
215 | command.forEach(item => {
|
216 | if (/^--?[a-zA-Z]+=/.test(item)) {
|
217 | const part1 = item.split('=')[0];
|
218 | const part2 = item.replace(part1 + '=', '');
|
219 | parsed.push(part1);
|
220 | parsed.push(part2);
|
221 | }
|
222 | else {
|
223 | parsed.push(item);
|
224 | }
|
225 | });
|
226 | return parsed;
|
227 | }
|
228 | function checkConfig(config) {
|
229 | if (config.help)
|
230 | throw new Error('"--help" option is reserved and cannot be declared in a getopt() call');
|
231 | Object.values(config).forEach(value => {
|
232 | if (!value || typeof value !== 'object') {
|
233 | console.warn('Boolean description of getopt() options is deprecated and will be ' +
|
234 | 'removed in a future "stdio" release. Please, use an object definitions instead.');
|
235 | return;
|
236 | }
|
237 | if (value.key === 'h')
|
238 | throw new Error('"-h" option is reserved and cannot be declared in a getopt() call');
|
239 | if (value.mandatory)
|
240 | console.warn('"mandatory" option is deprecated and will be removed in a ' +
|
241 | 'future "stdio" release. Please, use "required" instead.');
|
242 | });
|
243 | }
|
244 | exports.default = (config, command = process.argv, options) => {
|
245 | const { exitOnFailure = true, throwOnFailure = false, printOnFailure = true } = options || {};
|
246 | try {
|
247 | checkConfig(config);
|
248 | return getopt(config, preprocessCommand(command));
|
249 | }
|
250 | catch (error) {
|
251 | if (!error.message.startsWith(ERROR_PREFFIX)) {
|
252 | throw error;
|
253 | }
|
254 | const programName = command[1].split('/').pop() || 'program';
|
255 | const message = (error.message.replace(ERROR_PREFFIX, '') + '\n' + getHelpMessage(config, programName)).trim();
|
256 | if (printOnFailure)
|
257 | console.warn(message);
|
258 | if (exitOnFailure)
|
259 | process.exit(FAILURE);
|
260 | if (throwOnFailure)
|
261 | throw new Error(message);
|
262 | return null;
|
263 | }
|
264 | };
|