1/** 2 * @license 3 * Copyright (c) 2016, Contributors 4 * SPDX-License-Identifier: ISC 5 */ 6import { tokenizeArgString } from './tokenize-arg-string.js'; 7import { DefaultValuesForTypeKey } from './yargs-parser-types.js'; 8import { camelCase, decamelize, looksLikeNumber } from './string-utils.js'; 9let mixin; 10export class YargsParser { 11 constructor(_mixin) { 12 mixin = _mixin; 13 } 14 parse(argsInput, options) { 15 const opts = Object.assign({ 16 alias: undefined, 17 array: undefined, 18 boolean: undefined, 19 config: undefined, 20 configObjects: undefined, 21 configuration: undefined, 22 coerce: undefined, 23 count: undefined, 24 default: undefined, 25 envPrefix: undefined, 26 narg: undefined, 27 normalize: undefined, 28 string: undefined, 29 number: undefined, 30 __: undefined, 31 key: undefined 32 }, options); 33 // allow a string argument to be passed in rather 34 // than an argv array. 35 const args = tokenizeArgString(argsInput); 36 // tokenizeArgString adds extra quotes to args if argsInput is a string 37 // only strip those extra quotes in processValue if argsInput is a string 38 const inputIsString = typeof argsInput === 'string'; 39 // aliases might have transitive relationships, normalize this. 40 const aliases = combineAliases(Object.assign(Object.create(null), opts.alias)); 41 const configuration = Object.assign({ 42 'boolean-negation': true, 43 'camel-case-expansion': true, 44 'combine-arrays': false, 45 'dot-notation': true, 46 'duplicate-arguments-array': true, 47 'flatten-duplicate-arrays': true, 48 'greedy-arrays': true, 49 'halt-at-non-option': false, 50 'nargs-eats-options': false, 51 'negation-prefix': 'no-', 52 'parse-numbers': true, 53 'parse-positional-numbers': true, 54 'populate--': false, 55 'set-placeholder-key': false, 56 'short-option-groups': true, 57 'strip-aliased': false, 58 'strip-dashed': false, 59 'unknown-options-as-args': false 60 }, opts.configuration); 61 const defaults = Object.assign(Object.create(null), opts.default); 62 const configObjects = opts.configObjects || []; 63 const envPrefix = opts.envPrefix; 64 const notFlagsOption = configuration['populate--']; 65 const notFlagsArgv = notFlagsOption ? '--' : '_'; 66 const newAliases = Object.create(null); 67 const defaulted = Object.create(null); 68 // allow a i18n handler to be passed in, default to a fake one (util.format). 69 const __ = opts.__ || mixin.format; 70 const flags = { 71 aliases: Object.create(null), 72 arrays: Object.create(null), 73 bools: Object.create(null), 74 strings: Object.create(null), 75 numbers: Object.create(null), 76 counts: Object.create(null), 77 normalize: Object.create(null), 78 configs: Object.create(null), 79 nargs: Object.create(null), 80 coercions: Object.create(null), 81 keys: [] 82 }; 83 const negative = /^-([0-9]+(\.[0-9]+)?|\.[0-9]+)$/; 84 const negatedBoolean = new RegExp('^--' + configuration['negation-prefix'] + '(.+)'); 85 [].concat(opts.array || []).filter(Boolean).forEach(function (opt) { 86 const key = typeof opt === 'object' ? opt.key : opt; 87 // assign to flags[bools|strings|numbers] 88 const assignment = Object.keys(opt).map(function (key) { 89 const arrayFlagKeys = { 90 boolean: 'bools', 91 string: 'strings', 92 number: 'numbers' 93 }; 94 return arrayFlagKeys[key]; 95 }).filter(Boolean).pop(); 96 // assign key to be coerced 97 if (assignment) { 98 flags[assignment][key] = true; 99 } 100 flags.arrays[key] = true; 101 flags.keys.push(key); 102 }); 103 [].concat(opts.boolean || []).filter(Boolean).forEach(function (key) { 104 flags.bools[key] = true; 105 flags.keys.push(key); 106 }); 107 [].concat(opts.string || []).filter(Boolean).forEach(function (key) { 108 flags.strings[key] = true; 109 flags.keys.push(key); 110 }); 111 [].concat(opts.number || []).filter(Boolean).forEach(function (key) { 112 flags.numbers[key] = true; 113 flags.keys.push(key); 114 }); 115 [].concat(opts.count || []).filter(Boolean).forEach(function (key) { 116 flags.counts[key] = true; 117 flags.keys.push(key); 118 }); 119 [].concat(opts.normalize || []).filter(Boolean).forEach(function (key) { 120 flags.normalize[key] = true; 121 flags.keys.push(key); 122 }); 123 if (typeof opts.narg === 'object') { 124 Object.entries(opts.narg).forEach(([key, value]) => { 125 if (typeof value === 'number') { 126 flags.nargs[key] = value; 127 flags.keys.push(key); 128 } 129 }); 130 } 131 if (typeof opts.coerce === 'object') { 132 Object.entries(opts.coerce).forEach(([key, value]) => { 133 if (typeof value === 'function') { 134 flags.coercions[key] = value; 135 flags.keys.push(key); 136 } 137 }); 138 } 139 if (typeof opts.config !== 'undefined') { 140 if (Array.isArray(opts.config) || typeof opts.config === 'string') { 141 ; 142 [].concat(opts.config).filter(Boolean).forEach(function (key) { 143 flags.configs[key] = true; 144 }); 145 } 146 else if (typeof opts.config === 'object') { 147 Object.entries(opts.config).forEach(([key, value]) => { 148 if (typeof value === 'boolean' || typeof value === 'function') { 149 flags.configs[key] = value; 150 } 151 }); 152 } 153 } 154 // create a lookup table that takes into account all 155 // combinations of aliases: {f: ['foo'], foo: ['f']} 156 extendAliases(opts.key, aliases, opts.default, flags.arrays); 157 // apply default values to all aliases. 158 Object.keys(defaults).forEach(function (key) { 159 (flags.aliases[key] || []).forEach(function (alias) { 160 defaults[alias] = defaults[key]; 161 }); 162 }); 163 let error = null; 164 checkConfiguration(); 165 let notFlags = []; 166 const argv = Object.assign(Object.create(null), { _: [] }); 167 // TODO(bcoe): for the first pass at removing object prototype we didn't 168 // remove all prototypes from objects returned by this API, we might want 169 // to gradually move towards doing so. 170 const argvReturn = {}; 171 for (let i = 0; i < args.length; i++) { 172 const arg = args[i]; 173 const truncatedArg = arg.replace(/^-{3,}/, '---'); 174 let broken; 175 let key; 176 let letters; 177 let m; 178 let next; 179 let value; 180 // any unknown option (except for end-of-options, "--") 181 if (arg !== '--' && /^-/.test(arg) && isUnknownOptionAsArg(arg)) { 182 pushPositional(arg); 183 // ---, ---=, ----, etc, 184 } 185 else if (truncatedArg.match(/^---+(=|$)/)) { 186 // options without key name are invalid. 187 pushPositional(arg); 188 continue; 189 // -- separated by = 190 } 191 else if (arg.match(/^--.+=/) || (!configuration['short-option-groups'] && arg.match(/^-.+=/))) { 192 // Using [\s\S] instead of . because js doesn't support the 193 // 'dotall' regex modifier. See: 194 // http://stackoverflow.com/a/1068308/13216 195 m = arg.match(/^--?([^=]+)=([\s\S]*)$/); 196 // arrays format = '--f=a b c' 197 if (m !== null && Array.isArray(m) && m.length >= 3) { 198 if (checkAllAliases(m[1], flags.arrays)) { 199 i = eatArray(i, m[1], args, m[2]); 200 } 201 else if (checkAllAliases(m[1], flags.nargs) !== false) { 202 // nargs format = '--f=monkey washing cat' 203 i = eatNargs(i, m[1], args, m[2]); 204 } 205 else { 206 setArg(m[1], m[2], true); 207 } 208 } 209 } 210 else if (arg.match(negatedBoolean) && configuration['boolean-negation']) { 211 m = arg.match(negatedBoolean); 212 if (m !== null && Array.isArray(m) && m.length >= 2) { 213 key = m[1]; 214 setArg(key, checkAllAliases(key, flags.arrays) ? [false] : false); 215 } 216 // -- separated by space. 217 } 218 else if (arg.match(/^--.+/) || (!configuration['short-option-groups'] && arg.match(/^-[^-]+/))) { 219 m = arg.match(/^--?(.+)/); 220 if (m !== null && Array.isArray(m) && m.length >= 2) { 221 key = m[1]; 222 if (checkAllAliases(key, flags.arrays)) { 223 // array format = '--foo a b c' 224 i = eatArray(i, key, args); 225 } 226 else if (checkAllAliases(key, flags.nargs) !== false) { 227 // nargs format = '--foo a b c' 228 // should be truthy even if: flags.nargs[key] === 0 229 i = eatNargs(i, key, args); 230 } 231 else { 232 next = args[i + 1]; 233 if (next !== undefined && (!next.match(/^-/) || 234 next.match(negative)) && 235 !checkAllAliases(key, flags.bools) && 236 !checkAllAliases(key, flags.counts)) { 237 setArg(key, next); 238 i++; 239 } 240 else if (/^(true|false)$/.test(next)) { 241 setArg(key, next); 242 i++; 243 } 244 else { 245 setArg(key, defaultValue(key)); 246 } 247 } 248 } 249 // dot-notation flag separated by '='. 250 } 251 else if (arg.match(/^-.\..+=/)) { 252 m = arg.match(/^-([^=]+)=([\s\S]*)$/); 253 if (m !== null && Array.isArray(m) && m.length >= 3) { 254 setArg(m[1], m[2]); 255 } 256 // dot-notation flag separated by space. 257 } 258 else if (arg.match(/^-.\..+/) && !arg.match(negative)) { 259 next = args[i + 1]; 260 m = arg.match(/^-(.\..+)/); 261 if (m !== null && Array.isArray(m) && m.length >= 2) { 262 key = m[1]; 263 if (next !== undefined && !next.match(/^-/) && 264 !checkAllAliases(key, flags.bools) && 265 !checkAllAliases(key, flags.counts)) { 266 setArg(key, next); 267 i++; 268 } 269 else { 270 setArg(key, defaultValue(key)); 271 } 272 } 273 } 274 else if (arg.match(/^-[^-]+/) && !arg.match(negative)) { 275 letters = arg.slice(1, -1).split(''); 276 broken = false; 277 for (let j = 0; j < letters.length; j++) { 278 next = arg.slice(j + 2); 279 if (letters[j + 1] && letters[j + 1] === '=') { 280 value = arg.slice(j + 3); 281 key = letters[j]; 282 if (checkAllAliases(key, flags.arrays)) { 283 // array format = '-f=a b c' 284 i = eatArray(i, key, args, value); 285 } 286 else if (checkAllAliases(key, flags.nargs) !== false) { 287 // nargs format = '-f=monkey washing cat' 288 i = eatNargs(i, key, args, value); 289 } 290 else { 291 setArg(key, value); 292 } 293 broken = true; 294 break; 295 } 296 if (next === '-') { 297 setArg(letters[j], next); 298 continue; 299 } 300 // current letter is an alphabetic character and next value is a number 301 if (/[A-Za-z]/.test(letters[j]) && 302 /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next) && 303 checkAllAliases(next, flags.bools) === false) { 304 setArg(letters[j], next); 305 broken = true; 306 break; 307 } 308 if (letters[j + 1] && letters[j + 1].match(/\W/)) { 309 setArg(letters[j], next); 310 broken = true; 311 break; 312 } 313 else { 314 setArg(letters[j], defaultValue(letters[j])); 315 } 316 } 317 key = arg.slice(-1)[0]; 318 if (!broken && key !== '-') { 319 if (checkAllAliases(key, flags.arrays)) { 320 // array format = '-f a b c' 321 i = eatArray(i, key, args); 322 } 323 else if (checkAllAliases(key, flags.nargs) !== false) { 324 // nargs format = '-f a b c' 325 // should be truthy even if: flags.nargs[key] === 0 326 i = eatNargs(i, key, args); 327 } 328 else { 329 next = args[i + 1]; 330 if (next !== undefined && (!/^(-|--)[^-]/.test(next) || 331 next.match(negative)) && 332 !checkAllAliases(key, flags.bools) && 333 !checkAllAliases(key, flags.counts)) { 334 setArg(key, next); 335 i++; 336 } 337 else if (/^(true|false)$/.test(next)) { 338 setArg(key, next); 339 i++; 340 } 341 else { 342 setArg(key, defaultValue(key)); 343 } 344 } 345 } 346 } 347 else if (arg.match(/^-[0-9]$/) && 348 arg.match(negative) && 349 checkAllAliases(arg.slice(1), flags.bools)) { 350 // single-digit boolean alias, e.g: xargs -0 351 key = arg.slice(1); 352 setArg(key, defaultValue(key)); 353 } 354 else if (arg === '--') { 355 notFlags = args.slice(i + 1); 356 break; 357 } 358 else if (configuration['halt-at-non-option']) { 359 notFlags = args.slice(i); 360 break; 361 } 362 else { 363 pushPositional(arg); 364 } 365 } 366 // order of precedence: 367 // 1. command line arg 368 // 2. value from env var 369 // 3. value from config file 370 // 4. value from config objects 371 // 5. configured default value 372 applyEnvVars(argv, true); // special case: check env vars that point to config file 373 applyEnvVars(argv, false); 374 setConfig(argv); 375 setConfigObjects(); 376 applyDefaultsAndAliases(argv, flags.aliases, defaults, true); 377 applyCoercions(argv); 378 if (configuration['set-placeholder-key']) 379 setPlaceholderKeys(argv); 380 // for any counts either not in args or without an explicit default, set to 0 381 Object.keys(flags.counts).forEach(function (key) { 382 if (!hasKey(argv, key.split('.'))) 383 setArg(key, 0); 384 }); 385 // '--' defaults to undefined. 386 if (notFlagsOption && notFlags.length) 387 argv[notFlagsArgv] = []; 388 notFlags.forEach(function (key) { 389 argv[notFlagsArgv].push(key); 390 }); 391 if (configuration['camel-case-expansion'] && configuration['strip-dashed']) { 392 Object.keys(argv).filter(key => key !== '--' && key.includes('-')).forEach(key => { 393 delete argv[key]; 394 }); 395 } 396 if (configuration['strip-aliased']) { 397 ; 398 [].concat(...Object.keys(aliases).map(k => aliases[k])).forEach(alias => { 399 if (configuration['camel-case-expansion'] && alias.includes('-')) { 400 delete argv[alias.split('.').map(prop => camelCase(prop)).join('.')]; 401 } 402 delete argv[alias]; 403 }); 404 } 405 // Push argument into positional array, applying numeric coercion: 406 function pushPositional(arg) { 407 const maybeCoercedNumber = maybeCoerceNumber('_', arg); 408 if (typeof maybeCoercedNumber === 'string' || typeof maybeCoercedNumber === 'number') { 409 argv._.push(maybeCoercedNumber); 410 } 411 } 412 // how many arguments should we consume, based 413 // on the nargs option? 414 function eatNargs(i, key, args, argAfterEqualSign) { 415 let ii; 416 let toEat = checkAllAliases(key, flags.nargs); 417 // NaN has a special meaning for the array type, indicating that one or 418 // more values are expected. 419 toEat = typeof toEat !== 'number' || isNaN(toEat) ? 1 : toEat; 420 if (toEat === 0) { 421 if (!isUndefined(argAfterEqualSign)) { 422 error = Error(__('Argument unexpected for: %s', key)); 423 } 424 setArg(key, defaultValue(key)); 425 return i; 426 } 427 let available = isUndefined(argAfterEqualSign) ? 0 : 1; 428 if (configuration['nargs-eats-options']) { 429 // classic behavior, yargs eats positional and dash arguments. 430 if (args.length - (i + 1) + available < toEat) { 431 error = Error(__('Not enough arguments following: %s', key)); 432 } 433 available = toEat; 434 } 435 else { 436 // nargs will not consume flag arguments, e.g., -abc, --foo, 437 // and terminates when one is observed. 438 for (ii = i + 1; ii < args.length; ii++) { 439 if (!args[ii].match(/^-[^0-9]/) || args[ii].match(negative) || isUnknownOptionAsArg(args[ii])) 440 available++; 441 else 442 break; 443 } 444 if (available < toEat) 445 error = Error(__('Not enough arguments following: %s', key)); 446 } 447 let consumed = Math.min(available, toEat); 448 if (!isUndefined(argAfterEqualSign) && consumed > 0) { 449 setArg(key, argAfterEqualSign); 450 consumed--; 451 } 452 for (ii = i + 1; ii < (consumed + i + 1); ii++) { 453 setArg(key, args[ii]); 454 } 455 return (i + consumed); 456 } 457 // if an option is an array, eat all non-hyphenated arguments 458 // following it... YUM! 459 // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"] 460 function eatArray(i, key, args, argAfterEqualSign) { 461 let argsToSet = []; 462 let next = argAfterEqualSign || args[i + 1]; 463 // If both array and nargs are configured, enforce the nargs count: 464 const nargsCount = checkAllAliases(key, flags.nargs); 465 if (checkAllAliases(key, flags.bools) && !(/^(true|false)$/.test(next))) { 466 argsToSet.push(true); 467 } 468 else if (isUndefined(next) || 469 (isUndefined(argAfterEqualSign) && /^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next))) { 470 // for keys without value ==> argsToSet remains an empty [] 471 // set user default value, if available 472 if (defaults[key] !== undefined) { 473 const defVal = defaults[key]; 474 argsToSet = Array.isArray(defVal) ? defVal : [defVal]; 475 } 476 } 477 else { 478 // value in --option=value is eaten as is 479 if (!isUndefined(argAfterEqualSign)) { 480 argsToSet.push(processValue(key, argAfterEqualSign, true)); 481 } 482 for (let ii = i + 1; ii < args.length; ii++) { 483 if ((!configuration['greedy-arrays'] && argsToSet.length > 0) || 484 (nargsCount && typeof nargsCount === 'number' && argsToSet.length >= nargsCount)) 485 break; 486 next = args[ii]; 487 if (/^-/.test(next) && !negative.test(next) && !isUnknownOptionAsArg(next)) 488 break; 489 i = ii; 490 argsToSet.push(processValue(key, next, inputIsString)); 491 } 492 } 493 // If both array and nargs are configured, create an error if less than 494 // nargs positionals were found. NaN has special meaning, indicating 495 // that at least one value is required (more are okay). 496 if (typeof nargsCount === 'number' && ((nargsCount && argsToSet.length < nargsCount) || 497 (isNaN(nargsCount) && argsToSet.length === 0))) { 498 error = Error(__('Not enough arguments following: %s', key)); 499 } 500 setArg(key, argsToSet); 501 return i; 502 } 503 function setArg(key, val, shouldStripQuotes = inputIsString) { 504 if (/-/.test(key) && configuration['camel-case-expansion']) { 505 const alias = key.split('.').map(function (prop) { 506 return camelCase(prop); 507 }).join('.'); 508 addNewAlias(key, alias); 509 } 510 const value = processValue(key, val, shouldStripQuotes); 511 const splitKey = key.split('.'); 512 setKey(argv, splitKey, value); 513 // handle populating aliases of the full key 514 if (flags.aliases[key]) { 515 flags.aliases[key].forEach(function (x) { 516 const keyProperties = x.split('.'); 517 setKey(argv, keyProperties, value); 518 }); 519 } 520 // handle populating aliases of the first element of the dot-notation key 521 if (splitKey.length > 1 && configuration['dot-notation']) { 522 ; 523 (flags.aliases[splitKey[0]] || []).forEach(function (x) { 524 let keyProperties = x.split('.'); 525 // expand alias with nested objects in key 526 const a = [].concat(splitKey); 527 a.shift(); // nuke the old key. 528 keyProperties = keyProperties.concat(a); 529 // populate alias only if is not already an alias of the full key 530 // (already populated above) 531 if (!(flags.aliases[key] || []).includes(keyProperties.join('.'))) { 532 setKey(argv, keyProperties, value); 533 } 534 }); 535 } 536 // Set normalize getter and setter when key is in 'normalize' but isn't an array 537 if (checkAllAliases(key, flags.normalize) && !checkAllAliases(key, flags.arrays)) { 538 const keys = [key].concat(flags.aliases[key] || []); 539 keys.forEach(function (key) { 540 Object.defineProperty(argvReturn, key, { 541 enumerable: true, 542 get() { 543 return val; 544 }, 545 set(value) { 546 val = typeof value === 'string' ? mixin.normalize(value) : value; 547 } 548 }); 549 }); 550 } 551 } 552 function addNewAlias(key, alias) { 553 if (!(flags.aliases[key] && flags.aliases[key].length)) { 554 flags.aliases[key] = [alias]; 555 newAliases[alias] = true; 556 } 557 if (!(flags.aliases[alias] && flags.aliases[alias].length)) { 558 addNewAlias(alias, key); 559 } 560 } 561 function processValue(key, val, shouldStripQuotes) { 562 // strings may be quoted, clean this up as we assign values. 563 if (shouldStripQuotes) { 564 val = stripQuotes(val); 565 } 566 // handle parsing boolean arguments --foo=true --bar false. 567 if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) { 568 if (typeof val === 'string') 569 val = val === 'true'; 570 } 571 let value = Array.isArray(val) 572 ? val.map(function (v) { return maybeCoerceNumber(key, v); }) 573 : maybeCoerceNumber(key, val); 574 // increment a count given as arg (either no value or value parsed as boolean) 575 if (checkAllAliases(key, flags.counts) && (isUndefined(value) || typeof value === 'boolean')) { 576 value = increment(); 577 } 578 // Set normalized value when key is in 'normalize' and in 'arrays' 579 if (checkAllAliases(key, flags.normalize) && checkAllAliases(key, flags.arrays)) { 580 if (Array.isArray(val)) 581 value = val.map((val) => { return mixin.normalize(val); }); 582 else 583 value = mixin.normalize(val); 584 } 585 return value; 586 } 587 function maybeCoerceNumber(key, value) { 588 if (!configuration['parse-positional-numbers'] && key === '_') 589 return value; 590 if (!checkAllAliases(key, flags.strings) && !checkAllAliases(key, flags.bools) && !Array.isArray(value)) { 591 const shouldCoerceNumber = looksLikeNumber(value) && configuration['parse-numbers'] && (Number.isSafeInteger(Math.floor(parseFloat(`${value}`)))); 592 if (shouldCoerceNumber || (!isUndefined(value) && checkAllAliases(key, flags.numbers))) { 593 value = Number(value); 594 } 595 } 596 return value; 597 } 598 // set args from config.json file, this should be 599 // applied last so that defaults can be applied. 600 function setConfig(argv) { 601 const configLookup = Object.create(null); 602 // expand defaults/aliases, in-case any happen to reference 603 // the config.json file. 604 applyDefaultsAndAliases(configLookup, flags.aliases, defaults); 605 Object.keys(flags.configs).forEach(function (configKey) { 606 const configPath = argv[configKey] || configLookup[configKey]; 607 if (configPath) { 608 try { 609 let config = null; 610 const resolvedConfigPath = mixin.resolve(mixin.cwd(), configPath); 611 const resolveConfig = flags.configs[configKey]; 612 if (typeof resolveConfig === 'function') { 613 try { 614 config = resolveConfig(resolvedConfigPath); 615 } 616 catch (e) { 617 config = e; 618 } 619 if (config instanceof Error) { 620 error = config; 621 return; 622 } 623 } 624 else { 625 config = mixin.require(resolvedConfigPath); 626 } 627 setConfigObject(config); 628 } 629 catch (ex) { 630 // Deno will receive a PermissionDenied error if an attempt is 631 // made to load config without the --allow-read flag: 632 if (ex.name === 'PermissionDenied') 633 error = ex; 634 else if (argv[configKey]) 635 error = Error(__('Invalid JSON config file: %s', configPath)); 636 } 637 } 638 }); 639 } 640 // set args from config object. 641 // it recursively checks nested objects. 642 function setConfigObject(config, prev) { 643 Object.keys(config).forEach(function (key) { 644 const value = config[key]; 645 const fullKey = prev ? prev + '.' + key : key; 646 // if the value is an inner object and we have dot-notation 647 // enabled, treat inner objects in config the same as 648 // heavily nested dot notations (foo.bar.apple). 649 if (typeof value === 'object' && value !== null && !Array.isArray(value) && configuration['dot-notation']) { 650 // if the value is an object but not an array, check nested object 651 setConfigObject(value, fullKey); 652 } 653 else { 654 // setting arguments via CLI takes precedence over 655 // values within the config file. 656 if (!hasKey(argv, fullKey.split('.')) || (checkAllAliases(fullKey, flags.arrays) && configuration['combine-arrays'])) { 657 setArg(fullKey, value); 658 } 659 } 660 }); 661 } 662 // set all config objects passed in opts 663 function setConfigObjects() { 664 if (typeof configObjects !== 'undefined') { 665 configObjects.forEach(function (configObject) { 666 setConfigObject(configObject); 667 }); 668 } 669 } 670 function applyEnvVars(argv, configOnly) { 671 if (typeof envPrefix === 'undefined') 672 return; 673 const prefix = typeof envPrefix === 'string' ? envPrefix : ''; 674 const env = mixin.env(); 675 Object.keys(env).forEach(function (envVar) { 676 if (prefix === '' || envVar.lastIndexOf(prefix, 0) === 0) { 677 // get array of nested keys and convert them to camel case 678 const keys = envVar.split('__').map(function (key, i) { 679 if (i === 0) { 680 key = key.substring(prefix.length); 681 } 682 return camelCase(key); 683 }); 684 if (((configOnly && flags.configs[keys.join('.')]) || !configOnly) && !hasKey(argv, keys)) { 685 setArg(keys.join('.'), env[envVar]); 686 } 687 } 688 }); 689 } 690 function applyCoercions(argv) { 691 let coerce; 692 const applied = new Set(); 693 Object.keys(argv).forEach(function (key) { 694 if (!applied.has(key)) { // If we haven't already coerced this option via one of its aliases 695 coerce = checkAllAliases(key, flags.coercions); 696 if (typeof coerce === 'function') { 697 try { 698 const value = maybeCoerceNumber(key, coerce(argv[key])); 699 ([].concat(flags.aliases[key] || [], key)).forEach(ali => { 700 applied.add(ali); 701 argv[ali] = value; 702 }); 703 } 704 catch (err) { 705 error = err; 706 } 707 } 708 } 709 }); 710 } 711 function setPlaceholderKeys(argv) { 712 flags.keys.forEach((key) => { 713 // don't set placeholder keys for dot notation options 'foo.bar'. 714 if (~key.indexOf('.')) 715 return; 716 if (typeof argv[key] === 'undefined') 717 argv[key] = undefined; 718 }); 719 return argv; 720 } 721 function applyDefaultsAndAliases(obj, aliases, defaults, canLog = false) { 722 Object.keys(defaults).forEach(function (key) { 723 if (!hasKey(obj, key.split('.'))) { 724 setKey(obj, key.split('.'), defaults[key]); 725 if (canLog) 726 defaulted[key] = true; 727 (aliases[key] || []).forEach(function (x) { 728 if (hasKey(obj, x.split('.'))) 729 return; 730 setKey(obj, x.split('.'), defaults[key]); 731 }); 732 } 733 }); 734 } 735 function hasKey(obj, keys) { 736 let o = obj; 737 if (!configuration['dot-notation']) 738 keys = [keys.join('.')]; 739 keys.slice(0, -1).forEach(function (key) { 740 o = (o[key] || {}); 741 }); 742 const key = keys[keys.length - 1]; 743 if (typeof o !== 'object') 744 return false; 745 else 746 return key in o; 747 } 748 function setKey(obj, keys, value) { 749 let o = obj; 750 if (!configuration['dot-notation']) 751 keys = [keys.join('.')]; 752 keys.slice(0, -1).forEach(function (key) { 753 // TODO(bcoe): in the next major version of yargs, switch to 754 // Object.create(null) for dot notation: 755 key = sanitizeKey(key); 756 if (typeof o === 'object' && o[key] === undefined) { 757 o[key] = {}; 758 } 759 if (typeof o[key] !== 'object' || Array.isArray(o[key])) { 760 // ensure that o[key] is an array, and that the last item is an empty object. 761 if (Array.isArray(o[key])) { 762 o[key].push({}); 763 } 764 else { 765 o[key] = [o[key], {}]; 766 } 767 // we want to update the empty object at the end of the o[key] array, so set o to that object 768 o = o[key][o[key].length - 1]; 769 } 770 else { 771 o = o[key]; 772 } 773 }); 774 // TODO(bcoe): in the next major version of yargs, switch to 775 // Object.create(null) for dot notation: 776 const key = sanitizeKey(keys[keys.length - 1]); 777 const isTypeArray = checkAllAliases(keys.join('.'), flags.arrays); 778 const isValueArray = Array.isArray(value); 779 let duplicate = configuration['duplicate-arguments-array']; 780 // nargs has higher priority than duplicate 781 if (!duplicate && checkAllAliases(key, flags.nargs)) { 782 duplicate = true; 783 if ((!isUndefined(o[key]) && flags.nargs[key] === 1) || (Array.isArray(o[key]) && o[key].length === flags.nargs[key])) { 784 o[key] = undefined; 785 } 786 } 787 if (value === increment()) { 788 o[key] = increment(o[key]); 789 } 790 else if (Array.isArray(o[key])) { 791 if (duplicate && isTypeArray && isValueArray) { 792 o[key] = configuration['flatten-duplicate-arrays'] ? o[key].concat(value) : (Array.isArray(o[key][0]) ? o[key] : [o[key]]).concat([value]); 793 } 794 else if (!duplicate && Boolean(isTypeArray) === Boolean(isValueArray)) { 795 o[key] = value; 796 } 797 else { 798 o[key] = o[key].concat([value]); 799 } 800 } 801 else if (o[key] === undefined && isTypeArray) { 802 o[key] = isValueArray ? value : [value]; 803 } 804 else if (duplicate && !(o[key] === undefined || 805 checkAllAliases(key, flags.counts) || 806 checkAllAliases(key, flags.bools))) { 807 o[key] = [o[key], value]; 808 } 809 else { 810 o[key] = value; 811 } 812 } 813 // extend the aliases list with inferred aliases. 814 function extendAliases(...args) { 815 args.forEach(function (obj) { 816 Object.keys(obj || {}).forEach(function (key) { 817 // short-circuit if we've already added a key 818 // to the aliases array, for example it might 819 // exist in both 'opts.default' and 'opts.key'. 820 if (flags.aliases[key]) 821 return; 822 flags.aliases[key] = [].concat(aliases[key] || []); 823 // For "--option-name", also set argv.optionName 824 flags.aliases[key].concat(key).forEach(function (x) { 825 if (/-/.test(x) && configuration['camel-case-expansion']) { 826 const c = camelCase(x); 827 if (c !== key && flags.aliases[key].indexOf(c) === -1) { 828 flags.aliases[key].push(c); 829 newAliases[c] = true; 830 } 831 } 832 }); 833 // For "--optionName", also set argv['option-name'] 834 flags.aliases[key].concat(key).forEach(function (x) { 835 if (x.length > 1 && /[A-Z]/.test(x) && configuration['camel-case-expansion']) { 836 const c = decamelize(x, '-'); 837 if (c !== key && flags.aliases[key].indexOf(c) === -1) { 838 flags.aliases[key].push(c); 839 newAliases[c] = true; 840 } 841 } 842 }); 843 flags.aliases[key].forEach(function (x) { 844 flags.aliases[x] = [key].concat(flags.aliases[key].filter(function (y) { 845 return x !== y; 846 })); 847 }); 848 }); 849 }); 850 } 851 function checkAllAliases(key, flag) { 852 const toCheck = [].concat(flags.aliases[key] || [], key); 853 const keys = Object.keys(flag); 854 const setAlias = toCheck.find(key => keys.includes(key)); 855 return setAlias ? flag[setAlias] : false; 856 } 857 function hasAnyFlag(key) { 858 const flagsKeys = Object.keys(flags); 859 const toCheck = [].concat(flagsKeys.map(k => flags[k])); 860 return toCheck.some(function (flag) { 861 return Array.isArray(flag) ? flag.includes(key) : flag[key]; 862 }); 863 } 864 function hasFlagsMatching(arg, ...patterns) { 865 const toCheck = [].concat(...patterns); 866 return toCheck.some(function (pattern) { 867 const match = arg.match(pattern); 868 return match && hasAnyFlag(match[1]); 869 }); 870 } 871 // based on a simplified version of the short flag group parsing logic 872 function hasAllShortFlags(arg) { 873 // if this is a negative number, or doesn't start with a single hyphen, it's not a short flag group 874 if (arg.match(negative) || !arg.match(/^-[^-]+/)) { 875 return false; 876 } 877 let hasAllFlags = true; 878 let next; 879 const letters = arg.slice(1).split(''); 880 for (let j = 0; j < letters.length; j++) { 881 next = arg.slice(j + 2); 882 if (!hasAnyFlag(letters[j])) { 883 hasAllFlags = false; 884 break; 885 } 886 if ((letters[j + 1] && letters[j + 1] === '=') || 887 next === '-' || 888 (/[A-Za-z]/.test(letters[j]) && /^-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) || 889 (letters[j + 1] && letters[j + 1].match(/\W/))) { 890 break; 891 } 892 } 893 return hasAllFlags; 894 } 895 function isUnknownOptionAsArg(arg) { 896 return configuration['unknown-options-as-args'] && isUnknownOption(arg); 897 } 898 function isUnknownOption(arg) { 899 arg = arg.replace(/^-{3,}/, '--'); 900 // ignore negative numbers 901 if (arg.match(negative)) { 902 return false; 903 } 904 // if this is a short option group and all of them are configured, it isn't unknown 905 if (hasAllShortFlags(arg)) { 906 return false; 907 } 908 // e.g. '--count=2' 909 const flagWithEquals = /^-+([^=]+?)=[\s\S]*$/; 910 // e.g. '-a' or '--arg' 911 const normalFlag = /^-+([^=]+?)$/; 912 // e.g. '-a-' 913 const flagEndingInHyphen = /^-+([^=]+?)-$/; 914 // e.g. '-abc123' 915 const flagEndingInDigits = /^-+([^=]+?\d+)$/; 916 // e.g. '-a/usr/local' 917 const flagEndingInNonWordCharacters = /^-+([^=]+?)\W+.*$/; 918 // check the different types of flag styles, including negatedBoolean, a pattern defined near the start of the parse method 919 return !hasFlagsMatching(arg, flagWithEquals, negatedBoolean, normalFlag, flagEndingInHyphen, flagEndingInDigits, flagEndingInNonWordCharacters); 920 } 921 // make a best effort to pick a default value 922 // for an option based on name and type. 923 function defaultValue(key) { 924 if (!checkAllAliases(key, flags.bools) && 925 !checkAllAliases(key, flags.counts) && 926 `${key}` in defaults) { 927 return defaults[key]; 928 } 929 else { 930 return defaultForType(guessType(key)); 931 } 932 } 933 // return a default value, given the type of a flag., 934 function defaultForType(type) { 935 const def = { 936 [DefaultValuesForTypeKey.BOOLEAN]: true, 937 [DefaultValuesForTypeKey.STRING]: '', 938 [DefaultValuesForTypeKey.NUMBER]: undefined, 939 [DefaultValuesForTypeKey.ARRAY]: [] 940 }; 941 return def[type]; 942 } 943 // given a flag, enforce a default type. 944 function guessType(key) { 945 let type = DefaultValuesForTypeKey.BOOLEAN; 946 if (checkAllAliases(key, flags.strings)) 947 type = DefaultValuesForTypeKey.STRING; 948 else if (checkAllAliases(key, flags.numbers)) 949 type = DefaultValuesForTypeKey.NUMBER; 950 else if (checkAllAliases(key, flags.bools)) 951 type = DefaultValuesForTypeKey.BOOLEAN; 952 else if (checkAllAliases(key, flags.arrays)) 953 type = DefaultValuesForTypeKey.ARRAY; 954 return type; 955 } 956 function isUndefined(num) { 957 return num === undefined; 958 } 959 // check user configuration settings for inconsistencies 960 function checkConfiguration() { 961 // count keys should not be set as array/narg 962 Object.keys(flags.counts).find(key => { 963 if (checkAllAliases(key, flags.arrays)) { 964 error = Error(__('Invalid configuration: %s, opts.count excludes opts.array.', key)); 965 return true; 966 } 967 else if (checkAllAliases(key, flags.nargs)) { 968 error = Error(__('Invalid configuration: %s, opts.count excludes opts.narg.', key)); 969 return true; 970 } 971 return false; 972 }); 973 } 974 return { 975 aliases: Object.assign({}, flags.aliases), 976 argv: Object.assign(argvReturn, argv), 977 configuration: configuration, 978 defaulted: Object.assign({}, defaulted), 979 error: error, 980 newAliases: Object.assign({}, newAliases) 981 }; 982 } 983} 984// if any aliases reference each other, we should 985// merge them together. 986function combineAliases(aliases) { 987 const aliasArrays = []; 988 const combined = Object.create(null); 989 let change = true; 990 // turn alias lookup hash {key: ['alias1', 'alias2']} into 991 // a simple array ['key', 'alias1', 'alias2'] 992 Object.keys(aliases).forEach(function (key) { 993 aliasArrays.push([].concat(aliases[key], key)); 994 }); 995 // combine arrays until zero changes are 996 // made in an iteration. 997 while (change) { 998 change = false; 999 for (let i = 0; i < aliasArrays.length; i++) { 1000 for (let ii = i + 1; ii < aliasArrays.length; ii++) { 1001 const intersect = aliasArrays[i].filter(function (v) { 1002 return aliasArrays[ii].indexOf(v) !== -1; 1003 }); 1004 if (intersect.length) { 1005 aliasArrays[i] = aliasArrays[i].concat(aliasArrays[ii]); 1006 aliasArrays.splice(ii, 1); 1007 change = true; 1008 break; 1009 } 1010 } 1011 } 1012 } 1013 // map arrays back to the hash-lookup (de-dupe while 1014 // we're at it). 1015 aliasArrays.forEach(function (aliasArray) { 1016 aliasArray = aliasArray.filter(function (v, i, self) { 1017 return self.indexOf(v) === i; 1018 }); 1019 const lastAlias = aliasArray.pop(); 1020 if (lastAlias !== undefined && typeof lastAlias === 'string') { 1021 combined[lastAlias] = aliasArray; 1022 } 1023 }); 1024 return combined; 1025} 1026// this function should only be called when a count is given as an arg 1027// it is NOT called to set a default value 1028// thus we can start the count at 1 instead of 0 1029function increment(orig) { 1030 return orig !== undefined ? orig + 1 : 1; 1031} 1032// TODO(bcoe): in the next major version of yargs, switch to 1033// Object.create(null) for dot notation: 1034function sanitizeKey(key) { 1035 if (key === '__proto__') 1036 return '___proto___'; 1037 return key; 1038} 1039function stripQuotes(val) { 1040 return (typeof val === 'string' && 1041 (val[0] === "'" || val[0] === '"') && 1042 val[val.length - 1] === val[0]) 1043 ? val.substring(1, val.length - 1) 1044 : val; 1045} 1046