1'use strict'; 2 3const { 4 ArrayPrototypeForEach, 5 ArrayPrototypeIncludes, 6 ArrayPrototypeMap, 7 ArrayPrototypePush, 8 ArrayPrototypePushApply, 9 ArrayPrototypeShift, 10 ArrayPrototypeSlice, 11 ArrayPrototypeUnshiftApply, 12 ObjectEntries, 13 ObjectPrototypeHasOwnProperty: ObjectHasOwn, 14 StringPrototypeCharAt, 15 StringPrototypeIndexOf, 16 StringPrototypeSlice, 17 StringPrototypeStartsWith, 18} = require('./internal/primordials'); 19 20const { 21 validateArray, 22 validateBoolean, 23 validateBooleanArray, 24 validateObject, 25 validateString, 26 validateStringArray, 27 validateUnion, 28} = require('./internal/validators'); 29 30const { 31 kEmptyObject, 32} = require('./internal/util'); 33 34const { 35 findLongOptionForShort, 36 isLoneLongOption, 37 isLoneShortOption, 38 isLongOptionAndValue, 39 isOptionValue, 40 isOptionLikeValue, 41 isShortOptionAndValue, 42 isShortOptionGroup, 43 useDefaultValueOption, 44 objectGetOwn, 45 optionsGetOwn, 46} = require('./utils'); 47 48const { 49 codes: { 50 ERR_INVALID_ARG_VALUE, 51 ERR_PARSE_ARGS_INVALID_OPTION_VALUE, 52 ERR_PARSE_ARGS_UNKNOWN_OPTION, 53 ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL, 54 }, 55} = require('./internal/errors'); 56 57function getMainArgs() { 58 // Work out where to slice process.argv for user supplied arguments. 59 60 // Check node options for scenarios where user CLI args follow executable. 61 const execArgv = process.execArgv; 62 if (ArrayPrototypeIncludes(execArgv, '-e') || 63 ArrayPrototypeIncludes(execArgv, '--eval') || 64 ArrayPrototypeIncludes(execArgv, '-p') || 65 ArrayPrototypeIncludes(execArgv, '--print')) { 66 return ArrayPrototypeSlice(process.argv, 1); 67 } 68 69 // Normally first two arguments are executable and script, then CLI arguments 70 return ArrayPrototypeSlice(process.argv, 2); 71} 72 73/** 74 * In strict mode, throw for possible usage errors like --foo --bar 75 * 76 * @param {object} token - from tokens as available from parseArgs 77 */ 78function checkOptionLikeValue(token) { 79 if (!token.inlineValue && isOptionLikeValue(token.value)) { 80 // Only show short example if user used short option. 81 const example = StringPrototypeStartsWith(token.rawName, '--') ? 82 `'${token.rawName}=-XYZ'` : 83 `'--${token.name}=-XYZ' or '${token.rawName}-XYZ'`; 84 const errorMessage = `Option '${token.rawName}' argument is ambiguous. 85Did you forget to specify the option argument for '${token.rawName}'? 86To specify an option argument starting with a dash use ${example}.`; 87 throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(errorMessage); 88 } 89} 90 91/** 92 * In strict mode, throw for usage errors. 93 * 94 * @param {object} config - from config passed to parseArgs 95 * @param {object} token - from tokens as available from parseArgs 96 */ 97function checkOptionUsage(config, token) { 98 if (!ObjectHasOwn(config.options, token.name)) { 99 throw new ERR_PARSE_ARGS_UNKNOWN_OPTION( 100 token.rawName, config.allowPositionals); 101 } 102 103 const short = optionsGetOwn(config.options, token.name, 'short'); 104 const shortAndLong = `${short ? `-${short}, ` : ''}--${token.name}`; 105 const type = optionsGetOwn(config.options, token.name, 'type'); 106 if (type === 'string' && typeof token.value !== 'string') { 107 throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong} <value>' argument missing`); 108 } 109 // (Idiomatic test for undefined||null, expecting undefined.) 110 if (type === 'boolean' && token.value != null) { 111 throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortAndLong}' does not take an argument`); 112 } 113} 114 115 116/** 117 * Store the option value in `values`. 118 * 119 * @param {string} longOption - long option name e.g. 'foo' 120 * @param {string|undefined} optionValue - value from user args 121 * @param {object} options - option configs, from parseArgs({ options }) 122 * @param {object} values - option values returned in `values` by parseArgs 123 */ 124function storeOption(longOption, optionValue, options, values) { 125 if (longOption === '__proto__') { 126 return; // No. Just no. 127 } 128 129 // We store based on the option value rather than option type, 130 // preserving the users intent for author to deal with. 131 const newValue = optionValue ?? true; 132 if (optionsGetOwn(options, longOption, 'multiple')) { 133 // Always store value in array, including for boolean. 134 // values[longOption] starts out not present, 135 // first value is added as new array [newValue], 136 // subsequent values are pushed to existing array. 137 // (note: values has null prototype, so simpler usage) 138 if (values[longOption]) { 139 ArrayPrototypePush(values[longOption], newValue); 140 } else { 141 values[longOption] = [newValue]; 142 } 143 } else { 144 values[longOption] = newValue; 145 } 146} 147 148/** 149 * Store the default option value in `values`. 150 * 151 * @param {string} longOption - long option name e.g. 'foo' 152 * @param {string 153 * | boolean 154 * | string[] 155 * | boolean[]} optionValue - default value from option config 156 * @param {object} values - option values returned in `values` by parseArgs 157 */ 158function storeDefaultOption(longOption, optionValue, values) { 159 if (longOption === '__proto__') { 160 return; // No. Just no. 161 } 162 163 values[longOption] = optionValue; 164} 165 166/** 167 * Process args and turn into identified tokens: 168 * - option (along with value, if any) 169 * - positional 170 * - option-terminator 171 * 172 * @param {string[]} args - from parseArgs({ args }) or mainArgs 173 * @param {object} options - option configs, from parseArgs({ options }) 174 */ 175function argsToTokens(args, options) { 176 const tokens = []; 177 let index = -1; 178 let groupCount = 0; 179 180 const remainingArgs = ArrayPrototypeSlice(args); 181 while (remainingArgs.length > 0) { 182 const arg = ArrayPrototypeShift(remainingArgs); 183 const nextArg = remainingArgs[0]; 184 if (groupCount > 0) 185 groupCount--; 186 else 187 index++; 188 189 // Check if `arg` is an options terminator. 190 // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html 191 if (arg === '--') { 192 // Everything after a bare '--' is considered a positional argument. 193 ArrayPrototypePush(tokens, { kind: 'option-terminator', index }); 194 ArrayPrototypePushApply( 195 tokens, ArrayPrototypeMap(remainingArgs, (arg) => { 196 return { kind: 'positional', index: ++index, value: arg }; 197 }) 198 ); 199 break; // Finished processing args, leave while loop. 200 } 201 202 if (isLoneShortOption(arg)) { 203 // e.g. '-f' 204 const shortOption = StringPrototypeCharAt(arg, 1); 205 const longOption = findLongOptionForShort(shortOption, options); 206 let value; 207 let inlineValue; 208 if (optionsGetOwn(options, longOption, 'type') === 'string' && 209 isOptionValue(nextArg)) { 210 // e.g. '-f', 'bar' 211 value = ArrayPrototypeShift(remainingArgs); 212 inlineValue = false; 213 } 214 ArrayPrototypePush( 215 tokens, 216 { kind: 'option', name: longOption, rawName: arg, 217 index, value, inlineValue }); 218 if (value != null) ++index; 219 continue; 220 } 221 222 if (isShortOptionGroup(arg, options)) { 223 // Expand -fXzy to -f -X -z -y 224 const expanded = []; 225 for (let index = 1; index < arg.length; index++) { 226 const shortOption = StringPrototypeCharAt(arg, index); 227 const longOption = findLongOptionForShort(shortOption, options); 228 if (optionsGetOwn(options, longOption, 'type') !== 'string' || 229 index === arg.length - 1) { 230 // Boolean option, or last short in group. Well formed. 231 ArrayPrototypePush(expanded, `-${shortOption}`); 232 } else { 233 // String option in middle. Yuck. 234 // Expand -abfFILE to -a -b -fFILE 235 ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`); 236 break; // finished short group 237 } 238 } 239 ArrayPrototypeUnshiftApply(remainingArgs, expanded); 240 groupCount = expanded.length; 241 continue; 242 } 243 244 if (isShortOptionAndValue(arg, options)) { 245 // e.g. -fFILE 246 const shortOption = StringPrototypeCharAt(arg, 1); 247 const longOption = findLongOptionForShort(shortOption, options); 248 const value = StringPrototypeSlice(arg, 2); 249 ArrayPrototypePush( 250 tokens, 251 { kind: 'option', name: longOption, rawName: `-${shortOption}`, 252 index, value, inlineValue: true }); 253 continue; 254 } 255 256 if (isLoneLongOption(arg)) { 257 // e.g. '--foo' 258 const longOption = StringPrototypeSlice(arg, 2); 259 let value; 260 let inlineValue; 261 if (optionsGetOwn(options, longOption, 'type') === 'string' && 262 isOptionValue(nextArg)) { 263 // e.g. '--foo', 'bar' 264 value = ArrayPrototypeShift(remainingArgs); 265 inlineValue = false; 266 } 267 ArrayPrototypePush( 268 tokens, 269 { kind: 'option', name: longOption, rawName: arg, 270 index, value, inlineValue }); 271 if (value != null) ++index; 272 continue; 273 } 274 275 if (isLongOptionAndValue(arg)) { 276 // e.g. --foo=bar 277 const equalIndex = StringPrototypeIndexOf(arg, '='); 278 const longOption = StringPrototypeSlice(arg, 2, equalIndex); 279 const value = StringPrototypeSlice(arg, equalIndex + 1); 280 ArrayPrototypePush( 281 tokens, 282 { kind: 'option', name: longOption, rawName: `--${longOption}`, 283 index, value, inlineValue: true }); 284 continue; 285 } 286 287 ArrayPrototypePush(tokens, { kind: 'positional', index, value: arg }); 288 } 289 290 return tokens; 291} 292 293const parseArgs = (config = kEmptyObject) => { 294 const args = objectGetOwn(config, 'args') ?? getMainArgs(); 295 const strict = objectGetOwn(config, 'strict') ?? true; 296 const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict; 297 const returnTokens = objectGetOwn(config, 'tokens') ?? false; 298 const options = objectGetOwn(config, 'options') ?? { __proto__: null }; 299 // Bundle these up for passing to strict-mode checks. 300 const parseConfig = { args, strict, options, allowPositionals }; 301 302 // Validate input configuration. 303 validateArray(args, 'args'); 304 validateBoolean(strict, 'strict'); 305 validateBoolean(allowPositionals, 'allowPositionals'); 306 validateBoolean(returnTokens, 'tokens'); 307 validateObject(options, 'options'); 308 ArrayPrototypeForEach( 309 ObjectEntries(options), 310 ({ 0: longOption, 1: optionConfig }) => { 311 validateObject(optionConfig, `options.${longOption}`); 312 313 // type is required 314 const optionType = objectGetOwn(optionConfig, 'type'); 315 validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']); 316 317 if (ObjectHasOwn(optionConfig, 'short')) { 318 const shortOption = optionConfig.short; 319 validateString(shortOption, `options.${longOption}.short`); 320 if (shortOption.length !== 1) { 321 throw new ERR_INVALID_ARG_VALUE( 322 `options.${longOption}.short`, 323 shortOption, 324 'must be a single character' 325 ); 326 } 327 } 328 329 const multipleOption = objectGetOwn(optionConfig, 'multiple'); 330 if (ObjectHasOwn(optionConfig, 'multiple')) { 331 validateBoolean(multipleOption, `options.${longOption}.multiple`); 332 } 333 334 const defaultValue = objectGetOwn(optionConfig, 'default'); 335 if (defaultValue !== undefined) { 336 let validator; 337 switch (optionType) { 338 case 'string': 339 validator = multipleOption ? validateStringArray : validateString; 340 break; 341 342 case 'boolean': 343 validator = multipleOption ? validateBooleanArray : validateBoolean; 344 break; 345 } 346 validator(defaultValue, `options.${longOption}.default`); 347 } 348 } 349 ); 350 351 // Phase 1: identify tokens 352 const tokens = argsToTokens(args, options); 353 354 // Phase 2: process tokens into parsed option values and positionals 355 const result = { 356 values: { __proto__: null }, 357 positionals: [], 358 }; 359 if (returnTokens) { 360 result.tokens = tokens; 361 } 362 ArrayPrototypeForEach(tokens, (token) => { 363 if (token.kind === 'option') { 364 if (strict) { 365 checkOptionUsage(parseConfig, token); 366 checkOptionLikeValue(token); 367 } 368 storeOption(token.name, token.value, options, result.values); 369 } else if (token.kind === 'positional') { 370 if (!allowPositionals) { 371 throw new ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL(token.value); 372 } 373 ArrayPrototypePush(result.positionals, token.value); 374 } 375 }); 376 377 // Phase 3: fill in default values for missing args 378 ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 379 1: optionConfig }) => { 380 const mustSetDefault = useDefaultValueOption(longOption, 381 optionConfig, 382 result.values); 383 if (mustSetDefault) { 384 storeDefaultOption(longOption, 385 objectGetOwn(optionConfig, 'default'), 386 result.values); 387 } 388 }); 389 390 391 return result; 392}; 393 394module.exports = { 395 parseArgs, 396}; 397