From 33197822ef1bbcefe8878531598cea25088595fc Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 8 Jan 2025 08:48:57 -0800 Subject: [PATCH] feat: add unknownHandler and abbrevHandler This is in favor of proc-log --- README.md | 17 ++++---- lib/nopt-lib.js | 48 ++++++++++++++++------ lib/nopt.js | 4 ++ test/basic.js | 51 +++++++++++++++++++++++ test/lib.js | 107 +++++++++++++++++++++++++++++++++--------------- 5 files changed, 175 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index a99531c..19ef097 100644 --- a/README.md +++ b/README.md @@ -141,14 +141,15 @@ config object and remove its invalid properties. ## Error Handling -By default, nopt outputs a warning to standard error when invalid values for -known options are found. You can change this behavior by assigning a method -to `nopt.invalidHandler`. This method will be called with -the offending `nopt.invalidHandler(key, val, types)`. - -If no `nopt.invalidHandler` is assigned, then it will console.error -its whining. If it is assigned to boolean `false` then the warning is -suppressed. +By default nopt logs debug messages if `DEBUG_NOPT` or `NOPT_DEBUG` are set in the environment. + +You can assign the following methods to `nopt` for a more granular notification of invalid, unknown, and expanding options: + +`nopt.invalidHandler(key, value, type, data)` - Called when a value is invalid for its option. +`nopt.unknownHandler(key, next)` - Called when an option is found that has no configuration. In certain situations the next option on the command line will be parsed on its own instead of as part of the unknown option. In this case `next` will contain that option. +`nopt.abbrevHandler(short, long)` - Called when an option is automatically translated via abbreviations. + +You can also set any of these to `false` to disable the debugging messages that they generate. ## Abbreviations diff --git a/lib/nopt-lib.js b/lib/nopt-lib.js index fc34ee5..441c9cc 100644 --- a/lib/nopt-lib.js +++ b/lib/nopt-lib.js @@ -1,7 +1,6 @@ const abbrev = require('abbrev') const debug = require('./debug') const defaultTypeDefs = require('./type-defs') -const { log } = require('proc-log') const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k) @@ -26,7 +25,9 @@ function nopt (args, { types, shorthands, typeDefs, - invalidHandler, + invalidHandler, // opt is configured but its value does not validate against given type + unknownHandler, // opt is not configured + abbrevHandler, // opt is being expanded via abbrev typeDefault, dynamicTypes, } = {}) { @@ -39,7 +40,9 @@ function nopt (args, { original: args.slice(0), } - parse(args, data, argv.remain, { typeDefs, types, dynamicTypes, shorthands }) + parse(args, data, argv.remain, { + typeDefs, types, dynamicTypes, shorthands, unknownHandler, abbrevHandler, + }) // now data is full clean(data, { types, dynamicTypes, typeDefs, invalidHandler, typeDefault }) @@ -248,6 +251,8 @@ function parse (args, data, remain, { typeDefs = {}, shorthands = {}, dynamicTypes, + unknownHandler, + abbrevHandler, } = {}) { const StringType = typeDefs.String?.type const NumberType = typeDefs.Number?.type @@ -283,7 +288,7 @@ function parse (args, data, remain, { // see if it's a shorthand // if so, splice and back up to re-parse it. - const shRes = resolveShort(arg, shortAbbr, abbrevs, { shorthands }) + const shRes = resolveShort(arg, shortAbbr, abbrevs, { shorthands, abbrevHandler }) debug('arg=%j shRes=%j', arg, shRes) if (shRes) { args.splice.apply(args, [i, 1].concat(shRes)) @@ -301,8 +306,11 @@ function parse (args, data, remain, { // abbrev includes the original full string in its abbrev list if (abbrevs[arg] && abbrevs[arg] !== arg) { - /* eslint-disable-next-line max-len */ - log.warn(`Expanding "--${arg}" to "--${abbrevs[arg]}". This will stop working in the next major version of npm.`) + if (abbrevHandler) { + abbrevHandler(arg, abbrevs[arg]) + } else if (abbrevHandler !== false) { + debug(`abbrev: ${arg} -> ${abbrevs[arg]}`) + } arg = abbrevs[arg] } @@ -335,9 +343,21 @@ function parse (args, data, remain, { (argType === null || isTypeArray && ~argType.indexOf(null))) - if (typeof argType === 'undefined' && !hadEq && la && !la?.startsWith('-')) { - // npm itself will log the warning about the undefined argType - log.warn(`"${la}" is being parsed as a normal command line argument.`) + if (typeof argType === 'undefined') { + // la is going to unexpectedly be parsed outside the context of this arg + const hangingLa = !hadEq && la && !la?.startsWith('-') && !['true', 'false'].includes(la) + if (unknownHandler) { + if (hangingLa) { + unknownHandler(arg, la) + } else { + unknownHandler(arg) + } + } else if (unknownHandler !== false) { + debug(`unknown: ${arg}`) + if (hangingLa) { + debug(`unknown: ${la} parsed as normal opt`) + } + } } if (isBool) { @@ -429,7 +449,7 @@ const singleCharacters = (arg, shorthands) => { } function resolveShort (arg, ...rest) { - const { types = {}, shorthands = {} } = rest.length ? rest.pop() : {} + const { abbrevHandler, types = {}, shorthands = {} } = rest.length ? rest.pop() : {} const shortAbbr = rest[0] ?? abbrev(Object.keys(shorthands)) const abbrevs = rest[1] ?? abbrev(Object.keys(types)) @@ -466,9 +486,13 @@ function resolveShort (arg, ...rest) { } // if it's an abbr for a shorthand, then use that + // exact match has already happened so we don't need to account for that here if (shortAbbr[arg]) { - /* eslint-disable-next-line max-len */ - log.warn(`Expanding "--${arg}" to "--${shortAbbr[arg]}". This will stop working in the next major version of npm.`) + if (abbrevHandler) { + abbrevHandler(arg, shortAbbr[arg]) + } else if (abbrevHandler !== false) { + debug(`abbrev: ${arg} -> ${shortAbbr[arg]}`) + } arg = shortAbbr[arg] } diff --git a/lib/nopt.js b/lib/nopt.js index 37f01a0..9a24342 100644 --- a/lib/nopt.js +++ b/lib/nopt.js @@ -18,6 +18,8 @@ function nopt (types, shorthands, args = process.argv, slice = 2) { shorthands: shorthands || {}, typeDefs: exports.typeDefs, invalidHandler: exports.invalidHandler, + unknownHandler: exports.unknownHandler, + abbrevHandler: exports.abbrevHandler, }) } @@ -26,5 +28,7 @@ function clean (data, types, typeDefs = exports.typeDefs) { types: types || {}, typeDefs, invalidHandler: exports.invalidHandler, + unknownHandler: exports.unknownHandler, + abbrevHandler: exports.abbrevHandler, }) } diff --git a/test/basic.js b/test/basic.js index 0f5b738..f8b3a2c 100644 --- a/test/basic.js +++ b/test/basic.js @@ -346,6 +346,57 @@ t.test('custom invalidHandler', t => { nopt({ key: Number }, {}, ['--key', 'nope'], 0) }) +t.test('custom unknownHandler string', t => { + t.teardown(() => { + delete nopt.unknownHandler + }) + nopt.unknownHandler = (k, next) => { + t.match(k, 'x') + t.match(next, 'null') + t.end() + } + nopt({}, {}, ['--x', 'null'], 0) +}) + +t.test('custom unknownHandler boolean', t => { + t.teardown(() => { + delete nopt.unknownHandler + }) + nopt.unknownHandler = (k, next) => { + t.match(k, 'x') + t.match(next, undefined) + t.end() + } + nopt({}, {}, ['--x', 'false'], 0) +}) + +t.test('custom normal abbrevHandler', t => { + t.teardown(() => { + delete nopt.abbrevHandler + }) + nopt.abbrevHandler = (short, long) => { + t.match(short, 'shor') + t.match(long, 'shorthand') + t.end() + } + nopt({ shorthand: Boolean }, {}, ['--short', 'true'], 0) +}) + +t.test('custom shorthand abbrevHandler', t => { + t.teardown(() => { + delete nopt.abbrevHandler + }) + nopt.abbrevHandler = (short, long) => { + t.match(short, 'shor') + t.match(long, 'shorthand') + t.end() + } + nopt({ + longhand: Boolean, + }, { shorthand: '--longhand' }, + ['--short', 'true'], 0) +}) + t.test('numbered boolean', t => { const parsed = nopt({ key: [Boolean, String] }, {}, ['--key', '0'], 0) t.same(parsed.key, false) diff --git a/test/lib.js b/test/lib.js index 21f9a23..962ca34 100644 --- a/test/lib.js +++ b/test/lib.js @@ -2,24 +2,13 @@ const t = require('tap') const noptLib = require('../lib/nopt-lib.js') const Stream = require('stream') -const logs = [] -t.afterEach(() => { - logs.length = 0 -}) -process.on('log', (...msg) => { - logs.push(msg) -}) - -const nopt = (t, argv, opts, expected, expectedLogs) => { +const nopt = (t, argv, opts, expected) => { if (Array.isArray(argv)) { t.strictSame(noptLib.nopt(argv, { typeDefs: noptLib.typeDefs, ...opts }), expected) } else { noptLib.clean(argv, { typeDefs: noptLib.typeDefs, ...opts }) t.match(argv, expected) } - if (expectedLogs) { - t.match(expectedLogs, logs) - } t.end() } @@ -136,41 +125,97 @@ t.test('false invalid handler', (t) => { }) }) -t.test('longhand abbreviation', (t) => { - nopt(t, ['--lon', 'text'], { +t.test('false unknown handler string', (t) => { + // this is only for coverage + nopt(t, ['--x', 'null'], { + unknownHandler: false, + }, { + x: true, + argv: { + remain: ['null'], + cooked: ['--x', 'null'], + original: ['--x', 'null'], + }, + }) +}) + +t.test('default unknown handler opt', (t) => { + // this is only for coverage + nopt(t, ['--x', '--y'], {}, { + x: true, + y: true, + argv: { + remain: [], + cooked: ['--x', '--y'], + original: ['--x', '--y'], + }, + }) +}) + +t.test('false abbrev handler normal', (t) => { + // this is only for coverage + nopt(t, ['--long', 'true'], { + types: { + longhand: Boolean, + }, + abbrevHandler: false, + }, { + longhand: true, + argv: { + remain: [], + cooked: ['--long', 'true'], + original: ['--long', 'true'], + }, + }) +}) + +t.test('false abbrev handler shorthand', (t) => { + // this is only for coverage + nopt(t, ['--shor', 'true'], { + types: {}, + shorthands: { + short: '--longhand', + }, + abbrevHandler: false, + }, { + longhand: true, + argv: { + remain: [], + cooked: ['--longhand', 'true'], + original: ['--shor', 'true'], + }, + }) +}) + +t.test('normal abbreviation', (t) => { + nopt(t, ['--shor', 'text'], { types: { - long: String, + shorthand: String, }, }, { - long: 'text', + shorthand: 'text', argv: { remain: [], - cooked: ['--lon', 'text'], - original: ['--lon', 'text'], + cooked: ['--shor', 'text'], + original: ['--shor', 'text'], }, - }, [ - /* eslint-disable-next-line max-len */ - ['warn', 'Expanding "--lon" to "--long". This will stop working in the next major version of npm.'], - ]) + }) }) t.test('shorthand abbreviation', (t) => { nopt(t, ['--shor'], { types: {}, shorthands: { - short: '--shorthand', + short: '--longhand', }, }, { - shorthand: true, + longhand: true, argv: { remain: [], - cooked: ['--shorthand'], + cooked: ['--longhand'], original: ['--shor'], }, - }, [ - /* eslint-disable-next-line max-len */ - ['warn', 'Expanding "--shor" to "--short". This will stop working in the next major version of npm.'], - ]) + }) }) t.test('shorthands that is the same', (t) => { @@ -199,7 +244,5 @@ t.test('unknown multiple', (t) => { cooked: ['--mult', '--mult', '--mult', 'extra'], original: ['--mult', '--mult', '--mult', 'extra'], }, - }, [ - ['warn', '"extra" is being parsed as a normal command line argument.'], - ]) + }) })