From e2c556df682cf12d4722103c476cf0c93dde7701 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 | 41 +++++++++++++++++++++------- lib/nopt.js | 4 +++ test/basic.js | 36 +++++++++++++++++++++++++ test/lib.js | 71 +++++++++++++++++++++++++++++++++---------------- 5 files changed, 128 insertions(+), 41 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..ce668e7 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 or shorthand 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 @@ -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(`expand: ${arg} -> ${abbrevs[arg]}`) + } arg = abbrevs[arg] } @@ -335,9 +343,24 @@ 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 not being parsed as a value for this arg + const hangingLa = !hadEq && la && !la?.startsWith('-') + if (!hadEq && la && !la?.startsWith('-')) { + if (unknownHandler) { + if (hangingLa && !['true', 'false'].includes(la)) { + // la is going to unexpectedly be parsed outside the context of this arg + unknownHandler(arg, la) + } else { + unknownHandler(arg) + } + } else if (unknownHandler !== false) { + debug(`unknown: ${arg}`) + if (hangingLa && !['true', 'false'].includes(la)) { + debug(`unknown: ${la} parsed as normal opt`) + } + } + } } if (isBool) { @@ -467,8 +490,6 @@ function resolveShort (arg, ...rest) { // if it's an abbr for a shorthand, then use that 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.`) 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..60e9910 100644 --- a/test/basic.js +++ b/test/basic.js @@ -346,6 +346,42 @@ 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 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('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..d4515e0 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,6 +125,50 @@ t.test('false invalid handler', (t) => { }) }) +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', (t) => { + // this is only for coverage + nopt(t, ['--short', 'true'], { + types: { + shorthand: Boolean, + }, + abbrevHandler: false, + }, { + shorthand: true, + argv: { + remain: [], + cooked: ['--short', 'true'], + original: ['--short', 'true'], + }, + }) +}) + t.test('longhand abbreviation', (t) => { nopt(t, ['--lon', 'text'], { types: { @@ -148,10 +181,7 @@ t.test('longhand abbreviation', (t) => { cooked: ['--lon', 'text'], original: ['--lon', '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) => { @@ -167,10 +197,7 @@ t.test('shorthand abbreviation', (t) => { cooked: ['--shorthand'], 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 +226,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.'], - ]) + }) })