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