UNPKG

9.8 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const FAILURE = 1;
4const POSITIONAL_ARGS_KEY = 'args';
5const ERROR_PREFFIX = 'ERROR#GETOPT: ';
6function throwError(message) {
7 throw new Error(ERROR_PREFFIX + message);
8}
9function 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}
17function 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}
29function 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}
39function 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}
49function 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}
84function 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}
94function 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}
171function 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}
213function 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}
228function 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}
244exports.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};