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