UNPKG

hb-lib-tools

Version:

homebridge-lib Command-Line Tools`

319 lines (298 loc) 10.4 kB
// hb-lib-tools/lib/CommandLineParser.js // // Library for Homebridge plugins. // Copyright © 2017-2025 Erik Baauw. All rights reserved. // TODO: // - Change parameters to (params, callback) with: // - params.shortKey // - params.longKey // - params.key // - params.optional // - params.minumum // - params.helpText import { createRequire } from 'node:module' import { recommendedNodeVersion } from 'hb-lib-tools' import { OptionParser } from 'hb-lib-tools/OptionParser' const require = createRequire(import.meta.url) const packageJson = require('../package.json') /** Usage error. * @hideconstructor * @extends Error * @memberof CommandLineParser */ class UsageError extends Error {} /** Parser and validator for command-line arguments. * <br>See {@link CommandLineParser}. * @name CommandLineParser * @type {Class} * @memberof module:hb-lib-tools */ /** Parser and validator for command-line arguments. */ class CommandLineParser { static get UsageError () { return UsageError } /** Create a new parser instance. * @params {string} pkgJson - The contents of `package.json` to retrieve * the version and homepage for the command-line tool. */ constructor (pkgJson = packageJson) { this._callbacks = { flags: {}, options: {}, parameters: [], remaining: null } this._packageJson = pkgJson } _toShort (value) { if (value == null) { return null } if (typeof value !== 'string' || value.length !== 1) { throw new TypeError(`${value}: invalid short key`) } if (this._callbacks.flags[value] != null) { throw new SyntaxError(`${value}: duplicate short key`) } return value } _toLong (value) { if (value == null) { return null } if (typeof value !== 'string' || value.length === 1) { throw new TypeError(`${value}: invalid long key`) } if (this._callbacks.options[value] != null) { throw new SyntaxError(`${value}: duplicate long key`) } return value } /** Add a flag to print help text and exit. * * See {@link CommandLineParser#flag flag()}. * * For now, the help text needs to be specified explicitly. * <br>TODO: Generate helpText automatically from * {@link CommandLineParser#flag flag()}, * {@link CommandLineParser#option option()}, * {@link CommandLineParser#parameter parameter()}, and * {@link CommandLineParser#remaining remaining()} * @param {string} shortKey - The short key (e.g. `h` for `-h`). * @param {string} longKey - The long key (e.g. `help` for `--help`). * @param {string} helpText - The help text. * @return {CommandLineParser} this - For chaining. */ help (shortKey, longKey, helpText) { helpText = OptionParser.toString('helpText', helpText, true) this.flag(shortKey, longKey, () => { const recommendedVersion = recommendedNodeVersion(packageJson) const warning = (process.version.slice(1) !== recommendedVersion) ? `, recommended version: node v${recommendedVersion}` : '' console.log(helpText) console.log(` See ${this._packageJson.homepage.split('#')[0]} for more info. (${this._packageJson.name} v${this._packageJson.version}, node ${process.version}${warning})` ) process.exit(0) }) return this } /** Add a flag to print the version and exit. * * See {@link CommandLineParser#flag flag()}. * * @param {string} shortKey - The short key (e.g. `V` for `-V`). * @param {string} longKey - The long key (e.g. `version` for `--version`). * @return {CommandLineParser} this - For chaining. */ version (shortKey, longKey) { this.flag(shortKey, longKey, () => { console.log(this._packageJson.version) process.exit(0) }) return this } /** Add a callback for a flag. * * A flag is an optional command-line parameter, identified by a short key * (a single character, like `-v`), or by a long key (a word, like * `--verbose`). * * @param {string} shortKey - The short key (e.g. `v` for `-v`). * @param {string} longKey - The long key (e.g. `verbose` for `--verbose`). * @param {function} callback - The callback function.<br> * The function will be called when the flag is present, with the * following parameters: * * Name | Type | Attributes | Description * ---- | ---- | ---------- | ----------- * `key` | string | | The key. * @return {CommandLineParser} this - For chaining. */ flag (shortKey, longKey, callback) { shortKey = this._toShort(shortKey) longKey = this._toLong(longKey) callback = OptionParser.toFunction('callback', callback) if (shortKey != null) { this._callbacks.flags[shortKey] = callback } if (longKey != null) { this._callbacks.flags[longKey] = callback } return this } /** Add a callback for an option. * * An option is an optional command-line paramater that takes a value. * The option is identified by a short key (a single character, like `-t`), * or by a long key (a word, like `--timeout`). * The value can specified in the next or in the same command-line parameter: * `-t5` `--timeout=5`, `-t 5`, or `--timeout 5` * * @param {string} shortKey - The short key (e.g. `t` for `-t`). * @param {string} longKey - The long key (e.g. `timeout` for `--timeout`). * @param {function} callback - The callback function.<br> * The function will be called when the option is present, with the * following parameters: * * Name | Type | Attributes | Description * ---- | ---- | ---------- | ----------- * `value` | string | | The value. * `key` | string | | The (short or long) key. * @return {CommandLineParser} this - For chaining. */ option (shortKey, longKey, callback) { shortKey = this._toShort(shortKey) longKey = this._toLong(longKey) callback = OptionParser.toFunction('callback', callback) if (shortKey != null) { this._callbacks.options[shortKey] = callback } if (longKey != null) { this._callbacks.options[longKey] = callback } return this } /** Add a callback for a positional parameter. * * A positional paramater is a mandatory command-line parameter. * It is specified as a single value, e.g. `get` * * @param {string} key - The parameter key (e.g. `command`). * @param {function} callback - The callback function.<br> * The function will be called with the following parameters: * * Name | Type | Attributes | Description * ---- | ---- | ---------- | ----------- * `value` | string | | The parameter value. * `key` | string | | The parameter key. * @return {CommandLineParser} this - For chaining. */ parameter (key, callback, optional = false) { key = OptionParser.toString('key', key, true) callback = OptionParser.toFunction('callback', callback) this._callbacks.parameters.push({ key, callback, optional }) return this } // * @param {string} key - The name of the remaining parameters (e.g. // * `file` for `[`_file_` ...]`). /** Add a callback for the remaining parameters. * * The remaining parameters are any additional commmand-line parameters, * after the positional paramers, typically indicated as `[file ...]`. * @param {function} callback - The callback function.<br> * This function will be called with the following paramters: * * Name | Type | Attributes | Description * ---- | ---- | ---------- | ----------- * `values` | string[] | | A list of values of the remaining parameters. * @return {CommandLineParser} this - For chaining. */ remaining (/* key, */ callback) { callback = OptionParser.toFunction('callback', callback) this._callbacks.remaining = callback return this } /** Parse the command-line parameters. * * @throws {UsageError} In case of invalid command-line paramters. */ parse (wordList = process.argv.slice(2)) { // process.argv[0]: node executable, process.argv[1]: javascript file wordList = OptionParser.toArray('wordList', wordList) let wordIndex = 0 let charIndex function handleWord (word, long) { const key = long ? word.split('=')[0] : word[0] const option = (long ? '--' : '-') + key let value = long ? word.split('=')[1] : null let callback = this._callbacks.flags[key] if (callback) { if (value != null) { throw new UsageError(`${option}: option doesn't allow an argument`) } callback(option) return long } callback = this._callbacks.options[key] if (callback) { value = long ? word.split('=')[1] : word.substring(1) if (value) { charIndex = word.length } else { if (wordIndex >= wordList.length) { throw new UsageError(`${option}: option requires an argument`) } value = wordList[wordIndex++] } callback(value, option) return long } throw new UsageError(`${option}: unknown option`) } // Parse flags and options. while (wordIndex < wordList.length) { const word = wordList[wordIndex++] if (word[0] !== '-' || word === '-') { wordIndex -= 1 break } if (word === '--') { break } if (word[1] === '-') { handleWord.call(this, word.substring(2), true) continue } charIndex = 1 while (charIndex < word.length) { if (handleWord.call(this, word.substring(charIndex++), false)) { break } } } // Parse parameters. for (const p of this._callbacks.parameters) { if (wordIndex >= wordList.length) { if (!p.optional) { throw new UsageError(`parameter ${p.key} missing`) } break } const parameter = wordList[wordIndex++] p.callback(parameter) } const remaining = wordList.slice(wordIndex, wordList.length) const callback = this._callbacks.remaining if (callback) { callback(remaining) return } if (remaining.length > 0) { throw new UsageError('too many parameters') } } } export { CommandLineParser }