import camelCase from 'lodash.camelcase'; /** * Takes any input and guarantees an array back. * * - converts array-like objects (e.g. `arguments`) to a real array * - converts `undefined` to an empty array * - converts any another other, singular value (including `null`) into an array containing that value * - ignores input which is already an array * * @module array-back * @example * > const arrayify = require('array-back') * * > arrayify(undefined) * [] * * > arrayify(null) * [ null ] * * > arrayify(0) * [ 0 ] * * > arrayify([ 1, 2 ]) * [ 1, 2 ] * * > function f(){ return arrayify(arguments); } * > f(1,2,3) * [ 1, 2, 3 ] */ function isObject (input) { return typeof input === 'object' && input !== null } function isArrayLike (input) { return isObject(input) && typeof input.length === 'number' } /** * @param {*} - the input value to convert to an array * @returns {Array} * @alias module:array-back */ function arrayify (input) { if (Array.isArray(input)) { return input } else { if (input === undefined) { return [] } else if (isArrayLike(input)) { return Array.prototype.slice.call(input) } else { return [ input ] } } } /** * Takes any input and guarantees an array back. * * - converts array-like objects (e.g. `arguments`) to a real array * - converts `undefined` to an empty array * - converts any another other, singular value (including `null`) into an array containing that value * - ignores input which is already an array * * @module array-back * @example * > const arrayify = require('array-back') * * > arrayify(undefined) * [] * * > arrayify(null) * [ null ] * * > arrayify(0) * [ 0 ] * * > arrayify([ 1, 2 ]) * [ 1, 2 ] * * > function f(){ return arrayify(arguments); } * > f(1,2,3) * [ 1, 2, 3 ] */ function isObject$1 (input) { return typeof input === 'object' && input !== null } function isArrayLike$1 (input) { return isObject$1(input) && typeof input.length === 'number' } /** * @param {*} - the input value to convert to an array * @returns {Array} * @alias module:array-back */ function arrayify$1 (input) { if (Array.isArray(input)) { return input } else { if (input === undefined) { return [] } else if (isArrayLike$1(input)) { return Array.prototype.slice.call(input) } else { return [ input ] } } } /** * Find and either replace or remove items in an array. * * @module find-replace * @example * > const findReplace = require('find-replace') * > const numbers = [ 1, 2, 3] * * > findReplace(numbers, n => n === 2, 'two') * [ 1, 'two', 3 ] * * > findReplace(numbers, n => n === 2, [ 'two', 'zwei' ]) * [ 1, [ 'two', 'zwei' ], 3 ] * * > findReplace(numbers, n => n === 2, 'two', 'zwei') * [ 1, 'two', 'zwei', 3 ] * * > findReplace(numbers, n => n === 2) // no replacement, so remove * [ 1, 3 ] */ /** * @param {array} - The input array * @param {testFn} - A predicate function which, if returning `true` causes the current item to be operated on. * @param [replaceWith] {...any} - If specified, found values will be replaced with these values, else removed. * @returns {array} * @alias module:find-replace */ function findReplace (array, testFn) { const found = []; const replaceWiths = arrayify$1(arguments); replaceWiths.splice(0, 2); arrayify$1(array).forEach((value, index) => { let expanded = []; replaceWiths.forEach(replaceWith => { if (typeof replaceWith === 'function') { expanded = expanded.concat(replaceWith(value)); } else { expanded.push(replaceWith); } }); if (testFn(value)) { found.push({ index: index, replaceWithValue: expanded }); } }); found.reverse().forEach(item => { const spliceArgs = [ item.index, 1 ].concat(item.replaceWithValue); array.splice.apply(array, spliceArgs); }); return array } /** * Some useful tools for working with `process.argv`. * * @module argv-tools * @typicalName argvTools * @example * const argvTools = require('argv-tools') */ /** * Regular expressions for matching option formats. * @static */ const re = { short: /^-([^\d-])$/, long: /^--(\S+)/, combinedShort: /^-[^\d-]{2,}$/, optEquals: /^(--\S+?)=(.*)/ }; /** * Array subclass encapsulating common operations on `process.argv`. * @static */ class ArgvArray extends Array { /** * Clears the array has loads the supplied input. * @param {string[]} argv - The argv list to load. Defaults to `process.argv`. */ load (argv) { this.clear(); if (argv && argv !== process.argv) { argv = arrayify(argv); } else { /* if no argv supplied, assume we are parsing process.argv */ argv = process.argv.slice(0); const deleteCount = process.execArgv.some(isExecArg) ? 1 : 2; argv.splice(0, deleteCount); } argv.forEach(arg => this.push(String(arg))); } /** * Clear the array. */ clear () { this.length = 0; } /** * expand ``--option=value` style args. */ expandOptionEqualsNotation () { if (this.some(arg => re.optEquals.test(arg))) { const expandedArgs = []; this.forEach(arg => { const matches = arg.match(re.optEquals); if (matches) { expandedArgs.push(matches[1], matches[2]); } else { expandedArgs.push(arg); } }); this.clear(); this.load(expandedArgs); } } /** * expand getopt-style combinedShort options. */ expandGetoptNotation () { if (this.hasCombinedShortOptions()) { findReplace(this, re.combinedShort, expandCombinedShortArg); } } /** * Returns true if the array contains combined short options (e.g. `-ab`). * @returns {boolean} */ hasCombinedShortOptions () { return this.some(arg => re.combinedShort.test(arg)) } static from (argv) { const result = new this(); result.load(argv); return result } } /** * Expand a combined short option. * @param {string} - the string to expand, e.g. `-ab` * @returns {string[]} * @static */ function expandCombinedShortArg (arg) { /* remove initial hypen */ arg = arg.slice(1); return arg.split('').map(letter => '-' + letter) } /** * Returns true if the supplied arg matches `--option=value` notation. * @param {string} - the arg to test, e.g. `--one=something` * @returns {boolean} * @static */ function isOptionEqualsNotation (arg) { return re.optEquals.test(arg) } /** * Returns true if the supplied arg is in either long (`--one`) or short (`-o`) format. * @param {string} - the arg to test, e.g. `--one` * @returns {boolean} * @static */ function isOption (arg) { return (re.short.test(arg) || re.long.test(arg)) && !re.optEquals.test(arg) } /** * Returns true if the supplied arg is in long (`--one`) format. * @param {string} - the arg to test, e.g. `--one` * @returns {boolean} * @static */ function isLongOption (arg) { return re.long.test(arg) && !isOptionEqualsNotation(arg) } /** * Returns the name from a long, short or `--options=value` arg. * @param {string} - the arg to inspect, e.g. `--one` * @returns {string} * @static */ function getOptionName (arg) { if (re.short.test(arg)) { return arg.match(re.short)[1] } else if (isLongOption(arg)) { return arg.match(re.long)[1] } else if (isOptionEqualsNotation(arg)) { return arg.match(re.optEquals)[1].replace(/^--/, '') } else { return null } } function isValue (arg) { return !(isOption(arg) || re.combinedShort.test(arg) || re.optEquals.test(arg)) } function isExecArg (arg) { return ['--eval', '-e'].indexOf(arg) > -1 || arg.startsWith('--eval=') } /** * For type-checking Javascript values. * @module typical * @typicalname t * @example * const t = require('typical') */ /** * Returns true if input is a number * @param {*} - the input to test * @returns {boolean} * @static * @example * > t.isNumber(0) * true * > t.isNumber(1) * true * > t.isNumber(1.1) * true * > t.isNumber(0xff) * true * > t.isNumber(0644) * true * > t.isNumber(6.2e5) * true * > t.isNumber(NaN) * false * > t.isNumber(Infinity) * false */ function isNumber (n) { return !isNaN(parseFloat(n)) && isFinite(n) } /** * A plain object is a simple object literal, it is not an instance of a class. Returns true if the input `typeof` is `object` and directly decends from `Object`. * * @param {*} - the input to test * @returns {boolean} * @static * @example * > t.isPlainObject({ something: 'one' }) * true * > t.isPlainObject(new Date()) * false * > t.isPlainObject([ 0, 1 ]) * false * > t.isPlainObject(/test/) * false * > t.isPlainObject(1) * false * > t.isPlainObject('one') * false * > t.isPlainObject(null) * false * > t.isPlainObject((function * () {})()) * false * > t.isPlainObject(function * () {}) * false */ function isPlainObject (input) { return input !== null && typeof input === 'object' && input.constructor === Object } /** * An array-like value has all the properties of an array, but is not an array instance. Examples in the `arguments` object. Returns true if the input value is an object, not null and has a `length` property with a numeric value. * * @param {*} - the input to test * @returns {boolean} * @static * @example * function sum(x, y){ * console.log(t.isArrayLike(arguments)) * // prints `true` * } */ function isArrayLike$2 (input) { return isObject$2(input) && typeof input.length === 'number' } /** * returns true if the typeof input is `'object'`, but not null! * @param {*} - the input to test * @returns {boolean} * @static */ function isObject$2 (input) { return typeof input === 'object' && input !== null } /** * Returns true if the input value is defined * @param {*} - the input to test * @returns {boolean} * @static */ function isDefined (input) { return typeof input !== 'undefined' } /** * Returns true if the input value is a string * @param {*} - the input to test * @returns {boolean} * @static */ function isString (input) { return typeof input === 'string' } /** * Returns true if the input value is a boolean * @param {*} - the input to test * @returns {boolean} * @static */ function isBoolean (input) { return typeof input === 'boolean' } /** * Returns true if the input value is a function * @param {*} - the input to test * @returns {boolean} * @static */ function isFunction (input) { return typeof input === 'function' } /** * Returns true if the input value is an es2015 `class`. * @param {*} - the input to test * @returns {boolean} * @static */ function isClass (input) { if (isFunction(input)) { return /^class /.test(Function.prototype.toString.call(input)) } else { return false } } /** * Returns true if the input is a string, number, symbol, boolean, null or undefined value. * @param {*} - the input to test * @returns {boolean} * @static */ function isPrimitive (input) { if (input === null) return true switch (typeof input) { case 'string': case 'number': case 'symbol': case 'undefined': case 'boolean': return true default: return false } } /** * Returns true if the input is a Promise. * @param {*} - the input to test * @returns {boolean} * @static */ function isPromise (input) { if (input) { const isPromise = isDefined(Promise) && input instanceof Promise; const isThenable = input.then && typeof input.then === 'function'; return !!(isPromise || isThenable) } else { return false } } /** * Returns true if the input is an iterable (`Map`, `Set`, `Array`, Generator etc.). * @param {*} - the input to test * @returns {boolean} * @static * @example * > t.isIterable('string') * true * > t.isIterable(new Map()) * true * > t.isIterable([]) * true * > t.isIterable((function * () {})()) * true * > t.isIterable(Promise.resolve()) * false * > t.isIterable(Promise) * false * > t.isIterable(true) * false * > t.isIterable({}) * false * > t.isIterable(0) * false * > t.isIterable(1.1) * false * > t.isIterable(NaN) * false * > t.isIterable(Infinity) * false * > t.isIterable(function () {}) * false * > t.isIterable(Date) * false * > t.isIterable() * false * > t.isIterable({ then: function () {} }) * false */ function isIterable (input) { if (input === null || !isDefined(input)) { return false } else { return ( typeof input[Symbol.iterator] === 'function' || typeof input[Symbol.asyncIterator] === 'function' ) } } var t = { isNumber, isString, isBoolean, isPlainObject, isArrayLike: isArrayLike$2, isObject: isObject$2, isDefined, isFunction, isClass, isPrimitive, isPromise, isIterable }; /** * @module option-definition */ /** * Describes a command-line option. Additionally, if generating a usage guide with [command-line-usage](https://github.com/75lb/command-line-usage) you could optionally add `description` and `typeLabel` properties to each definition. * * @alias module:option-definition * @typicalname option */ class OptionDefinition { constructor (definition) { /** * The only required definition property is `name`, so the simplest working example is * ```js * const optionDefinitions = [ * { name: 'file' }, * { name: 'depth' } * ] * ``` * * Where a `type` property is not specified it will default to `String`. * * | # | argv input | commandLineArgs() output | * | --- | -------------------- | ------------ | * | 1 | `--file` | `{ file: null }` | * | 2 | `--file lib.js` | `{ file: 'lib.js' }` | * | 3 | `--depth 2` | `{ depth: '2' }` | * * Unicode option names and aliases are valid, for example: * ```js * const optionDefinitions = [ * { name: 'один' }, * { name: '两' }, * { name: 'три', alias: 'т' } * ] * ``` * @type {string} */ this.name = definition.name; /** * The `type` value is a setter function (you receive the output from this), enabling you to be specific about the type and value received. * * The most common values used are `String` (the default), `Number` and `Boolean` but you can use a custom function, for example: * * ```js * const fs = require('fs') * * class FileDetails { * constructor (filename) { * this.filename = filename * this.exists = fs.existsSync(filename) * } * } * * const cli = commandLineArgs([ * { name: 'file', type: filename => new FileDetails(filename) }, * { name: 'depth', type: Number } * ]) * ``` * * | # | argv input | commandLineArgs() output | * | --- | ----------------- | ------------ | * | 1 | `--file asdf.txt` | `{ file: { filename: 'asdf.txt', exists: false } }` | * * The `--depth` option expects a `Number`. If no value was set, you will receive `null`. * * | # | argv input | commandLineArgs() output | * | --- | ----------------- | ------------ | * | 2 | `--depth` | `{ depth: null }` | * | 3 | `--depth 2` | `{ depth: 2 }` | * * @type {function} * @default String */ this.type = definition.type || String; /** * getopt-style short option names. Can be any single character (unicode included) except a digit or hyphen. * * ```js * const optionDefinitions = [ * { name: 'hot', alias: 'h', type: Boolean }, * { name: 'discount', alias: 'd', type: Boolean }, * { name: 'courses', alias: 'c' , type: Number } * ] * ``` * * | # | argv input | commandLineArgs() output | * | --- | ------------ | ------------ | * | 1 | `-hcd` | `{ hot: true, courses: null, discount: true }` | * | 2 | `-hdc 3` | `{ hot: true, discount: true, courses: 3 }` | * * @type {string} */ this.alias = definition.alias; /** * Set this flag if the option takes a list of values. You will receive an array of values, each passed through the `type` function (if specified). * * ```js * const optionDefinitions = [ * { name: 'files', type: String, multiple: true } * ] * ``` * * Note, examples 1 and 3 below demonstrate "greedy" parsing which can be disabled by using `lazyMultiple`. * * | # | argv input | commandLineArgs() output | * | --- | ------------ | ------------ | * | 1 | `--files one.js two.js` | `{ files: [ 'one.js', 'two.js' ] }` | * | 2 | `--files one.js --files two.js` | `{ files: [ 'one.js', 'two.js' ] }` | * | 3 | `--files *` | `{ files: [ 'one.js', 'two.js' ] }` | * * @type {boolean} */ this.multiple = definition.multiple; /** * Identical to `multiple` but with greedy parsing disabled. * * ```js * const optionDefinitions = [ * { name: 'files', lazyMultiple: true }, * { name: 'verbose', alias: 'v', type: Boolean, lazyMultiple: true } * ] * ``` * * | # | argv input | commandLineArgs() output | * | --- | ------------ | ------------ | * | 1 | `--files one.js --files two.js` | `{ files: [ 'one.js', 'two.js' ] }` | * | 2 | `-vvv` | `{ verbose: [ true, true, true ] }` | * * @type {boolean} */ this.lazyMultiple = definition.lazyMultiple; /** * Any values unaccounted for by an option definition will be set on the `defaultOption`. This flag is typically set on the most commonly-used option to make for more concise usage (i.e. `$ example *.js` instead of `$ example --files *.js`). * * ```js * const optionDefinitions = [ * { name: 'files', multiple: true, defaultOption: true } * ] * ``` * * | # | argv input | commandLineArgs() output | * | --- | ------------ | ------------ | * | 1 | `--files one.js two.js` | `{ files: [ 'one.js', 'two.js' ] }` | * | 2 | `one.js two.js` | `{ files: [ 'one.js', 'two.js' ] }` | * | 3 | `*` | `{ files: [ 'one.js', 'two.js' ] }` | * * @type {boolean} */ this.defaultOption = definition.defaultOption; /** * An initial value for the option. * * ```js * const optionDefinitions = [ * { name: 'files', multiple: true, defaultValue: [ 'one.js' ] }, * { name: 'max', type: Number, defaultValue: 3 } * ] * ``` * * | # | argv input | commandLineArgs() output | * | --- | ------------ | ------------ | * | 1 | | `{ files: [ 'one.js' ], max: 3 }` | * | 2 | `--files two.js` | `{ files: [ 'two.js' ], max: 3 }` | * | 3 | `--max 4` | `{ files: [ 'one.js' ], max: 4 }` | * * @type {*} */ this.defaultValue = definition.defaultValue; /** * When your app has a large amount of options it makes sense to organise them in groups. * * There are two automatic groups: `_all` (contains all options) and `_none` (contains options without a `group` specified in their definition). * * ```js * const optionDefinitions = [ * { name: 'verbose', group: 'standard' }, * { name: 'help', group: [ 'standard', 'main' ] }, * { name: 'compress', group: [ 'server', 'main' ] }, * { name: 'static', group: 'server' }, * { name: 'debug' } * ] * ``` * * * * * * * * * * * * * * * * * *
#Command LinecommandLineArgs() output
1--verbose

    *{
    *  _all: { verbose: true },
    *  standard: { verbose: true }
    *}
    *
2--debug

    *{
    *  _all: { debug: true },
    *  _none: { debug: true }
    *}
    *
3--verbose --debug --compress

    *{
    *  _all: {
    *    verbose: true,
    *    debug: true,
    *    compress: true
    *  },
    *  standard: { verbose: true },
    *  server: { compress: true },
    *  main: { compress: true },
    *  _none: { debug: true }
    *}
    *
4--compress

    *{
    *  _all: { compress: true },
    *  server: { compress: true },
    *  main: { compress: true }
    *}
    *
* * @type {string|string[]} */ this.group = definition.group; /* pick up any remaining properties */ for (let prop in definition) { if (!this[prop]) this[prop] = definition[prop]; } } isBoolean () { return this.type === Boolean || (t.isFunction(this.type) && this.type.name === 'Boolean') } isMultiple () { return this.multiple || this.lazyMultiple } static create (def) { const result = new this(def); return result } } /** * @module option-definitions */ /** * @alias module:option-definitions */ class Definitions extends Array { /** * validate option definitions * @returns {string} */ validate () { const someHaveNoName = this.some(def => !def.name); if (someHaveNoName) { halt( 'INVALID_DEFINITIONS', 'Invalid option definitions: the `name` property is required on each definition' ); } const someDontHaveFunctionType = this.some(def => def.type && typeof def.type !== 'function'); if (someDontHaveFunctionType) { halt( 'INVALID_DEFINITIONS', 'Invalid option definitions: the `type` property must be a setter fuction (default: `Boolean`)' ); } let invalidOption; const numericAlias = this.some(def => { invalidOption = def; return t.isDefined(def.alias) && t.isNumber(def.alias) }); if (numericAlias) { halt( 'INVALID_DEFINITIONS', 'Invalid option definition: to avoid ambiguity an alias cannot be numeric [--' + invalidOption.name + ' alias is -' + invalidOption.alias + ']' ); } const multiCharacterAlias = this.some(def => { invalidOption = def; return t.isDefined(def.alias) && def.alias.length !== 1 }); if (multiCharacterAlias) { halt( 'INVALID_DEFINITIONS', 'Invalid option definition: an alias must be a single character' ); } const hypenAlias = this.some(def => { invalidOption = def; return def.alias === '-' }); if (hypenAlias) { halt( 'INVALID_DEFINITIONS', 'Invalid option definition: an alias cannot be "-"' ); } const duplicateName = hasDuplicates(this.map(def => def.name)); if (duplicateName) { halt( 'INVALID_DEFINITIONS', 'Two or more option definitions have the same name' ); } const duplicateAlias = hasDuplicates(this.map(def => def.alias)); if (duplicateAlias) { halt( 'INVALID_DEFINITIONS', 'Two or more option definitions have the same alias' ); } const duplicateDefaultOption = hasDuplicates(this.map(def => def.defaultOption)); if (duplicateDefaultOption) { halt( 'INVALID_DEFINITIONS', 'Only one option definition can be the defaultOption' ); } const defaultBoolean = this.some(def => { invalidOption = def; return def.isBoolean() && def.defaultOption }); if (defaultBoolean) { halt( 'INVALID_DEFINITIONS', `A boolean option ["${invalidOption.name}"] can not also be the defaultOption.` ); } } /** * Get definition by option arg (e.g. `--one` or `-o`) * @param {string} * @returns {Definition} */ get (arg) { if (isOption(arg)) { return re.short.test(arg) ? this.find(def => def.alias === getOptionName(arg)) : this.find(def => def.name === getOptionName(arg)) } else { return this.find(def => def.name === arg) } } getDefault () { return this.find(def => def.defaultOption === true) } isGrouped () { return this.some(def => def.group) } whereGrouped () { return this.filter(containsValidGroup) } whereNotGrouped () { return this.filter(def => !containsValidGroup(def)) } whereDefaultValueSet () { return this.filter(def => t.isDefined(def.defaultValue)) } static from (definitions) { if (definitions instanceof this) return definitions const result = super.from(arrayify(definitions), def => OptionDefinition.create(def)); result.validate(); return result } } function halt (name, message) { const err = new Error(message); err.name = name; throw err } function containsValidGroup (def) { return arrayify(def.group).some(group => group) } function hasDuplicates (array) { const items = {}; for (let i = 0; i < array.length; i++) { const value = array[i]; if (items[value]) { return true } else { if (t.isDefined(value)) items[value] = true; } } } /** * @module argv-parser */ /** * @alias module:argv-parser */ class ArgvParser { /** * @param {OptionDefinitions} - Definitions array * @param {object} [options] - Options * @param {string[]} [options.argv] - Overrides `process.argv` * @param {boolean} [options.stopAtFirstUnknown] - */ constructor (definitions, options) { this.options = Object.assign({}, options); /** * Option Definitions */ this.definitions = Definitions.from(definitions); /** * Argv */ this.argv = ArgvArray.from(this.options.argv); if (this.argv.hasCombinedShortOptions()) { findReplace(this.argv, re.combinedShort.test.bind(re.combinedShort), arg => { arg = arg.slice(1); return arg.split('').map(letter => ({ origArg: `-${arg}`, arg: '-' + letter })) }); } } /** * Yields one `{ event, name, value, arg, def }` argInfo object for each arg in `process.argv` (or `options.argv`). */ * [Symbol.iterator] () { const definitions = this.definitions; let def; let value; let name; let event; let singularDefaultSet = false; let unknownFound = false; let origArg; for (let arg of this.argv) { if (t.isPlainObject(arg)) { origArg = arg.origArg; arg = arg.arg; } if (unknownFound && this.options.stopAtFirstUnknown) { yield { event: 'unknown_value', arg, name: '_unknown', value: undefined }; continue } /* handle long or short option */ if (isOption(arg)) { def = definitions.get(arg); value = undefined; if (def) { value = def.isBoolean() ? true : null; event = 'set'; } else { event = 'unknown_option'; } /* handle --option-value notation */ } else if (isOptionEqualsNotation(arg)) { const matches = arg.match(re.optEquals); def = definitions.get(matches[1]); if (def) { if (def.isBoolean()) { yield { event: 'unknown_value', arg, name: '_unknown', value, def }; event = 'set'; value = true; } else { event = 'set'; value = matches[2]; } } else { event = 'unknown_option'; } /* handle value */ } else if (isValue(arg)) { if (def) { value = arg; event = 'set'; } else { /* get the defaultOption */ def = this.definitions.getDefault(); if (def && !singularDefaultSet) { value = arg; event = 'set'; } else { event = 'unknown_value'; def = undefined; } } } name = def ? def.name : '_unknown'; const argInfo = { event, arg, name, value, def }; if (origArg) { argInfo.subArg = arg; argInfo.arg = origArg; } yield argInfo; /* unknownFound logic */ if (name === '_unknown') unknownFound = true; /* singularDefaultSet logic */ if (def && def.defaultOption && !def.isMultiple() && event === 'set') singularDefaultSet = true; /* reset values once consumed and yielded */ if (def && def.isBoolean()) def = undefined; /* reset the def if it's a singular which has been set */ if (def && !def.multiple && t.isDefined(value) && value !== null) { def = undefined; } value = undefined; event = undefined; name = undefined; origArg = undefined; } } } const _value = new WeakMap(); /** * Encapsulates behaviour (defined by an OptionDefinition) when setting values */ class Option { constructor (definition) { this.definition = new OptionDefinition(definition); this.state = null; /* set or default */ this.resetToDefault(); } get () { return _value.get(this) } set (val) { this._set(val, 'set'); } _set (val, state) { const def = this.definition; if (def.isMultiple()) { /* don't add null or undefined to a multiple */ if (val !== null && val !== undefined) { const arr = this.get(); if (this.state === 'default') arr.length = 0; arr.push(def.type(val)); this.state = state; } } else { /* throw if already set on a singlar defaultOption */ if (!def.isMultiple() && this.state === 'set') { const err = new Error(`Singular option already set [${this.definition.name}=${this.get()}]`); err.name = 'ALREADY_SET'; err.value = val; err.optionName = def.name; throw err } else if (val === null || val === undefined) { _value.set(this, val); // /* required to make 'partial: defaultOption with value equal to defaultValue 2' pass */ // if (!(def.defaultOption && !def.isMultiple())) { // this.state = state // } } else { _value.set(this, def.type(val)); this.state = state; } } } resetToDefault () { if (t.isDefined(this.definition.defaultValue)) { if (this.definition.isMultiple()) { _value.set(this, arrayify(this.definition.defaultValue).slice()); } else { _value.set(this, this.definition.defaultValue); } } else { if (this.definition.isMultiple()) { _value.set(this, []); } else { _value.set(this, null); } } this.state = 'default'; } static create (definition) { definition = new OptionDefinition(definition); if (definition.isBoolean()) { return FlagOption.create(definition) } else { return new this(definition) } } } class FlagOption extends Option { set (val) { super.set(true); } static create (def) { return new this(def) } } /** * A map of { DefinitionNameString: Option }. By default, an Output has an `_unknown` property and any options with defaultValues. */ class Output extends Map { constructor (definitions) { super(); /** * @type {OptionDefinitions} */ this.definitions = Definitions.from(definitions); /* by default, an Output has an `_unknown` property and any options with defaultValues */ this.set('_unknown', Option.create({ name: '_unknown', multiple: true })); for (const def of this.definitions.whereDefaultValueSet()) { this.set(def.name, Option.create(def)); } } toObject (options) { options = options || {}; const output = {}; for (const item of this) { const name = options.camelCase && item[0] !== '_unknown' ? camelCase(item[0]) : item[0]; const option = item[1]; if (name === '_unknown' && !option.get().length) continue output[name] = option.get(); } if (options.skipUnknown) delete output._unknown; return output } } class GroupedOutput extends Output { toObject (options) { const superOutputNoCamel = super.toObject({ skipUnknown: options.skipUnknown }); const superOutput = super.toObject(options); const unknown = superOutput._unknown; delete superOutput._unknown; const grouped = { _all: superOutput }; if (unknown && unknown.length) grouped._unknown = unknown; this.definitions.whereGrouped().forEach(def => { const name = options.camelCase ? camelCase(def.name) : def.name; const outputValue = superOutputNoCamel[def.name]; for (const groupName of arrayify(def.group)) { grouped[groupName] = grouped[groupName] || {}; if (t.isDefined(outputValue)) { grouped[groupName][name] = outputValue; } } }); this.definitions.whereNotGrouped().forEach(def => { const name = options.camelCase ? camelCase(def.name) : def.name; const outputValue = superOutputNoCamel[def.name]; if (t.isDefined(outputValue)) { if (!grouped._none) grouped._none = {}; grouped._none[name] = outputValue; } }); return grouped } } /** * @module command-line-args */ /** * Returns an object containing all option values set on the command line. By default it parses the global [`process.argv`](https://nodejs.org/api/process.html#process_process_argv) array. * * Parsing is strict by default - an exception is thrown if the user sets a singular option more than once or sets an unknown value or option (one without a valid [definition](https://github.com/75lb/command-line-args/blob/master/doc/option-definition.md)). To be more permissive, enabling [partial](https://github.com/75lb/command-line-args/wiki/Partial-mode-example) or [stopAtFirstUnknown](https://github.com/75lb/command-line-args/wiki/stopAtFirstUnknown) modes will return known options in the usual manner while collecting unknown arguments in a separate `_unknown` property. * * @param {module:definition[]} - An array of [OptionDefinition](https://github.com/75lb/command-line-args/blob/master/doc/option-definition.md) objects * @param {object} [options] - Options. * @param {string[]} [options.argv] - An array of strings which, if present will be parsed instead of `process.argv`. * @param {boolean} [options.partial] - If `true`, an array of unknown arguments is returned in the `_unknown` property of the output. * @param {boolean} [options.stopAtFirstUnknown] - If `true`, parsing will stop at the first unknown argument and the remaining arguments returned in `_unknown`. When set, `partial: true` is also implied. * @param {boolean} [options.camelCase] - If `true`, options with hypenated names (e.g. `move-to`) will be returned in camel-case (e.g. `moveTo`). * @returns {object} * @throws `UNKNOWN_OPTION` If `options.partial` is false and the user set an undefined option. The `err.optionName` property contains the arg that specified an unknown option, e.g. `--one`. * @throws `UNKNOWN_VALUE` If `options.partial` is false and the user set a value unaccounted for by an option definition. The `err.value` property contains the unknown value, e.g. `5`. * @throws `ALREADY_SET` If a user sets a singular, non-multiple option more than once. The `err.optionName` property contains the option name that has already been set, e.g. `one`. * @throws `INVALID_DEFINITIONS` * - If an option definition is missing the required `name` property * - If an option definition has a `type` value that's not a function * - If an alias is numeric, a hyphen or a length other than 1 * - If an option definition name was used more than once * - If an option definition alias was used more than once * - If more than one option definition has `defaultOption: true` * - If a `Boolean` option is also set as the `defaultOption`. * @alias module:command-line-args */ function commandLineArgs (optionDefinitions, options) { options = options || {}; if (options.stopAtFirstUnknown) options.partial = true; optionDefinitions = Definitions.from(optionDefinitions); const parser = new ArgvParser(optionDefinitions, { argv: options.argv, stopAtFirstUnknown: options.stopAtFirstUnknown }); const OutputClass = optionDefinitions.isGrouped() ? GroupedOutput : Output; const output = new OutputClass(optionDefinitions); /* Iterate the parser setting each known value to the output. Optionally, throw on unknowns. */ for (const argInfo of parser) { const arg = argInfo.subArg || argInfo.arg; if (!options.partial) { if (argInfo.event === 'unknown_value') { const err = new Error(`Unknown value: ${arg}`); err.name = 'UNKNOWN_VALUE'; err.value = arg; throw err } else if (argInfo.event === 'unknown_option') { const err = new Error(`Unknown option: ${arg}`); err.name = 'UNKNOWN_OPTION'; err.optionName = arg; throw err } } let option; if (output.has(argInfo.name)) { option = output.get(argInfo.name); } else { option = Option.create(argInfo.def); output.set(argInfo.name, option); } if (argInfo.name === '_unknown') { option.set(arg); } else { option.set(argInfo.value); } } return output.toObject({ skipUnknown: !options.partial, camelCase: options.camelCase }) } export default commandLineArgs;