From 4e1aacb44fa99ecbc1b8b5cf64ba80aa9dceb992 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 6 Jul 2022 18:56:13 -0500 Subject: [PATCH] Remove command-line-args dependency --- package-lock.json | 34 - package.json | 1 - prod.js | 2 +- server/libs/commandLineArgs/index.js | 1399 ++++++++++++++++++++++++++ 4 files changed, 1400 insertions(+), 36 deletions(-) create mode 100644 server/libs/commandLineArgs/index.js diff --git a/package-lock.json b/package-lock.json index 700eba81..c6cf8a00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,11 +93,6 @@ } } }, - "array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==" - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -210,17 +205,6 @@ "get-intrinsic": "^1.0.2" } }, - "command-line-args": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", - "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", - "requires": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" - } - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -489,14 +473,6 @@ "unpipe": "~1.0.0" } }, - "find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", - "requires": { - "array-back": "^3.0.1" - } - }, "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -665,11 +641,6 @@ } } }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -1018,11 +989,6 @@ "mime-types": "~2.1.24" } }, - "typical": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", - "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==" - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 631144b1..d6a34687 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "archiver": "^5.3.0", "axios": "^0.26.1", "bcryptjs": "^2.4.3", - "command-line-args": "^5.2.0", "date-and-time": "^2.3.1", "express": "^4.17.1", "express-fileupload": "^1.2.1", diff --git a/prod.js b/prod.js index 20ea004d..22f84234 100644 --- a/prod.js +++ b/prod.js @@ -6,7 +6,7 @@ const optionDefinitions = [ { name: 'source', alias: 's', type: String } ] -const commandLineArgs = require('command-line-args') +const commandLineArgs = require('./server/libs/commandLineArgs') const options = commandLineArgs(optionDefinitions) const Path = require('path') diff --git a/server/libs/commandLineArgs/index.js b/server/libs/commandLineArgs/index.js new file mode 100644 index 00000000..5d727dbb --- /dev/null +++ b/server/libs/commandLineArgs/index.js @@ -0,0 +1,1399 @@ +'use strict'; + +// +// modified for use in audiobookshelf (removed camelCase opt) +// Source: https://github.com/75lb/command-line-args +// + +/** + * 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' } + * ] + * ``` + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
#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 (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 = 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 = 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 = 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} - 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.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 }) +} + +module.exports = commandLineArgs; \ No newline at end of file