UNPKG

hb-lib-tools

Version:

homebridge-lib Command-Line Tools`

892 lines (821 loc) 31.5 kB
// hb-lib-tools/lib/OptionParser.js // // Library for Homebridge plugins. // Copyright © 2018-2025 Erik Baauw. All rights reserved. import { EventEmitter } from 'node:events' import { posix } from 'node:path' import { isIPv4, isIPv6 } from 'node:net' const noop = () => {} /** User input error. * @hideconstructor * @extends Error * @memberof OptionParser */ class UserInputError extends Error {} // Create a new RangeError or UserInputError, depending on userInput. function newRangeError (message, userInput = false) { return userInput ? new UserInputError(message) : new RangeError(message) } // Create a new SyntaxError or UserInputError, depending on userInput. function newSyntaxError (message, userInput = false) { return userInput ? new UserInputError(message) : new SyntaxError(message) } // Create a new TypeError or UserInputError, depending on userInput. function newTypeError (message, userInput = false) { return userInput ? new UserInputError(message) : new TypeError(message) } /** Parser and validator for options and other parameters. * <br>See {@link OptionParser}. * @name OptionParser * @type {Class} * @memberof module:hb-lib-tools */ /** Parser and validator for options and other parameters. * * @extends EventEmitter * @emits userInputError * @emits warning */ class OptionParser extends EventEmitter { /** Commonly used regular expressions. * @type {object} * @property {RegExp} hostname - Internet hostname. * @property {RegExp} int - Decimal integer. * @property {RegExp} intBin - Binary integer, optionally prefixed with `0b`. * @property {RegExp} intOct - Octal integer, optionally prefixed with `0o`. * @property {RegExp} intHex - Hexadecimal integer, optionally prefixed with `0x`. * @property {RegExp} ipv4 - IPv4 address in dot notation. * @property {RegExp} number - Number. * @property {RegExp} mac - Mac address (EUI-48). * @property {RegExp} mac64 - 64-bit mac address (EUI-64). * @property {RegExp} uuid - UUID. */ static get patterns () { return Object.freeze({ _host: /^(?:\[(.+)\]|([^:]+))(?::([0-9]{1,5}))?$/, hostname: /^[a-zA-Z0-9](:?[a-zA-Z0-9-]*[a-zA-Z0-9])*(:?\.[a-zA-Z0-9](:?[a-zA-Z0-9-]*[a-zA-Z0-9])*)*$/, int: /^\s*([+-]?)([0-9]+(?:\.0*)?)\s*$/, intBin: /^\s*([+-]?)(?:0[bB])([01]+)\s*$/, intOct: /^\s*([+-]?)(?:0[oO])([0-8]+)\s*$/, intHex: /^\s*([+-]?)(?:0[xX])([0-9A-Fa-f]+)\s*$/, ipv4: /^(\d{1,2}|[01]\d{2}|2[0-4]\d|25[0-5])\.(\d{1,2}|[01]\d{2}|2[0-4]\d|25[0-5])\.(\d{1,2}|[01]\d{2}|2[0-4]\d|25[0-5])\.(\d{1,2}|[01]\d{2}|2[0-4]\d|25[0-5])$/, number: /^\s*[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\s*$/, mac: /^([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})$/, mac64: /^([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})[:-]([0-9a-fA-F]{1,2})$/, uuid: /^([0-9a-fA-F]{8})-([0-9a-fA-F]{4})-([1-5][0-9a-fA-F]{3})-([89abAB][0-9a-fA-F]{3})-([0-9a-fA-F]{12})$/ }) } static get UserInputError () { return UserInputError } /** Casts input value to boolean. * * Valid input values are: * - A boolean; * - A number with value 0 (false) or 1 (true); * - A string with value 'false', 'no', 'off', or '0' (false); or * with value 'true', 'yes', 'on', or '1' (true). * @param {!string} key - The key of the input value (for error messages). * @param {*} value - The input value. * @param {boolean} [userInput=false] - Value was input by user. * @returns {boolean} The value as boolean. * @throws {TypeError} On invalid input value. * @throws {UserError} On error, when value was input by user. */ static toBool (key, value, userInput = false) { key === 'key' || key === 'nonEmpty' || OptionParser.toString('key', key, true) userInput === false || OptionParser.toBool('userInput', userInput) if (value == null) { throw newTypeError(`${key}: missing boolean value`, userInput) } if (typeof value === 'boolean') { return value } if (typeof value === 'string') { value = value.toLowerCase() } if (['true', 'yes', 'on', '1', 1].includes(value)) { return true } if (['false', 'no', 'off', '0', 0].includes(value)) { return false } throw newTypeError(`${key}: not a boolean`, userInput) } /** Casts input value to integer, optionally clamped between min and max. * * Valid input values are: * - A boolean: false (0) or true (1); * - A number with an integer value; * - A string holding an integer value in decimal, binary, octal or * hexadecimal notation. * @param {!string} key - The key of the input value (for error messages). * @param {*} value - The input value. * @param {?integer} min - Minimum value returned. * @param {?integer} max - Maximum value returned. * @param {boolean} [userInput=false] - Value was input by user. * @returns {integer} The value as integer. * @throws {TypeError} On invalid input value. * @throws {UserError} On error, when value was input by user. */ static toInt (key, value, min = -Infinity, max = Infinity, userInput = false) { OptionParser.toString('key', key, true) min === -Infinity || OptionParser.toInt('min', min) max === Infinity || OptionParser.toInt('max', max) userInput === false || OptionParser.toBool('userInput', userInput) if (max < min) { throw newRangeError('max: smaller than min') } if (value == null) { throw newTypeError(`${key}: missing integer value`, userInput) } if (typeof value === 'boolean') { value = value ? 1 : 0 } else if (typeof value === 'number' || typeof value === 'string') { if (OptionParser.patterns.int.test(value)) { value = parseInt(value) } else if (OptionParser.patterns.intHex.test(value)) { value = parseInt(value, 16) } else if (OptionParser.patterns.intOct.test(value)) { const a = OptionParser.patterns.intOct.exec(value) value = parseInt(a[1] + a[2], 8) } else if (OptionParser.patterns.intBin.test(value)) { const a = OptionParser.patterns.intBin.exec(value) value = parseInt(a[1] + a[2], 2) } else { throw newTypeError(`${key}: not an integer`, userInput) } } else { throw newTypeError(`${key}: not an integer`, userInput) } return Math.min(Math.max(value, min), max) } /** Converts an integer value to a formatted string. * * The integer value is converted to a string in the specified radix. * The string is prepended with spaces (radix 10) or zeroes (other radix * values) to match the minimum length. * * @param {!string} key - The key of the value (for error messages). * @param {integer} value - The input value. * @param {integer} [radix=10] - The radix. * @param {integer} [length=0] - The minimum length of the formatted string. * @param {boolean} [userInput=false] - Value was input by user. * @returns {string} The formatted string. * @throws {TypeError} On invalid input value. */ static toIntString (key, value, radix = 10, length = 0, userInput = false) { radix === 10 || OptionParser.toInt('radix', 2, 36) length === 0 || OptionParser.toInt('length', 0, 32) if (value < 0 && radix !== 10) { throw new RangeError(`${key}: not an unsigned integer`, userInput) } value = OptionParser.toInt( 'value', value, undefined, undefined, userInput ).toString(radix).toUpperCase() if (value.length > length) { return value } const prefix = radix === 10 ? ' ' : '00000000000000000000000000000000' return (prefix + value).slice(-length) } /** Casts input value to number, optionally clamped between min and max. * * Valid input values are: * - A boolean: false (0) or true (1); * - A real number (not: NaN, -Infinity, Infinity); * - A string holding a number value. * @param {!string} key - The key of the input value (for error messages). * @param {*} value - The input value. * @param {?number} min - Minimum value returned. * @param {?number} max - Maximum value returned. * @param {boolean} [userInput=false] - Value was input by user. * @returns {number} The value as number. * @throws {TypeError} On invalid input value. * @throws {UserError} On error, when value was input by user. */ static toNumber (key, value, min = -Infinity, max = Infinity, userInput = false) { OptionParser.toString('key', key, true) min === -Infinity || OptionParser.toNumber('min', min) max === Infinity || OptionParser.toNumber('max', max) userInput === false || OptionParser.toBool('userInput', userInput) if (max < min) { throw newRangeError('max: smaller than min') } if (value == null) { throw newTypeError(`${key}: missing number value`, userInput) } if (typeof value === 'boolean') { value = value ? 1 : 0 } else if (typeof value === 'number' || typeof value === 'string') { if (OptionParser.patterns.number.test(value)) { value = parseFloat(value) } else { throw newTypeError(`${key}: not a number`, userInput) } } else { throw newTypeError(`${key}: not a number`, userInput) } return Math.min(Math.max(value, min), max) } /** Converts an integer value to a formatted string. * * The integer value is converted to a string, optionally with a fixed * number of decimals. * The string is prepended with `0`s to match the minimum length. * * @param {!string} key - The key of the value (for error messages). * @param {integer} value - The input value. * @param {integer} [length=0] - The minimum length of the formatted string. * @param {?integer} decimals - The fixed number of decimals * @param {boolean} [userInput=false] - Value was input by user. * @returns {string} The formatted string. * @throws {TypeError} On invalid input value. */ static toNumberString ( key, value, length = 0, decimals = null, userInput = false ) { OptionParser.toString('key', key, true) length === 0 || OptionParser.toInt('length', 0, 32) decimals === 0 || OptionParser.toInt('decimals', 0, 16) value = OptionParser.toNumber(key, value, undefined, undefined, userInput) if (decimals == null) { value = value.toString(10) } else { value = value.toFixed(decimals) } if (value.length > length) { return value } return (' ' + value).slice(-length) } /** Casts input value to string, optionally non-empty. * * Valid values are: * - A string. * - A boolean. * - A number. * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @param {boolean} [nonEmpty=false] - Empty string is invalid value. * @param {boolean} [userInput=false] - Value was input by user. * @returns {string} The value as string. * @throws {TypeError} On invalid input value. * @throws {RangeError} On empty string, when noEmptyString has been set. * @throws {UserError} On error, when value was input by user. */ static toString (key, value, nonEmpty = false, userInput = false) { key === 'key' || OptionParser.toString('key', key, true) nonEmpty === false || OptionParser.toBool('nonEmpty', nonEmpty) userInput === false || OptionParser.toBool('userInput', userInput) if (value == null && nonEmpty) { throw newTypeError(`${key}: missing string value`, userInput) } else if (value == null) { value = '' } else if (typeof value === 'boolean' || typeof value === 'number') { value = '' + value } else if (typeof value !== 'string') { throw newTypeError(`${key}: not a string`, userInput) } if (nonEmpty && value === '') { throw newRangeError(`${key}: not a non-empty string`, userInput) } return value } /** Casts input value to hostname[:port]. * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @param {boolean} [asString=false] - Return path as string instead of object. * @param {boolean} [userInput=false] - Value was input by user. * @returns {object|string} The value as { hostname: hostname, port: port }. * @throws {TypeError} On invalid input value. * @throws {RangeError} On empty string, when noEmptyString has been set. * @throws {UserError} On error, when value was input by user. */ static toHost (key, value, asString = false, userInput = false) { key === 'key' || OptionParser.toString('key', key, true) asString === false || OptionParser.toBool('asString', asString) userInput === false || OptionParser.toBool('userInput', userInput) OptionParser.toString(key, value, true, userInput) const response = {} const list = OptionParser.patterns._host.exec(value) if (list == null) { throw newRangeError(`${key}: not a valid host`, userInput) } if (list[1] != null) { if (!isIPv6(list[1])) { throw newRangeError(`${key}: [${list[1]}]: not a valid IPv6 address`, userInput) } response.hostname = '[' + list[1] + ']' } else if (isIPv4(list[2])) { response.hostname = list[2].split('.').map((byte) => { return parseInt(byte) }).join('.') } else if (OptionParser.patterns.hostname.test(list[2])) { response.hostname = list[2] } else { throw newRangeError(`${key}: ${list[2]}: not a valid hostname or IPv4 address`, userInput) } let host = response.hostname if (list[3] != null) { const port = parseInt(list[3], 10) if (port < 0 || port > 65535) { throw newRangeError(`${key}: ${port}: not a valid port`, userInput) } response.port = port host += ':' + port } return asString ? host : response } /** Casts input value to path. * * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @param {boolean} [userInput=false] - Value was input by user. * @returns {string} The value as normalised resource path. * @throws {TypeError} On invalid input value. * @throws {RangeError} On empty string, on string not starting with '/'. * @throws {UserError} On error, when value was input by user. */ static toPath (key, value, userInput = false) { OptionParser.toString('key', key, true) userInput === false || OptionParser.toBool('userInput', userInput) value = OptionParser.toString(key, value, true, userInput) if (value[0] !== '/') { throw newRangeError(`${key}: ${value}: not a valid path`, key, value) } return posix.normalize(value) } /** Casts input value to array. * * Valid values are: * - Null (empty array); * - A boolean, number, or string (singleton array); * - An array. * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @param {boolean} [userInput=false] - Value was input by user. * @returns {string} The value as array. * @throws {TypeError} On invalid input value. * @throws {UserError} On error, when value was input by user. */ static toArray (key, value, userInput = false) { OptionParser.toString('key', key, true) userInput === false || OptionParser.toBool('userInput', userInput) if (value == null) { return [] } if (['boolean', 'number', 'string'].includes(typeof value)) { return [value] } if (Array.isArray(value)) { return value } throw newTypeError(`${key}: not an array`, userInput) } /** Casts input value to object. * * Valid values are: * - Null (empty object); * - A proper object (i.e. not a class instance). * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @param {boolean} [userInput=false] - Value was input by user. * @returns {Object} The value. * @throws {TypeError} On invalid input value. * @throws {UserError} On error, when value was input by user. */ static toObject (key, value, userInput = false) { OptionParser.toString('key', key, true) userInput === false || OptionParser.toBool('userInput', userInput) if (value == null) { return {} } if ( typeof value !== 'object' || value == null || value.constructor.name !== 'Object' ) { throw newTypeError(`${key}: not an object`, userInput) } return value } /** Casts input value to function. * * Valid values are: * - A proper function (i.e. not a class). * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @returns {function} The value. * @throws {TypeError} On invalid input value. */ static toFunction (key, value) { OptionParser.toString('key', key, true) if (value == null) { throw new TypeError(`${key}: missing function value`) } if ( typeof value === 'function' && value.prototype == null && value.constructor.name === 'Function' ) { return value } throw new TypeError(`${key}: not a function`) } /** Casts input value to function. * * Valid values are: * - A proper async function. * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @returns {function} The value. * @throws {TypeError} On invalid input value. */ static toAsyncFunction (key, value) { OptionParser.toString('key', key, true) if (value == null) { throw new TypeError(`${key}: missing async function value`) } if ( typeof value === 'function' && value.constructor.name === 'AsyncFunction' ) { return value } throw new TypeError(`${key}: not an async function`) } /** Casts input value to class. * * Valid values are: * - A proper Class or function with a prototype. * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @param {?Class} SuperClass - Check for subclass of SuperClass. * @returns {*} The value. * @throws {TypeError} On invalid input value. */ static toClass (key, value, SuperClass) { OptionParser.toString('key', key, true) SuperClass === undefined || OptionParser.toClass('SuperClass', SuperClass) if (value == null) { throw new TypeError(`${key}: missing class value`) } if (typeof value !== 'function' || value.prototype == null) { throw new TypeError(`${key}: not a class`) } if ( SuperClass != null && value !== SuperClass && !(value.prototype instanceof SuperClass) ) { throw new TypeError(`${key}: not a subclass of ${SuperClass.name}`) } return value } /** Casts input value to class instance. * * Valid values are: * - A class instance or a proper function. * @param {!string} key - The key of the value (for error messages). * @param {*} value - The input value. * @param {!Class} Class - Check for instance of Class. * @returns {Class} The value. * @throws {TypeError} On invalid input value. */ static toInstance (key, value, Class) { OptionParser.toString('key', key, true) OptionParser.toClass('Class', Class) if (value == null) { throw new TypeError(`${key}: missing instance of ${Class.name} value`) } if (value instanceof Class) { return value } throw new TypeError(`${key}: not an instance of ${Class.name}`) } /** Creates a new OptionParser instance * * @param {boolean} [userInput=false] - Options were input by user. */ constructor (object = {}, userInput = false) { super() this._object = OptionParser.toObject('object', object) this._userInput = OptionParser.toBool('userInput', userInput) this._callbacks = {} } /** Checks that key is valid and not yet in use. * * @param {!string} key - The key. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ _toKey (key) { key = OptionParser.toString('key', key, true) if (this._callbacks[key] != null) { throw new SyntaxError(`${key}: duplicate key`) } return key } /** Defines a key that takes an array as value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ arrayKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toArray(key, value, this._userInput) } return this } /** Defines a key that takes an async function as value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ asyncFunctionKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toAsyncFunction(key, value) } return this } /** Defines a key that takes a boolean value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ boolKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toBool(key, value, this._userInput) } return this } /** Defines a key that takes an enum value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ enumKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { value = OptionParser.toString( key, value, true, this._userInput ) const callback = this._callbacks[key].list[value] if (callback == null) { throw newRangeError(`${value}: invalid ${key}`, this._userInput) } this._object[key] = value callback() } this._callbacks[key].list = {} return this } /** Defines a value for an enum key. * * @param {!string} key - The key. * @param {!string} value - The key. * @param {?function} callback - Function to call when enum value is present. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ enumKeyValue (key, value, callback = noop) { key = OptionParser.toString('key', key, true) value = OptionParser.toString('value', value, true) OptionParser.toFunction(key, this._callbacks[key]) callback = OptionParser.toFunction('callback', callback) this._callbacks[key].list[value] = callback return this } /** Defines a key that takes a function as value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ functionKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toFunction(key, value) } return this } /** Defines a key that takes a hostname[:port] as value. * * @param {!string} key - The key. * @param {string} [hostnameKey=hostname] - The key for the hostname. * @param {string} [portKey=port] - The key for the port. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ hostKey (key = 'host', hostnameKey = 'hostname', portKey = 'port') { key = this._toKey(key) hostnameKey = OptionParser.toString('hostnameKey', hostnameKey, true) portKey = OptionParser.toString('portKey', portKey, true) this._callbacks[key] = (value) => { const host = OptionParser.toHost(key, value, false, this._userInput) this._object[hostnameKey] = host.hostname if (host.port != null) { this._object[portKey] = host.port } } return this } /** Defines a key that takes an integer value, * optionally clamped between min and max. * * @param {!string} key - The key. * @param {!Class} Class - Check for instance of Class. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ instanceKey (key, Class) { key = this._toKey(key) Class = OptionParser.toClass('Class', Class) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toInstance(key, value, Class) } return this } /** Defines a key that takes an integer value, * optionally clamped between min and max. * * @param {!string} key - The key. * @param {?integer} min - Minimum value returned. * @param {?integer} max - Maximum value returned. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ intKey (key, min, max) { key = this._toKey(key) min = min == null ? -Infinity : OptionParser.toInt('min', min) max = max == null ? Infinity : OptionParser.toInt('max', max) if (max < min) { throw newRangeError('max: smaller than min') } this._callbacks[key] = (value) => { this._object[key] = OptionParser.toInt(key, value, min, max, this._userInput) } return this } /** Defines a key that takes a list of strings as value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ listKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { const array = [] const map = {} for (const element of OptionParser.toArray(key, value)) { try { OptionParser.toString(`${key}.${element}`, element, true, this._userInput) if (map[element]) { throw newSyntaxError(`${key}.${element}: duplicate key`, this._userInput) } map[element] = true array.push(element) } catch (error) { if (error instanceof UserInputError) { this.emit('userInputError', `${key}: ${error.message}`) } else { throw error } } } this._object[key] = array } return this } /** Defines a key that takes an number value, * optionally clamped between min and max. * * @param {!string} key - The key. * @param {?number} min - Minimum value returned. * @param {?number} max - Maximum value returned. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ numberKey (key, min, max) { key = this._toKey(key) min = min == null ? -Infinity : OptionParser.toNumber('min', min) max = max == null ? Infinity : OptionParser.toNumber('max', max) if (max < min) { throw newRangeError('max: smaller than min') } this._callbacks[key] = (value) => { this._object[key] = OptionParser.toNumber(key, value, min, max, this._userInput) } return this } /** Defines a key that takes an object as value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ objectKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toObject(key, value, this._userInput) } return this } /** Defines a key that takes a resource path as value. * * @param {!string} key - The key. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ pathKey (key) { key = this._toKey(key) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toPath(key, value, this._userInput) } return this } /** Defines a key that takes a string value. * * @param {!string} key - The key. * @param {boolean} [nonEmpty=false] - Reject empty string. * @return {OptionParser} this - For chaining. * @throws {TypeError} When key is not a string. * @throws {RangeError} When key is empty string. * @throws {SyntaxError} On duplicate key. */ stringKey (key, nonEmpty = false) { key = this._toKey(key) this._callbacks[key] = (value) => { this._object[key] = OptionParser.toString( key, value, nonEmpty, this._userInput ) } return this } /** Parse options. * * @param {object} options - The input options. * @param {?object} defaults - The default values, to be overwritten by * the corresponding values in `options`. * @returns {object} The * @throws {TypeError} When option has wrong type. * @throws {RangeError} When option has wrong value. * @throws {SyntaxError} Unknown option. * @throws {UserInputError} On error, when value was input by user. */ parse (options) { options = OptionParser.toObject('options', options) for (const key in options) { try { const value = options[key] if (this._callbacks[key] == null) { throw newSyntaxError(`${key}: invalid key`, this._userInput) } this._callbacks[key](value) } catch (error) { if (error instanceof UserInputError) { // this.emit('userInputError', `${key}: ${error.message}`) this.emit('userInputError', error) } else { // error.message = `${key}: ${error.message}` throw error } } } return this._object } } export { OptionParser }