Skip to content

Commit

Permalink
feat: add unknownHandler and abbrevHandler
Browse files Browse the repository at this point in the history
This is in favor of proc-log
  • Loading branch information
wraithgar committed Jan 14, 2025
1 parent bd7f53a commit 84ff74c
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 52 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 36 additions & 12 deletions lib/nopt-lib.js
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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,
} = {}) {
Expand All @@ -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 })
Expand Down Expand Up @@ -248,6 +251,8 @@ function parse (args, data, remain, {
typeDefs = {},
shorthands = {},
dynamicTypes,
unknownHandler,
abbrevHandler,
} = {}) {
const StringType = typeDefs.String?.type
const NumberType = typeDefs.Number?.type
Expand Down Expand Up @@ -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))
Expand All @@ -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]
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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]
}

Expand Down
4 changes: 4 additions & 0 deletions lib/nopt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}

Expand All @@ -26,5 +28,7 @@ function clean (data, types, typeDefs = exports.typeDefs) {
types: types || {},
typeDefs,
invalidHandler: exports.invalidHandler,
unknownHandler: exports.unknownHandler,
abbrevHandler: exports.abbrevHandler,
})
}
51 changes: 51 additions & 0 deletions test/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
107 changes: 75 additions & 32 deletions test/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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.'],
])
})
})

0 comments on commit 84ff74c

Please sign in to comment.