1369 lines
36 KiB
JavaScript
1369 lines
36 KiB
JavaScript
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' }
|
|
* ]
|
|
* ```
|
|
*
|
|
*<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 (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;
|