Spaces:
Running
Running
import camelCase from 'lodash.camelcase'; | |
/** | |
* Takes any input and guarantees an array back. | |
* | |
* - Converts array-like objects (e.g. `arguments`, `Set`) to a real array. | |
* - Converts `undefined` to an empty array. | |
* - Converts any another other, singular value (including `null`, objects and iterables other than `Set`) 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 ] | |
* | |
* > arrayify(new Set([ 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 | |
} | |
if (input === undefined) { | |
return [] | |
} | |
if (isArrayLike(input) || input instanceof Set) { | |
return Array.from(input) | |
} | |
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' } | |
* ] | |
* ``` | |
* | |
*<table> | |
* <tr> | |
* <th>#</th><th>Command Line</th><th>commandLineArgs() output</th> | |
* </tr> | |
* <tr> | |
* <td>1</td><td><code>--verbose</code></td><td><pre><code> | |
*{ | |
* _all: { verbose: true }, | |
* standard: { verbose: true } | |
*} | |
*</code></pre></td> | |
* </tr> | |
* <tr> | |
* <td>2</td><td><code>--debug</code></td><td><pre><code> | |
*{ | |
* _all: { debug: true }, | |
* _none: { debug: true } | |
*} | |
*</code></pre></td> | |
* </tr> | |
* <tr> | |
* <td>3</td><td><code>--verbose --debug --compress</code></td><td><pre><code> | |
*{ | |
* _all: { | |
* verbose: true, | |
* debug: true, | |
* compress: true | |
* }, | |
* standard: { verbose: true }, | |
* server: { compress: true }, | |
* main: { compress: true }, | |
* _none: { debug: true } | |
*} | |
*</code></pre></td> | |
* </tr> | |
* <tr> | |
* <td>4</td><td><code>--compress</code></td><td><pre><code> | |
*{ | |
* _all: { compress: true }, | |
* server: { compress: true }, | |
* main: { compress: true } | |
*} | |
*</code></pre></td> | |
* </tr> | |
*</table> | |
* | |
* @type {string|string[]} | |
*/ | |
this.group = definition.group; | |
/* pick up any remaining properties */ | |
for (const 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 | |
* @param {boolean} [caseInsensitive=false] - whether arguments will be parsed in a case insensitive manner | |
* @returns {string} | |
*/ | |
validate (caseInsensitive) { | |
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 => caseInsensitive ? def.name.toLowerCase() : def.name)); | |
if (duplicateName) { | |
halt( | |
'INVALID_DEFINITIONS', | |
'Two or more option definitions have the same name' | |
); | |
} | |
const duplicateAlias = hasDuplicates(this.map(def => caseInsensitive && t.isDefined(def.alias) ? def.alias.toLowerCase() : def.alias)); | |
if (duplicateAlias) { | |
halt( | |
'INVALID_DEFINITIONS', | |
'Two or more option definitions have the same alias' | |
); | |
} | |
const duplicateDefaultOption = this.filter(def => def.defaultOption === true).length > 1; | |
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} [arg] the argument name to get the definition for | |
* @param {boolean} [caseInsensitive] whether to use case insensitive comparisons when finding the appropriate definition | |
* @returns {Definition} | |
*/ | |
get (arg, caseInsensitive) { | |
if (isOption(arg)) { | |
if (re.short.test(arg)) { | |
const shortOptionName = getOptionName(arg); | |
if (caseInsensitive) { | |
const lowercaseShortOptionName = shortOptionName.toLowerCase(); | |
return this.find(def => t.isDefined(def.alias) && def.alias.toLowerCase() === lowercaseShortOptionName) | |
} else { | |
return this.find(def => def.alias === shortOptionName) | |
} | |
} else { | |
const optionName = getOptionName(arg); | |
if (caseInsensitive) { | |
const lowercaseOptionName = optionName.toLowerCase(); | |
return this.find(def => def.name.toLowerCase() === lowercaseOptionName) | |
} else { | |
return this.find(def => def.name === optionName) | |
} | |
} | |
} 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, caseInsensitive) { | |
if (definitions instanceof this) return definitions | |
const result = super.from(arrayify(definitions), def => OptionDefinition.create(def)); | |
result.validate(caseInsensitive); | |
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] - | |
* @param {boolean} [options.caseInsensitive] - Arguments will be parsed in a case insensitive manner. Defaults to false. | |
*/ | |
constructor (definitions, options) { | |
this.options = Object.assign({}, options); | |
/** | |
* Option Definitions | |
*/ | |
this.definitions = Definitions.from(definitions, this.options.caseInsensitive); | |
/** | |
* 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, this.options.caseInsensitive); | |
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], this.options.caseInsensitive); | |
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 {Array<OptionDefinition>} - 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`). | |
* @param {boolean} [options.caseInsensitive] - If `true`, the case of each option name or alias parsed is insignificant. In other words, both `--Verbose` and `--verbose`, `-V` and `-v` would be equivalent. Defaults to false. | |
* @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, options.caseInsensitive); | |
const parser = new ArgvParser(optionDefinitions, { | |
argv: options.argv, | |
stopAtFirstUnknown: options.stopAtFirstUnknown, | |
caseInsensitive: options.caseInsensitive | |
}); | |
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; | |