From f43915c735509c55b858dc3a6ec5aeebf4fe4fc7 Mon Sep 17 00:00:00 2001 From: n loewen Date: Sat, 23 Sep 2023 18:29:05 -0700 Subject: [PATCH] Breaking - Change: Rewrite with a new interface --- new-interface-sketch.js | 21 ------ opter.js | 149 ++++++++++++++++------------------------ test.js | 83 ++-------------------- 3 files changed, 66 insertions(+), 187 deletions(-) delete mode 100644 new-interface-sketch.js diff --git a/new-interface-sketch.js b/new-interface-sketch.js deleted file mode 100644 index 4f047bb..0000000 --- a/new-interface-sketch.js +++ /dev/null @@ -1,21 +0,0 @@ -/* -const opter = function (argv) { - return new Opter(argv); -} -*/ - -const cfgExample = { - synonyms: [['-d', '--debug']], - requiredOptions: ['-i', '-o'], - requiredArguments: { '-i': {min: 1, max: 1, tooFew: '', tooMany: ''}, } -} - -class Opter { - constructor(argv, cfg) { - // this.opts = ... - } - - contains() { - - } -} \ No newline at end of file diff --git a/opter.js b/opter.js index 1374678..e739eac 100644 --- a/opter.js +++ b/opter.js @@ -1,112 +1,83 @@ -// https://unix.stackexchange.com/questions/364383/confusion-about-changing-meaning-of-arguments-and-options-is-there-an-official -// https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/basedefs/V1_chap12.html#tag_12_01 +/* For reference: -// 'options' are --foo, -f -// -// 'option-arguments' are (non-option) things that follow an option, and that the option expects -// eg. 'filename.txt' in './example --input filename.txt' -// -// 'operands' are (non-option) things that follow an option, but aren't option-arguments -// eg 'filename.txt' in './example --verbose filename.txt' + - 'options' are flags preceded by one or two dashs: + eg. -f, --foo + + - 'option-arguments' are (non-option) strings that follow an option (and go with it) + eg. 'filename.txt' in './example --input filename.txt' + + - 'operands' are (non-option) strings that follow an option, but aren't option-arguments + eg 'filename.txt' in './example --verbose filename.txt' -const defaultErrorMessages = { - missingRequiredOption: (optionName) => `Missing required option '${optionName}'`, - missingRequiredArgument: (optionName) => `Missing argument for option ${optionName}`, - tooManyArguments: (optionName) => `Too many arguments for option ${optionName}`, -} + see https://unix.stackexchange.com/questions/364383/confusion-about-changing-meaning-of-arguments-and-options-is-there-an-official + and https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/basedefs/V1_chap12.html#tag_12_01 +*/ -// TODO figure out how to avoid having to use '.opts to access the output of this' - -module.exports = class Opts { - constructor(argv) { - this.opts = get(argv); - this.synonyms = []; - } - - // TODO equip prototype with ability to print directly through `console.log(opts)` +module.exports = class Opter { + synonyms = {}; + definedOptions = {}; + userOptions = {}; /** - * Declare multiple option flags to be synonymous. - * Use this if you have short and long names for the same flag - * eg: '-d' and '--debug' - * - * @param {Array} namesWithDashes - * @returns {void} + * @param {string} shortFlag + * @param {string} longFlag + * @param {boolean} [required] + * @param {boolean} [requiresArgument] + * @param {number} [maxArguments] **/ - synonymize(...namesWithDashes) { - const names = namesWithDashes.map(n => stripDashes(n)); - names.forEach((n) => { - const others = names.filter(x => x != n) - this.synonyms[n] = others; - }); + addOption(shortFlag, longFlag, required=false, requiresArgument=false, maxArguments=Infinity) { + let reqs = { required: required, requiresArgument: requiresArgument, maxArguments: maxArguments }; + const short = stripDashes(shortFlag); + const long = stripDashes(longFlag); + this.definedOptions[long] = reqs; + this.synonyms[long] = short; } /** - * Verify the presence of a required option - * @param {string} nameWithDashes - * @param {string} [errorMessage] - * @returns {Boolean} **True** indicates that the option is present + * @param {object} argv An argv object (`process.argv`) + * @returns {Object>} **/ - requireOption(nameWithDashes, errorMessage=null) { - const name = stripDashes(nameWithDashes); - if (name in this.opts) return true; - if (name in this.synonyms) { - const syns = this.synonyms[name]; - const hits = syns.filter(s => s in this.opts ).length; - if (hits > 0) { return true; } - } - throw new Error(errorMessage || defaultErrorMessages.missingRequiredOption(nameWithDashes)); - } + parse(argv) { + this.userOptions = argvToObject(argv); - /** - * Verify the presence of arguments for an option that requires them, - * if that option is present - * @param {string} nameWithDashes - * @param {number} [minRequired] - * @param {number} [maxRequired] - * @param {string} [tooFewMessage] Error message if there are too few arguments provided - * @param {string} [tooManyMessage] Error message if there are too many arguments provided - * @returns {Boolean} **True** indicates that the arguments are present, or that the option is *not* present - **/ - requireOptionArgument(nameWithDashes, minRequired=1, maxRequired=1, tooFewMessage=null, tooManyMessage=null) { - let name = stripDashes(nameWithDashes); + Object.keys(this.definedOptions).forEach(longName => { + let shortName = this.synonyms[longName]; + let reqs = this.definedOptions[longName]; + let providedLong = typeof this.userOptions[longName] !== 'undefined'; + let providedShort = typeof this.userOptions[shortName] !== 'undefined'; + let provided = providedLong || providedShort; - const checkArgs = (name) => { - if (typeof this.opts[name] === 'boolean') { - throw new Error(tooFewMessage || defaultErrorMessages.missingRequiredArgument(nameWithDashes)); - } else if (this.opts[name].length < minRequired) { - throw new Error(tooFewMessage || defaultErrorMessages.missingRequiredArgument(nameWithDashes)); - } else if (this.opts[name].length > maxRequired) { - throw new Error(tooManyMessage || defaultErrorMessages.tooManyArguments(nameWithDashes)); + let userArgs = null; + // arrays have type 'object': + if (typeof this.userOptions[longName] === 'object') { userArgs = this.userOptions[longName]; } + if (typeof this.userOptions[shortName] === 'object') { userArgs = this.userOptions[shortName]; } + + /* Check that required options are provided */ + if (reqs.required && !provided) { + throw new Error(`Missing required option '${longName}'`); } - } - if (name in this.opts) { - checkArgs(name); - } - if (name in this.synonyms) { - const syn = this.synonyms[name].filter(s => s in this.opts )[0]; - checkArgs(syn); - } - return true; - } + /* Check the number of arguments */ + if (provided && reqs.requiresArgument) { + // Make sure required arguments are provided + if (userArgs === null) { + throw new Error(`Missing required argument for '${longName}'`); + } + // And make sure there aren't too many + if (userArgs.length > reqs.maxArguments) { + throw new Error(`Too many arguments for '${longName}'`); + } + } + }) - contains(nameWithDashes) { - const name = stripDashes(nameWithDashes); - if (name in this.opts) return true; - if (name in this.synonyms) { - const syns = this.synonyms[name]; - const hits = syns.filter(s => s in this.opts).length; - if (hits > 0) { return true; } - } - return false; + return this.userOptions; } } /** - * @returns [ string: (boolean | string[]) ] + * @returns {Object.>} **/ -function get(argv) { +function argvToObject(argv) { argv = process.argv.slice(2); // Label everything in argv as either diff --git a/test.js b/test.js index a8ffe42..03f61fb 100644 --- a/test.js +++ b/test.js @@ -1,80 +1,9 @@ const Opter = require('./opter.js'); -// const args = argparser.parse(process.argv); -// console.log('args', args); -// console.log('args', args); -// requireOption(args, '--i'); -// argsrequire(args, '-i'); +const opter = new Opter(); +opter.addOption('-i', '--input', true); +opter.addOption('-d', '--debug'); +opter.addOption('-f', '--filename', false, true); +let opts = opter.parse(process.argv); -const opts = new Opter(process.argv) -opts.synonymize('-i', '--input'); -opts.synonymize('-d', '--debug'); -opts.requireOption('-i'); -opts.requireOptionArgument('--input'); -console.log('contains --debug', opts.contains('-d')); -console.log('contains --input', opts.contains('--input')); - -// const _defaultErrorMessages = { -// missingRequiredOption: (optionName) => `Missing required option '${optionName}'`, -// missingRequiredArgument: (optionName) => `Missing argument for option ${optionName}`, -// tooManyArguments: (optionName) => `Too many arguments for option ${optionName}`, -// } -// -// /** -// * Declare multiple option flags to be synonymous. -// * Use this if you have short and long names for the same flag -// * eg: '-d' and '--debug' -// * -// * @param {Array} namesWithDashes -// * @returns {void} -// **/ -// function synonymize(opts /* <-- TODO tmp */, ...namesWithDashes) { -// namesWithDashes.slice(1).forEach((n) => { -// const x = _stripDashes(n); -// this.opts[x] = opts[namesWithDashes[0]]; // TODO check 'this' works -// }); -// } -// -// function _stripDashes(optionName) { -// if (optionName.startsWith('--')) return optionName.substring(2); -// if (optionName.startsWith('-')) return optionName.substring(1); -// return optionName; -// } -// -// /** -// * Verify the presence of a required option -// * @param {string} nameWithDashes -// * @param {string} [errorMessage] -// * @returns {Boolean} **True** indicates that the option is present -// **/ -// function requireOption(userProvidedOpts, nameWithDashes, errorMessage=null) { -// if (_stripDashes(nameWithDashes) in userProvidedOpts) return true; -// throw new Error(errorMessage || _defaultErrorMessages.missingRequiredOption(nameWithDashes)); -// } -// -// /** -// * Verify the presence of arguments for an option that requires them, -// * if that option is present -// * @param {string} nameWithDashes -// * @param {number} [minRequired] -// * @param {number} [maxRequired] -// * @param {string} [tooFewMessage] Error message if there are too few arguments provided -// * @param {string} [tooManyMessage] Error message if there are too many arguments provided -// * @returns {Boolean} **True** indicates that the arguments are present, or that the option is *not* present -// **/ -// // TODO remove first param when making this a method on an Args object -// function argsrequire(userProvidedArgs, nameWithDashes, minRequired=1, maxRequired=1, tooFewMessage=null, tooManyMessage=null) { -// let name = _stripDashes(nameWithDashes); -// if (name in userProvidedArgs) { -// console.log('type', typeof userProvidedArgs[name] === 'boolean'); -// console.log(userProvidedArgs[name]); -// if (typeof userProvidedArgs[name] === 'boolean') { -// throw new Error(tooFewMessage || _defaultErrorMessages.missingRequiredArgument(nameWithDashes)); -// } else if (userProvidedArgs[name].length < minRequired) { -// throw new Error(tooFewMessage || _defaultErrorMessages.missingRequiredArgument(nameWithDashes)); -// } else if (userProvidedArgs[name].length > maxRequired) { -// throw new Error(tooManyMessage || _defaultErrorMessages.tooManyArguments(nameWithDashes)); -// } -// } -// return true; -// } \ No newline at end of file +console.log(opts); \ No newline at end of file