From 6378f683e7e8b0571cd38825e33af743a12c5373 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 24 Jun 2023 12:31:25 +1200 Subject: [PATCH 01/15] Add editorConfig --- .editorconfig | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7597855 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: +http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*.js] +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 From c88b33563bc9a4467ed6597492a292abf7a45ac8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 24 Jun 2023 12:36:32 +1200 Subject: [PATCH 02/15] First cut at strict - throw for missing string option value - throw for unexpected value for boolean option --- index.js | 35 ++++++++++++------ test/strict.js | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 test/strict.js diff --git a/index.js b/index.js index 78cafa8..22c524e 100644 --- a/index.js +++ b/index.js @@ -121,15 +121,33 @@ module.exports = function (args, opts) { } function setArg(key, val, arg) { + // validation if (arg && flags.unknownFn && !argDefined(key, arg)) { if (flags.unknownFn(arg) === false) { return; } } + if (opts.strict) { + if (flags.strings[key] && val === true) { + throw new Error('Missing option value for option "' + key + '"'); + } + if (isBooleanKey(key) && typeof val === 'string' && !(/^(true|false)$/).test(val)) { + throw new Error('Unexpected option value for option "' + key + '"'); + } + } - var value = !flags.strings[key] && isNumber(val) - ? Number(val) - : val; - setKey(argv, key.split('.'), value); + // coercion + var value = val; + if (!flags.strings[key] && isNumber(val)) { + value = Number(val); + } + if (flags.strings[key] && val === true) { + value = ''; + } + if (isBooleanKey(key) && typeof val === 'string') { + value = value !== 'false'; + } + // store + setKey(argv, key.split('.'), value); (aliases[key] || []).forEach(function (x) { setKey(argv, x.split('.'), value); }); @@ -162,9 +180,6 @@ module.exports = function (args, opts) { var m = arg.match(/^--([^=]+)=([\s\S]*)$/); key = m[1]; var value = m[2]; - if (isBooleanKey(key)) { - value = value !== 'false'; - } setArg(key, value, arg); } else if ((/^--no-.+/).test(arg)) { key = arg.match(/^--no-(.+)/)[1]; @@ -184,7 +199,7 @@ module.exports = function (args, opts) { setArg(key, next === 'true', arg); i += 1; } else { - setArg(key, flags.strings[key] ? '' : true, arg); + setArg(key, true, arg); } } else if ((/^-[^-]+/).test(arg)) { var letters = arg.slice(1, -1).split(''); @@ -218,7 +233,7 @@ module.exports = function (args, opts) { broken = true; break; } else { - setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg); + setArg(letters[j], true); } } @@ -235,7 +250,7 @@ module.exports = function (args, opts) { setArg(key, args[i + 1] === 'true', arg); i += 1; } else { - setArg(key, flags.strings[key] ? '' : true, arg); + setArg(key, true); } } } else { diff --git a/test/strict.js b/test/strict.js new file mode 100644 index 0000000..a862b04 --- /dev/null +++ b/test/strict.js @@ -0,0 +1,96 @@ +'use strict'; + +var parse = require('../'); +var test = require('tape'); + +function throwsWhenStrict(args, parseOptions, testOptions) { + // does not throw by default + testOptions.t.doesNotThrow(function () { + parse(args, parseOptions); + }); + // throws when strict + var strictOptions = JSON.parse(JSON.stringify(parseOptions)); + strictOptions.strict = true; + testOptions.t.throws(function () { + parse(args, strictOptions); + }, testOptions.expected); +} + +var kMissingString = /Missing option value/; +var kBooleanWithValue = /Unexpected option value/; + +test('strict missing option value: long string option used alone', function (t) { + throwsWhenStrict(['--str'], { string: ['str'] }, { t: t, expected: kMissingString }); + t.end(); +}); + +test('strict missing option value: short string option used alone', function (t) { + throwsWhenStrict(['-s'], { string: ['s'] }, { t: t, expected: kMissingString }); + t.end(); +}); + +test('strict missing option value: string option alias used alone', function (t) { + throwsWhenStrict(['-s'], { string: ['str'], alias: { str: 's' } }, { t: t, expected: kMissingString }); + t.end(); +}); + +test('strict missing option value: string option followed by option (rather than value)', function (t) { + throwsWhenStrict(['--str', '-a'], { string: ['str'] }, { t: t, expected: kMissingString }); + t.end(); +}); + +test('strict missing option value: short string option used before end of short option group', function (t) { + throwsWhenStrict(['-sb'], { string: ['s'], boolean: 'b' }, { t: t, expected: kMissingString }); + t.end(); +}); + +test('strict missing option value: empty string is ok value', function (t) { + t.doesNotThrow(function () { + parse(['--str', ''], { string: ['str'] }); + }); + t.end(); +}); + +test('strict missing option value: implied empty string is ok (--str=)', function (t) { + t.doesNotThrow(function () { + parse(['--str='], { string: ['str'] }); + }); + t.end(); +}); + +test('strict unexpected option value: long boolean option given value (other than true/false)', function (t) { + throwsWhenStrict(['--bool=x'], { boolean: ['bool'] }, { t: t, expected: kBooleanWithValue }); + t.end(); +}); + +test('strict unexpected option value: long boolean option given true is ok', function (t) { + t.doesNotThrow(function () { + parse(['--bool=true'], { boolean: ['bool'] }); + }); + t.end(); +}); + +test('strict unexpected option value: long boolean option given false is ok', function (t) { + t.doesNotThrow(function () { + parse(['--bool=false'], { boolean: ['bool'] }); + }); + t.end(); +}); + +test('strict unexpected option value: short boolean option given value (other than true/false)', function (t) { + throwsWhenStrict(['--b=x'], { boolean: ['b'] }, { t: t, expected: kBooleanWithValue }); + t.end(); +}); + +test('strict unexpected option value: short boolean option given value', function (t) { + t.doesNotThrow(function () { + parse(['--b=true'], { boolean: ['b'] }); + }); + t.end(); +}); +test('strict unexpected option value: short boolean option given value', function (t) { + t.doesNotThrow(function () { + parse(['--b=false'], { boolean: ['b'] }); + }); + t.end(); +}); From faa0bf1a5ab3b839c2ffe4c46cdb5d1c7606f0e3 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 24 Jun 2023 14:37:59 +1200 Subject: [PATCH 03/15] Comment out stale tests --- test/array.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/array.js b/test/array.js index 24a1d1c..3350160 100644 --- a/test/array.js +++ b/test/array.js @@ -71,6 +71,8 @@ test('auto bool accumulates with auto string', function (t) { t.end(); }); +/* No longer possible to get string into boolean option so not longer relevant + test('declared boolean overwrites string', function (t) { var options = { boolean: ['b'], @@ -120,3 +122,4 @@ test('declared boolean alias overwrites string', function (t) { t.end(); }); +*/ From 4fc976fa73034132ad85bb32366cff073b180646 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 24 Jun 2023 14:56:07 +1200 Subject: [PATCH 04/15] Add tests for (new) consistent short boolean value handling --- test/array.js | 53 --------------------------------------------------- test/bool.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/test/array.js b/test/array.js index 3350160..c862295 100644 --- a/test/array.js +++ b/test/array.js @@ -70,56 +70,3 @@ test('auto bool accumulates with auto string', function (t) { t.end(); }); - -/* No longer possible to get string into boolean option so not longer relevant - -test('declared boolean overwrites string', function (t) { - var options = { - boolean: ['b'], - }; - - // Verify the setup, that can get a string into the option. (Can't do this for long options.) - var argv1 = parse(['-b=xyz'], options); - t.deepEqual(argv1, { - b: 'xyz', - _: [], - }); - - // Check that declared boolean overwrites string, and does not accumulate into array. - var argv2 = parse(['-b=xyz', '-b'], options); - - t.deepEqual(argv2, { - b: true, - _: [], - }); - - t.end(); -}); - -test('declared boolean alias overwrites string', function (t) { - // https://github.com/minimistjs/minimist/issues/31 - var options = { - boolean: ['b'], - alias: { b: 'B' }, - }; - - // Verify the setup, that can get a string into the option. (Can't do this for long options.) - var argv1 = parse(['-B=xyz'], options); - t.deepEqual(argv1, { - b: 'xyz', - B: 'xyz', - _: [], - }); - - // Check that declared boolean overwrites string, and does not accumulate into array. - var argv2 = parse(['-B=xyz', '-B'], options); - - t.deepEqual(argv2, { - b: true, - B: true, - _: [], - }); - - t.end(); -}); -*/ diff --git a/test/bool.js b/test/bool.js index 930ccc6..0fa82b3 100644 --- a/test/bool.js +++ b/test/bool.js @@ -143,6 +143,55 @@ test('boolean --boool=false', function (t) { t.end(); }); +test('boolean --boool=other', function (t) { + // legacy edge case + var parsed = parse(['--boool=other'], { + default: { + boool: false, + }, + boolean: ['boool'], + }); + + t.same(parsed.boool, true); + t.end(); +}); + +test('boolean -b=true', function (t) { + var parsed = parse(['-b=true'], { + default: { + b: false, + }, + boolean: ['b'], + }); + + t.same(parsed.b, true); + t.end(); +}); + +test('boolean -b=false', function (t) { + var parsed = parse(['-b=false'], { + default: { + b: true, + }, + boolean: ['b'], + }); + + t.same(parsed.b, false); + t.end(); +}); + +test('boolean -b=other', function (t) { + var parsed = parse(['-b=other'], { + default: { + b: false, + }, + boolean: ['b'], + }); + + t.same(parsed.b, true); + t.end(); +}); + test('boolean using something similar to true', function (t) { var opts = { boolean: 'h' }; var result = parse(['-h', 'true.txt'], opts); From e1fa9f6652bb89d7d68ead8dee22a80bd25bf685 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 24 Jun 2023 17:02:08 +1200 Subject: [PATCH 05/15] Add strict error for unknown option --- index.js | 14 +++++++++++--- test/strict.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 22c524e..9fee10c 100644 --- a/index.js +++ b/index.js @@ -77,13 +77,18 @@ module.exports = function (args, opts) { var argv = { _: [] }; - function argDefined(key, arg) { - return (flags.allBools && (/^--[^=]+$/).test(arg)) - || flags.strings[key] + function keyDefined(key) { + return flags.strings[key] || flags.bools[key] || aliases[key]; } + function argDefined(key, arg) { + // legacy test for whether to call unknownFn + return (flags.allBools && (/^--[^=]+$/).test(arg)) + || keyDefined(key); + } + function setKey(obj, keys, value) { var o = obj; for (var i = 0; i < keys.length - 1; i++) { @@ -132,6 +137,9 @@ module.exports = function (args, opts) { if (isBooleanKey(key) && typeof val === 'string' && !(/^(true|false)$/).test(val)) { throw new Error('Unexpected option value for option "' + key + '"'); } + if (!keyDefined(key)) { + throw new Error('Unknown option "' + key + '"'); + } } // coercion diff --git a/test/strict.js b/test/strict.js index a862b04..62b1c25 100644 --- a/test/strict.js +++ b/test/strict.js @@ -18,6 +18,7 @@ function throwsWhenStrict(args, parseOptions, testOptions) { var kMissingString = /Missing option value/; var kBooleanWithValue = /Unexpected option value/; +var kUnknownOption = /Unknown option/; test('strict missing option value: long string option used alone', function (t) { throwsWhenStrict(['--str'], { string: ['str'] }, { t: t, expected: kMissingString }); @@ -88,9 +89,44 @@ test('strict unexpected option value: short boolean option given value', functio }); t.end(); }); + test('strict unexpected option value: short boolean option given value', function (t) { t.doesNotThrow(function () { parse(['--b=false'], { boolean: ['b'] }); }); t.end(); }); + +test('strict unknown option: unknown option', function (t) { + throwsWhenStrict(['-u'], { }, { t: t, expected: kUnknownOption }); + throwsWhenStrict(['--long'], { }, { t: t, expected: kUnknownOption }); + throwsWhenStrict(['-u=x'], { }, { t: t, expected: kUnknownOption }); + throwsWhenStrict(['--long=x'], { }, { t: t, expected: kUnknownOption }); + t.end(); +}); + +test('strict unknown option: opt.boolean is known', function (t) { + t.doesNotThrow(function () { + parse(['--bool'], { boolean: ['bool'], strict: true }); + parse(['-b'], { boolean: ['b'], strict: true }); + }); + t.end(); +}); + +test('strict unknown option: opt.string is known', function (t) { + t.doesNotThrow(function () { + parse(['--str', 'SSS'], { string: ['str'], strict: true }); + parse(['-s', 'SSS'], { string: ['s'], strict: true }); + }); + t.end(); +}); + +test('strict unknown option: opt.alias is known', function (t) { + t.doesNotThrow(function () { + var options = { alias: { aaa: ['a', 'AAA'] }, strict: true }; + parse(['--aaa'], options); + parse(['-a'], options); + parse(['--AAA'], options); + }); + t.end(); +}); From af41dfcae80d7329af2cdf7d65b7fc83b4b1aca9 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 24 Jun 2023 19:13:33 +1200 Subject: [PATCH 06/15] Add test for interaction of opts.strict and opts.unknown --- test/strict.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/strict.js b/test/strict.js index 62b1c25..d026346 100644 --- a/test/strict.js +++ b/test/strict.js @@ -130,3 +130,25 @@ test('strict unknown option: opt.alias is known', function (t) { }); t.end(); }); + +test('strict unknown option: opts.unknown returns false', function (t) { + // Mirror non-strict and skip argument processing if opts.unknown returns false. + // Otherwise, throw for unknown option as usual. + + function unknownFn() { + } + function unknownFnTrue() { + return true; + } + function unknownFnFalse() { + return false; + } + + throwsWhenStrict(['--x=y'], { unknown: unknownFn }, { t: t, expected: kUnknownOption }); + throwsWhenStrict(['--x=y'], { unknown: unknownFnTrue }, { t: t, expected: kUnknownOption }); + t.doesNotThrow(function () { + parse(['--x=y'], { strict: true, unknown: unknownFnFalse }); + }); + + t.end(); +}); From d4de1abbd22d495a618105d50dddf7d07a25ee9a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 24 Jun 2023 19:48:52 +1200 Subject: [PATCH 07/15] Add opts.known to simplify strict for options that are neither string nor boolean --- index.js | 8 +++++++- test/strict.js | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 9fee10c..52dbe0c 100644 --- a/index.js +++ b/index.js @@ -25,6 +25,7 @@ module.exports = function (args, opts) { var flags = { bools: {}, + known: {}, strings: {}, unknownFn: null, }; @@ -73,6 +74,10 @@ module.exports = function (args, opts) { } }); + [].concat(opts.known).filter(Boolean).forEach(function (key) { + flags.known[key] = true; + }); + var defaults = opts.default || {}; var argv = { _: [] }; @@ -80,7 +85,8 @@ module.exports = function (args, opts) { function keyDefined(key) { return flags.strings[key] || flags.bools[key] - || aliases[key]; + || aliases[key] + || flags.known[key]; } function argDefined(key, arg) { diff --git a/test/strict.js b/test/strict.js index d026346..416ee74 100644 --- a/test/strict.js +++ b/test/strict.js @@ -131,6 +131,20 @@ test('strict unknown option: opt.alias is known', function (t) { t.end(); }); +test('strict unknown option: opt.known is known (of course!)', function (t) { + t.doesNotThrow(function () { + // try known as a string and array of strings, with and without option values + parse(['--aaa'], { known: 'aaa', strict: true }); + parse(['--aaa=value'], { known: 'aaa', strict: true }); + parse(['--aaa', 'value'], { known: 'aaa', strict: true }); + parse(['--bbb'], { known: ['aaa', 'bbb'], strict: true }); + parse(['-s'], { known: ['s'], strict: true }); + parse(['-s=123'], { known: ['s'], strict: true }); + parse(['-abc'], { known: ['a', 'b', 'c'], strict: true }); + }); + t.end(); +}); + test('strict unknown option: opts.unknown returns false', function (t) { // Mirror non-strict and skip argument processing if opts.unknown returns false. // Otherwise, throw for unknown option as usual. From 3419b1551130bb26e87cf77fdf245e4d0cf932fc Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 27 Jun 2023 21:42:53 +1200 Subject: [PATCH 08/15] Add implemention for opts.number --- index.js | 30 ++++++++++++++++++++++++------ test/strict.js | 26 +++++++++++++------------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 52dbe0c..c0d3581 100644 --- a/index.js +++ b/index.js @@ -25,7 +25,8 @@ module.exports = function (args, opts) { var flags = { bools: {}, - known: {}, + // known: {}, + numbers: {}, strings: {}, unknownFn: null, }; @@ -65,6 +66,7 @@ module.exports = function (args, opts) { }); }); + // populating flags.strings with explicit keys and aliases [].concat(opts.string).filter(Boolean).forEach(function (key) { flags.strings[key] = true; if (aliases[key]) { @@ -74,19 +76,30 @@ module.exports = function (args, opts) { } }); - [].concat(opts.known).filter(Boolean).forEach(function (key) { - flags.known[key] = true; + // populating flags.numbers with explicit keys and aliases + [].concat(opts.number).filter(Boolean).forEach(function (key) { + flags.numbers[key] = true; + if (aliases[key]) { + [].concat(aliases[key]).forEach(function (k) { + flags.numbers[k] = true; + }); + } }); + // [].concat(opts.known).filter(Boolean).forEach(function (key) { + // flags.known[key] = true; + // }); + var defaults = opts.default || {}; var argv = { _: [] }; function keyDefined(key) { return flags.strings[key] + || flags.numbers[key] || flags.bools[key] - || aliases[key] - || flags.known[key]; + || aliases[key]; + // || flags.known[key]; } function argDefined(key, arg) { @@ -140,6 +153,9 @@ module.exports = function (args, opts) { if (flags.strings[key] && val === true) { throw new Error('Missing option value for option "' + key + '"'); } + if (flags.numbers[key] && !isNumber(val)) { + throw new Error('Expecting number value for option "' + key + '"'); + } if (isBooleanKey(key) && typeof val === 'string' && !(/^(true|false)$/).test(val)) { throw new Error('Unexpected option value for option "' + key + '"'); } @@ -150,7 +166,9 @@ module.exports = function (args, opts) { // coercion var value = val; - if (!flags.strings[key] && isNumber(val)) { + if (flags.numbers[key]) { + value = Number(val); + } else if (!flags.strings[key] && isNumber(val)) { value = Number(val); } if (flags.strings[key] && val === true) { diff --git a/test/strict.js b/test/strict.js index 416ee74..6f8c9f6 100644 --- a/test/strict.js +++ b/test/strict.js @@ -131,19 +131,19 @@ test('strict unknown option: opt.alias is known', function (t) { t.end(); }); -test('strict unknown option: opt.known is known (of course!)', function (t) { - t.doesNotThrow(function () { - // try known as a string and array of strings, with and without option values - parse(['--aaa'], { known: 'aaa', strict: true }); - parse(['--aaa=value'], { known: 'aaa', strict: true }); - parse(['--aaa', 'value'], { known: 'aaa', strict: true }); - parse(['--bbb'], { known: ['aaa', 'bbb'], strict: true }); - parse(['-s'], { known: ['s'], strict: true }); - parse(['-s=123'], { known: ['s'], strict: true }); - parse(['-abc'], { known: ['a', 'b', 'c'], strict: true }); - }); - t.end(); -}); +// test('strict unknown option: opt.known is known (of course!)', function (t) { +// t.doesNotThrow(function () { +// // try known as a string and array of strings, with and without option values +// parse(['--aaa'], { known: 'aaa', strict: true }); +// parse(['--aaa=value'], { known: 'aaa', strict: true }); +// parse(['--aaa', 'value'], { known: 'aaa', strict: true }); +// parse(['--bbb'], { known: ['aaa', 'bbb'], strict: true }); +// parse(['-s'], { known: ['s'], strict: true }); +// parse(['-s=123'], { known: ['s'], strict: true }); +// parse(['-abc'], { known: ['a', 'b', 'c'], strict: true }); +// }); +// t.end(); +// }); test('strict unknown option: opts.unknown returns false', function (t) { // Mirror non-strict and skip argument processing if opts.unknown returns false. From 8ad4e00af60b74faa42615faff44674ff5c50a3d Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 28 Jun 2023 22:16:49 +1200 Subject: [PATCH 09/15] Preserve legacy behaviour --- index.js | 12 ++++++++---- test/array.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/bool.js | 6 +++--- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index c0d3581..f27e4e2 100644 --- a/index.js +++ b/index.js @@ -144,7 +144,11 @@ module.exports = function (args, opts) { } } - function setArg(key, val, arg) { + // eslint-disable-next-line max-params + function setArg(key, val, arg, legacyBehaviours) { + var legacy = legacyBehaviours || {}; + // legacy.dumbBoolean is for -b=foo which has untyped automatic behaviour [sic] instead of boolean behaviour + // validation if (arg && flags.unknownFn && !argDefined(key, arg)) { if (flags.unknownFn(arg) === false) { return; } @@ -156,7 +160,7 @@ module.exports = function (args, opts) { if (flags.numbers[key] && !isNumber(val)) { throw new Error('Expecting number value for option "' + key + '"'); } - if (isBooleanKey(key) && typeof val === 'string' && !(/^(true|false)$/).test(val)) { + if (isBooleanKey(key) && typeof val === 'string' && !((/^(true|false)$/).test(val) || legacy.dumbBoolean)) { throw new Error('Unexpected option value for option "' + key + '"'); } if (!keyDefined(key)) { @@ -174,7 +178,7 @@ module.exports = function (args, opts) { if (flags.strings[key] && val === true) { value = ''; } - if (isBooleanKey(key) && typeof val === 'string') { + if (isBooleanKey(key) && !legacy.dumbBoolean && typeof val === 'string') { value = value !== 'false'; } @@ -246,7 +250,7 @@ module.exports = function (args, opts) { } if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') { - setArg(letters[j], next.slice(1), arg); + setArg(letters[j], next.slice(1), arg, { dumbBoolean: true }); broken = true; break; } diff --git a/test/array.js b/test/array.js index c862295..24a1d1c 100644 --- a/test/array.js +++ b/test/array.js @@ -70,3 +70,53 @@ test('auto bool accumulates with auto string', function (t) { t.end(); }); + +test('declared boolean overwrites string', function (t) { + var options = { + boolean: ['b'], + }; + + // Verify the setup, that can get a string into the option. (Can't do this for long options.) + var argv1 = parse(['-b=xyz'], options); + t.deepEqual(argv1, { + b: 'xyz', + _: [], + }); + + // Check that declared boolean overwrites string, and does not accumulate into array. + var argv2 = parse(['-b=xyz', '-b'], options); + + t.deepEqual(argv2, { + b: true, + _: [], + }); + + t.end(); +}); + +test('declared boolean alias overwrites string', function (t) { + // https://github.com/minimistjs/minimist/issues/31 + var options = { + boolean: ['b'], + alias: { b: 'B' }, + }; + + // Verify the setup, that can get a string into the option. (Can't do this for long options.) + var argv1 = parse(['-B=xyz'], options); + t.deepEqual(argv1, { + b: 'xyz', + B: 'xyz', + _: [], + }); + + // Check that declared boolean overwrites string, and does not accumulate into array. + var argv2 = parse(['-B=xyz', '-B'], options); + + t.deepEqual(argv2, { + b: true, + B: true, + _: [], + }); + + t.end(); +}); diff --git a/test/bool.js b/test/bool.js index 0fa82b3..34ec95a 100644 --- a/test/bool.js +++ b/test/bool.js @@ -164,7 +164,7 @@ test('boolean -b=true', function (t) { boolean: ['b'], }); - t.same(parsed.b, true); + t.same(parsed.b, 'true'); // [sic] legacy behaviour t.end(); }); @@ -176,7 +176,7 @@ test('boolean -b=false', function (t) { boolean: ['b'], }); - t.same(parsed.b, false); + t.same(parsed.b, 'false'); // [sic] legacy behaviour t.end(); }); @@ -188,7 +188,7 @@ test('boolean -b=other', function (t) { boolean: ['b'], }); - t.same(parsed.b, true); + t.same(parsed.b, 'other'); // [sic] legacy behaviour t.end(); }); From 92dae236a5a9fab164240f9782c8cf7794d9e351 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 29 Jun 2023 19:53:58 +1200 Subject: [PATCH 10/15] Restore original setVal and do strict independently --- index.js | 60 +++++++++++++++++++++++++++----------------------- test/strict.js | 40 ++++++++++++++++----------------- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/index.js b/index.js index f27e4e2..298b411 100644 --- a/index.js +++ b/index.js @@ -144,15 +144,9 @@ module.exports = function (args, opts) { } } - // eslint-disable-next-line max-params - function setArg(key, val, arg, legacyBehaviours) { - var legacy = legacyBehaviours || {}; - // legacy.dumbBoolean is for -b=foo which has untyped automatic behaviour [sic] instead of boolean behaviour - - // validation - if (arg && flags.unknownFn && !argDefined(key, arg)) { - if (flags.unknownFn(arg) === false) { return; } - } + function checkStrictVal(key, val) { + // Have a separate routine from setArg to avoid affecting non-strict results, + // as the strict checks need less processed values. if (opts.strict) { if (flags.strings[key] && val === true) { throw new Error('Missing option value for option "' + key + '"'); @@ -160,30 +154,24 @@ module.exports = function (args, opts) { if (flags.numbers[key] && !isNumber(val)) { throw new Error('Expecting number value for option "' + key + '"'); } - if (isBooleanKey(key) && typeof val === 'string' && !((/^(true|false)$/).test(val) || legacy.dumbBoolean)) { + if (isBooleanKey(key) && typeof val === 'string' && !(/^(true|false)$/).test(val)) { throw new Error('Unexpected option value for option "' + key + '"'); } if (!keyDefined(key)) { throw new Error('Unknown option "' + key + '"'); } } + } - // coercion - var value = val; - if (flags.numbers[key]) { - value = Number(val); - } else if (!flags.strings[key] && isNumber(val)) { - value = Number(val); - } - if (flags.strings[key] && val === true) { - value = ''; - } - if (isBooleanKey(key) && !legacy.dumbBoolean && typeof val === 'string') { - value = value !== 'false'; + function setArg(key, val, arg) { + if (arg && flags.unknownFn && !argDefined(key, arg)) { + if (flags.unknownFn(arg) === false) { return; } } - - // store + var value = !flags.strings[key] && isNumber(val) + ? Number(val) + : val; setKey(argv, key.split('.'), value); + (aliases[key] || []).forEach(function (x) { setKey(argv, x.split('.'), value); }); @@ -216,9 +204,14 @@ module.exports = function (args, opts) { var m = arg.match(/^--([^=]+)=([\s\S]*)$/); key = m[1]; var value = m[2]; + checkStrictVal(key, value); + if (isBooleanKey(key)) { + value = value !== 'false'; + } setArg(key, value, arg); } else if ((/^--no-.+/).test(arg)) { key = arg.match(/^--no-(.+)/)[1]; + checkStrictVal(key, false); setArg(key, false, arg); } else if ((/^--.+/).test(arg)) { key = arg.match(/^--(.+)/)[1]; @@ -229,13 +222,16 @@ module.exports = function (args, opts) { && !isBooleanKey(key) && !flags.allBools ) { + checkStrictVal(key, next); setArg(key, next, arg); i += 1; } else if ((/^(true|false)$/).test(next)) { + checkStrictVal(key, next); setArg(key, next === 'true', arg); i += 1; } else { - setArg(key, true, arg); + checkStrictVal(key, true); + setArg(key, flags.strings[key] ? '' : true, arg); } } else if ((/^-[^-]+/).test(arg)) { var letters = arg.slice(1, -1).split(''); @@ -245,12 +241,14 @@ module.exports = function (args, opts) { next = arg.slice(j + 2); if (next === '-') { + checkStrictVal(letters[j], next); setArg(letters[j], next, arg); continue; } if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') { - setArg(letters[j], next.slice(1), arg, { dumbBoolean: true }); + checkStrictVal(letters[j], next.slice(1)); + setArg(letters[j], next.slice(1), arg); broken = true; break; } @@ -259,17 +257,20 @@ module.exports = function (args, opts) { (/[A-Za-z]/).test(letters[j]) && (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next) ) { + checkStrictVal(letters[j], next); setArg(letters[j], next, arg); broken = true; break; } if (letters[j + 1] && letters[j + 1].match(/\W/)) { + checkStrictVal(letters[j], arg.slice(j + 2)); setArg(letters[j], arg.slice(j + 2), arg); broken = true; break; } else { - setArg(letters[j], true); + checkStrictVal(letters[j], true); + setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg); } } @@ -280,13 +281,16 @@ module.exports = function (args, opts) { && !(/^(-|--)[^-]/).test(args[i + 1]) && !isBooleanKey(key) ) { + checkStrictVal(key, args[i + 1]); setArg(key, args[i + 1], arg); i += 1; } else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) { + checkStrictVal(key, args[i + 1]); setArg(key, args[i + 1] === 'true', arg); i += 1; } else { - setArg(key, true); + checkStrictVal(key, true); + setArg(key, flags.strings[key] ? '' : true, arg); } } } else { diff --git a/test/strict.js b/test/strict.js index 6f8c9f6..f06fb01 100644 --- a/test/strict.js +++ b/test/strict.js @@ -145,24 +145,24 @@ test('strict unknown option: opt.alias is known', function (t) { // t.end(); // }); -test('strict unknown option: opts.unknown returns false', function (t) { - // Mirror non-strict and skip argument processing if opts.unknown returns false. - // Otherwise, throw for unknown option as usual. - - function unknownFn() { - } - function unknownFnTrue() { - return true; - } - function unknownFnFalse() { - return false; - } - - throwsWhenStrict(['--x=y'], { unknown: unknownFn }, { t: t, expected: kUnknownOption }); - throwsWhenStrict(['--x=y'], { unknown: unknownFnTrue }, { t: t, expected: kUnknownOption }); - t.doesNotThrow(function () { - parse(['--x=y'], { strict: true, unknown: unknownFnFalse }); - }); +// test('strict unknown option: opts.unknown returns false', function (t) { +// // Mirror non-strict and skip argument processing if opts.unknown returns false. +// // Otherwise, throw for unknown option as usual. + +// function unknownFn() { +// } +// function unknownFnTrue() { +// return true; +// } +// function unknownFnFalse() { +// return false; +// } + +// throwsWhenStrict(['--x=y'], { unknown: unknownFn }, { t: t, expected: kUnknownOption }); +// throwsWhenStrict(['--x=y'], { unknown: unknownFnTrue }, { t: t, expected: kUnknownOption }); +// t.doesNotThrow(function () { +// parse(['--x=y'], { strict: true, unknown: unknownFnFalse }); +// }); - t.end(); -}); +// t.end(); +// }); From 439185e5b384434589c65166ed28620fb6f0950d Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 29 Jun 2023 19:55:05 +1200 Subject: [PATCH 11/15] Restore blank line --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 298b411..c9a7b14 100644 --- a/index.js +++ b/index.js @@ -167,6 +167,7 @@ module.exports = function (args, opts) { if (arg && flags.unknownFn && !argDefined(key, arg)) { if (flags.unknownFn(arg) === false) { return; } } + var value = !flags.strings[key] && isNumber(val) ? Number(val) : val; From 4336111d30dc294ab0d9d0fd9a532fd592730fb2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 29 Jun 2023 20:07:20 +1200 Subject: [PATCH 12/15] Add back (untested) implementation for new number support --- index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index c9a7b14..4f46a15 100644 --- a/index.js +++ b/index.js @@ -168,9 +168,12 @@ module.exports = function (args, opts) { if (flags.unknownFn(arg) === false) { return; } } - var value = !flags.strings[key] && isNumber(val) - ? Number(val) - : val; + var value = val; + if (flags.numbers[key]) { + value = Number(val); + } else if (!flags.strings[key] && isNumber(val)) { + value = Number(val); + } setKey(argv, key.split('.'), value); (aliases[key] || []).forEach(function (x) { From 789fbdd5a0a40dc8cb8db522c1cd4e12e1bfbe67 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 2 Jul 2023 16:26:12 +1200 Subject: [PATCH 13/15] Add tests for new number type --- index.js | 10 ++++- test/num.js | 76 ++++++++++++++++++++++++++++++++++++ test/strict.js | 103 ++++++++++++++++++++++++++++++++----------------- 3 files changed, 151 insertions(+), 38 deletions(-) diff --git a/index.js b/index.js index 4f46a15..20a71c6 100644 --- a/index.js +++ b/index.js @@ -151,7 +151,7 @@ module.exports = function (args, opts) { if (flags.strings[key] && val === true) { throw new Error('Missing option value for option "' + key + '"'); } - if (flags.numbers[key] && !isNumber(val)) { + if (flags.numbers[key] && !(isNumber(val) || val === false)) { throw new Error('Expecting number value for option "' + key + '"'); } if (isBooleanKey(key) && typeof val === 'string' && !(/^(true|false)$/).test(val)) { @@ -170,7 +170,13 @@ module.exports = function (args, opts) { var value = val; if (flags.numbers[key]) { - value = Number(val); + if (isNumber(val)) { + value = Number(val); + } else if (value === false) { + value = val; // --no-foo + } else { + value = NaN; + } } else if (!flags.strings[key] && isNumber(val)) { value = Number(val); } diff --git a/test/num.js b/test/num.js index 074393e..2cc35d0 100644 --- a/test/num.js +++ b/test/num.js @@ -36,3 +36,79 @@ test('already a number', function (t) { t.deepEqual(typeof argv._[0], 'number'); t.end(); }); + +test('number: short option', function (t) { + var options = { number: 'n' }; + var argv = parse(['-n', '123'], options); + t.deepEqual(argv, { n: 123, _: [] }); + + // argv = parse(['-n', '-123'], options); + // t.deepEqual(argv, { n: -123, _: [] }); + + argv = parse(['-n=123'], options); + t.deepEqual(argv, { n: 123, _: [] }); + + argv = parse(['-n', 'xyz'], options); + t.deepEqual(argv, { n: NaN, _: [] }); + + argv = parse(['-n=xyz'], options); + t.deepEqual(argv, { n: NaN, _: [] }); + + // Special case of missing argument value + argv = parse(['-n'], options); + t.deepEqual(argv, { n: NaN, _: [] }); + + t.end(); +}); + +test('number: long option', function (t) { + var options = { number: 'num' }; + var argv = parse(['--num', '123'], options); + t.deepEqual(argv, { num: 123, _: [] }); + + // argv = parse(['--num', '-123'], options); + // t.deepEqual(argv, { num: -123, _: [] }); + + argv = parse(['--num=123'], options); + t.deepEqual(argv, { num: 123, _: [] }); + + argv = parse(['--num', 'xyz'], options); + t.deepEqual(argv, { num: NaN, _: [] }); + + argv = parse(['--num=xyz'], options); + t.deepEqual(argv, { num: NaN, _: [] }); + + // Special case of missing argument value + argv = parse(['--num'], options); + t.deepEqual(argv, { num: NaN, _: [] }); + + // Special case of negated + argv = parse(['--no-num'], options); + t.deepEqual(argv, { num: false, _: [] }); + + t.end(); +}); + +test('number: alias', function (t) { + var options = { number: 'num', alias: { num: 'n' } }; + var argv = parse(['-n', '123'], options); + t.deepEqual(argv, { n: 123, num: 123, _: [] }); + + // argv = parse(['-n', '-123'], options); + // t.deepEqual(argv, { n: -123, num: 123, _: [] }); + + argv = parse(['-n=123'], options); + t.deepEqual(argv, { n: 123, num: 123, _: [] }); + + argv = parse(['-n', 'xyz'], options); + t.deepEqual(argv, { n: NaN, num: NaN, _: [] }); + + argv = parse(['-n=xyz'], options); + t.deepEqual(argv, { n: NaN, num: NaN, _: [] }); + + // Special case of missing argument value + argv = parse(['-n'], options); + t.deepEqual(argv, { n: NaN, num: NaN, _: [] }); + + t.end(); +}); diff --git a/test/strict.js b/test/strict.js index f06fb01..852d275 100644 --- a/test/strict.js +++ b/test/strict.js @@ -17,9 +17,12 @@ function throwsWhenStrict(args, parseOptions, testOptions) { } var kMissingString = /Missing option value/; +var kMissingNumber = /Expecting number value/; var kBooleanWithValue = /Unexpected option value/; var kUnknownOption = /Unknown option/; +// missing option value + test('strict missing option value: long string option used alone', function (t) { throwsWhenStrict(['--str'], { string: ['str'] }, { t: t, expected: kMissingString }); t.end(); @@ -59,6 +62,62 @@ test('strict missing option value: implied empty string is ok (--str=)', functio t.end(); }); +// missing number value + +test('strict missing number value: long number option used alone', function (t) { + throwsWhenStrict(['--num'], { number: ['num'] }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: short number option used alone', function (t) { + throwsWhenStrict(['-n'], { number: ['n'] }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: number option alias used alone', function (t) { + throwsWhenStrict(['-n'], { number: ['num'], alias: { num: 'n' } }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: long number option followed by non-number', function (t) { + throwsWhenStrict(['--num', 'xyz'], { number: ['num'] }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: short number option followed by non-number', function (t) { + throwsWhenStrict(['-n', 'xyz'], { number: ['n'] }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: long number option = non-number', function (t) { + throwsWhenStrict(['--num=xyz'], { number: ['num'] }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: short number option = non-number', function (t) { + throwsWhenStrict(['-n=xyz'], { number: ['n'] }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: number option followed by option (rather than value)', function (t) { + throwsWhenStrict(['--num', '-a'], { number: ['num'] }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: short number option used before end of short option group', function (t) { + throwsWhenStrict(['-nb'], { number: ['n'], boolean: 'b' }, { t: t, expected: kMissingNumber }); + t.end(); +}); + +test('strict missing number value: number does not throw', function (t) { + t.doesNotThrow(function () { + parse(['--num', '123'], { number: ['num'] }); + }); + t.end(); +}); + +// unexpected option value + test('strict unexpected option value: long boolean option given value (other than true/false)', function (t) { throwsWhenStrict(['--bool=x'], { boolean: ['bool'] }, { t: t, expected: kBooleanWithValue }); t.end(); @@ -121,6 +180,14 @@ test('strict unknown option: opt.string is known', function (t) { t.end(); }); +test('strict unknown option: opt.number is known', function (t) { + t.doesNotThrow(function () { + parse(['--num', '123'], { number: ['num'], strict: true }); + parse(['-n', '123'], { number: ['n'], strict: true }); + }); + t.end(); +}); + test('strict unknown option: opt.alias is known', function (t) { t.doesNotThrow(function () { var options = { alias: { aaa: ['a', 'AAA'] }, strict: true }; @@ -130,39 +197,3 @@ test('strict unknown option: opt.alias is known', function (t) { }); t.end(); }); - -// test('strict unknown option: opt.known is known (of course!)', function (t) { -// t.doesNotThrow(function () { -// // try known as a string and array of strings, with and without option values -// parse(['--aaa'], { known: 'aaa', strict: true }); -// parse(['--aaa=value'], { known: 'aaa', strict: true }); -// parse(['--aaa', 'value'], { known: 'aaa', strict: true }); -// parse(['--bbb'], { known: ['aaa', 'bbb'], strict: true }); -// parse(['-s'], { known: ['s'], strict: true }); -// parse(['-s=123'], { known: ['s'], strict: true }); -// parse(['-abc'], { known: ['a', 'b', 'c'], strict: true }); -// }); -// t.end(); -// }); - -// test('strict unknown option: opts.unknown returns false', function (t) { -// // Mirror non-strict and skip argument processing if opts.unknown returns false. -// // Otherwise, throw for unknown option as usual. - -// function unknownFn() { -// } -// function unknownFnTrue() { -// return true; -// } -// function unknownFnFalse() { -// return false; -// } - -// throwsWhenStrict(['--x=y'], { unknown: unknownFn }, { t: t, expected: kUnknownOption }); -// throwsWhenStrict(['--x=y'], { unknown: unknownFnTrue }, { t: t, expected: kUnknownOption }); -// t.doesNotThrow(function () { -// parse(['--x=y'], { strict: true, unknown: unknownFnFalse }); -// }); - -// t.end(); -// }); From 2e5928612aa2cd1f409e7068e155e629cecd6b26 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 6 Jul 2023 18:50:22 +1200 Subject: [PATCH 14/15] Add support for space-separated negative number --- index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/index.js b/index.js index 20a71c6..4610b04 100644 --- a/index.js +++ b/index.js @@ -239,6 +239,11 @@ module.exports = function (args, opts) { checkStrictVal(key, next); setArg(key, next === 'true', arg); i += 1; + } else if (flags.numbers[key] && isNumber(next)) { + // This is a second look to pick up negative numbers. + checkStrictVal(key, next); + setArg(key, next, arg); + i += 1; } else { checkStrictVal(key, true); setArg(key, flags.strings[key] ? '' : true, arg); @@ -298,6 +303,11 @@ module.exports = function (args, opts) { checkStrictVal(key, args[i + 1]); setArg(key, args[i + 1] === 'true', arg); i += 1; + } else if (flags.numbers[key] && isNumber(args[i + 1])) { + // This is a second look to pick up negative numbers. + checkStrictVal(key, args[i + 1]); + setArg(key, args[i + 1], arg); + i += 1; } else { checkStrictVal(key, true); setArg(key, flags.strings[key] ? '' : true, arg); From 63f093fa4f7e9e8f491149e010a135dad5df78c1 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 26 Jul 2023 17:32:44 +1200 Subject: [PATCH 15/15] test: enable tests for negative numbers for number type --- test/num.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/num.js b/test/num.js index 2cc35d0..c14a3e6 100644 --- a/test/num.js +++ b/test/num.js @@ -3,7 +3,7 @@ var parse = require('../'); var test = require('tape'); -test('nums', function (t) { +test('implicit nums', function (t) { var argv = parse([ '-x', '1234', '-y', '5.67', @@ -37,13 +37,13 @@ test('already a number', function (t) { t.end(); }); -test('number: short option', function (t) { +test('number type: short option', function (t) { var options = { number: 'n' }; var argv = parse(['-n', '123'], options); t.deepEqual(argv, { n: 123, _: [] }); - // argv = parse(['-n', '-123'], options); - // t.deepEqual(argv, { n: -123, _: [] }); + argv = parse(['-n', '-123'], options); + t.deepEqual(argv, { n: -123, _: [] }); argv = parse(['-n=123'], options); t.deepEqual(argv, { n: 123, _: [] }); @@ -61,13 +61,13 @@ test('number: short option', function (t) { t.end(); }); -test('number: long option', function (t) { +test('number type: long option', function (t) { var options = { number: 'num' }; var argv = parse(['--num', '123'], options); t.deepEqual(argv, { num: 123, _: [] }); - // argv = parse(['--num', '-123'], options); - // t.deepEqual(argv, { num: -123, _: [] }); + argv = parse(['--num', '-123'], options); + t.deepEqual(argv, { num: -123, _: [] }); argv = parse(['--num=123'], options); t.deepEqual(argv, { num: 123, _: [] });