From 697493f23f7be34e33cce5151f0bfeafcb68b499 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 8 Feb 2022 17:34:10 -0800 Subject: [PATCH 01/30] feat: Restructure options API --- index.js | 84 +++++++++++++++++++++++++++---------------- test/index.js | 99 ++++++++++++++++++++++++--------------------------- validators.js | 14 ++++++++ 3 files changed, 114 insertions(+), 83 deletions(-) diff --git a/index.js b/index.js index 6948332..ba0e50e 100644 --- a/index.js +++ b/index.js @@ -2,11 +2,11 @@ const { ArrayPrototypeConcat, - ArrayPrototypeIncludes, ArrayPrototypeSlice, ArrayPrototypeSplice, ArrayPrototypePush, ObjectHasOwn, + ObjectEntries, StringPrototypeCharAt, StringPrototypeIncludes, StringPrototypeIndexOf, @@ -16,7 +16,9 @@ const { const { validateArray, - validateObject + validateObject, + validateString, + validateBoolean, } = require('./validators'); function getMainArgs() { @@ -53,39 +55,46 @@ function getMainArgs() { return ArrayPrototypeSlice(process.argv, 2); } -function storeOptionValue(parseOptions, option, value, result) { - const multiple = parseOptions.multiples && - ArrayPrototypeIncludes(parseOptions.multiples, option); +function storeOptionValue(options, arg, value, result) { + const option = options[arg] || {}; // Flags - result.flags[option] = true; + result.flags[arg] = true; // Values - if (multiple) { + if (option.multiples) { // Always store value in array, including for flags. - // result.values[option] starts out not present, + // result.values[arg] starts out not present, // first value is added as new array [newValue], // subsequent values are pushed to existing array. const usedAsFlag = value === undefined; const newValue = usedAsFlag ? true : value; - if (result.values[option] !== undefined) - ArrayPrototypePush(result.values[option], newValue); + if (result.values[arg] !== undefined) + ArrayPrototypePush(result.values[arg], newValue); else - result.values[option] = [newValue]; + result.values[arg] = [newValue]; } else { - result.values[option] = value; + result.values[arg] = value; } } -const parseArgs = ( - argv = getMainArgs(), +const parseArgs = ({ + args = getMainArgs(), options = {} -) => { - validateArray(argv, 'argv'); +} = {}) => { + validateArray(args, 'args'); validateObject(options, 'options'); - for (const key of ['withValue', 'multiples']) { - if (ObjectHasOwn(options, key)) { - validateArray(options[key], `options.${key}`); + for (const [arg, option] of ObjectEntries(options)) { + validateObject(option, `options.${arg}`); + + if (ObjectHasOwn(option, 'short')) { + validateString(option.short, `options.${arg}.short`); + } + + for (const config of ['withValue', 'multiples']) { + if (ObjectHasOwn(option, config)) { + validateBoolean(option[config], `options.${arg}.${config}`); + } } } @@ -96,43 +105,54 @@ const parseArgs = ( }; let pos = 0; - while (pos < argv.length) { - let arg = argv[pos]; + while (pos < args.length) { + let arg = args[pos]; if (StringPrototypeStartsWith(arg, '-')) { + // e.g. `arg` is: + // '-' | '--' | '-f' | '-fo' | '--foo' | '-f=bar' | '--for=bar' if (arg === '-') { + // e.g. `arg` is: '-' // '-' commonly used to represent stdin/stdout, treat as positional result.positionals = ArrayPrototypeConcat(result.positionals, '-'); ++pos; continue; } else if (arg === '--') { + // e.g. `arg` is: '--' // Everything after a bare '--' is considered a positional argument // and is returned verbatim result.positionals = ArrayPrototypeConcat( result.positionals, - ArrayPrototypeSlice(argv, ++pos) + ArrayPrototypeSlice(args, ++pos) ); return result; } else if (StringPrototypeCharAt(arg, 1) !== '-') { + // e.g. `arg` is: '-f' | '-foo' | '-f=bar' // Look for shortcodes: -fXzy and expand them to -f -X -z -y: if (arg.length > 2) { + // `arg` is '-foo' for (let i = 2; i < arg.length; i++) { const short = StringPrototypeCharAt(arg, i); // Add 'i' to 'pos' such that short options are parsed in order // of definition: - ArrayPrototypeSplice(argv, pos + (i - 1), 0, `-${short}`); + ArrayPrototypeSplice(args, pos + (i - 1), 0, `-${short}`); } } arg = StringPrototypeCharAt(arg, 1); // short - if (options.short && options.short[arg]) - arg = options.short[arg]; // now long! + for (const [longName, option] of ObjectEntries(options)) { + if (option.short === arg) { + arg = longName; + break; + } + } // ToDo: later code tests for `=` in arg and wrong for shorts } else { arg = StringPrototypeSlice(arg, 2); // remove leading -- } if (StringPrototypeIncludes(arg, '=')) { + // e.g. `arg` is: 'for=bar' | 'foo=bar=baz' // Store option=value same way independent of `withValue` as: // - looks like a value, store as a value // - match the intention of the user @@ -143,18 +163,21 @@ const parseArgs = ( StringPrototypeSlice(arg, 0, index), StringPrototypeSlice(arg, index + 1), result); - } else if (pos + 1 < argv.length && - !StringPrototypeStartsWith(argv[pos + 1], '-') + } else if (pos + 1 < args.length && + !StringPrototypeStartsWith(args[pos + 1], '-') ) { + // If next arg is NOT a flag, check if the current arg is + // is configured to use `withValue` and store the next arg. + // withValue option should also support setting values when '= // isn't used ie. both --foo=b and --foo b should work - // If withValue option is specified, take next position arguement as + // If withValue option is specified, take next position argument as // value and then increment pos so that we don't re-evaluate that // arg, else set value as undefined ie. --foo b --bar c, after setting // b as the value for foo, evaluate --bar next and skip 'b' - const val = options.withValue && - ArrayPrototypeIncludes(options.withValue, arg) ? argv[++pos] : + const val = options[arg] && options[arg].withValue ? + args[++pos] : undefined; storeOptionValue(options, arg, val, result); } else { @@ -163,7 +186,6 @@ const parseArgs = ( // shave value as undefined storeOptionValue(options, arg, undefined, result); } - } else { // Arguements without a dash prefix are considered "positional" ArrayPrototypePush(result.positionals, arg); diff --git a/test/index.js b/test/index.js index 8487923..cbd963e 100644 --- a/test/index.js +++ b/test/index.js @@ -9,7 +9,7 @@ const { parseArgs } = require('../index.js'); test('when short option used as flag then stored as flag', function(t) { const passedArgs = ['-f']; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [] }; - const args = parseArgs(passedArgs); + const args = parseArgs({ args: passedArgs }); t.deepEqual(args, expected); @@ -19,7 +19,7 @@ test('when short option used as flag then stored as flag', function(t) { test('when short option used as flag before positional then stored as flag and positional (and not value)', function(t) { const passedArgs = ['-f', 'bar']; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [ 'bar' ] }; - const args = parseArgs(passedArgs); + const args = parseArgs({ args: passedArgs }); t.deepEqual(args, expected); @@ -28,9 +28,9 @@ test('when short option used as flag before positional then stored as flag and p test('when short option withValue used with value then stored as value', function(t) { const passedArgs = ['-f', 'bar']; - const passedOptions = { withValue: ['f'] }; + const passedOptions = { f: { withValue: true } }; const expected = { flags: { f: true }, values: { f: 'bar' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -39,9 +39,9 @@ test('when short option withValue used with value then stored as value', functio test('when short option listed in short used as flag then long option stored as flag', function(t) { const passedArgs = ['-f']; - const passedOptions = { short: { f: 'foo' } }; + const passedOptions = { foo: { short: 'f' } }; const expected = { flags: { foo: true }, values: { foo: undefined }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -50,9 +50,9 @@ test('when short option listed in short used as flag then long option stored as test('when short option listed in short and long listed in withValue and used with value then long option stored as value', function(t) { const passedArgs = ['-f', 'bar']; - const passedOptions = { short: { f: 'foo' }, withValue: ['foo'] }; + const passedOptions = { foo: { short: 'f', withValue: true } }; const expected = { flags: { foo: true }, values: { foo: 'bar' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -61,9 +61,9 @@ test('when short option listed in short and long listed in withValue and used wi test('when short option withValue used without value then stored as flag', function(t) { const passedArgs = ['-f']; - const passedOptions = { withValue: ['f'] }; + const passedOptions = { f: { withValue: true } }; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -74,7 +74,7 @@ test('short option group behaves like multiple short options', function(t) { const passedArgs = ['-rf']; const passedOptions = { }; const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -85,18 +85,18 @@ test('short option group does not consume subsequent positional', function(t) { const passedArgs = ['-rf', 'foo']; const passedOptions = { }; const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['foo'] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); }); -// See: Guideline 5 https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html +// // See: Guideline 5 https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html test('if terminal of short-option group configured withValue, subsequent positional is stored', function(t) { const passedArgs = ['-rvf', 'foo']; - const passedOptions = { withValue: ['f'] }; + const passedOptions = { f: { withValue: true } }; const expected = { flags: { r: true, f: true, v: true }, values: { r: undefined, v: undefined, f: 'foo' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -104,9 +104,9 @@ test('if terminal of short-option group configured withValue, subsequent positio test('handles short-option groups in conjunction with long-options', function(t) { const passedArgs = ['-rf', '--foo', 'foo']; - const passedOptions = { withValue: ['foo'] }; + const passedOptions = { foo: { withValue: true } }; const expected = { flags: { r: true, f: true, foo: true }, values: { r: undefined, f: undefined, foo: 'foo' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -114,9 +114,9 @@ test('handles short-option groups in conjunction with long-options', function(t) test('handles short-option groups with "short" alias configured', function(t) { const passedArgs = ['-rf']; - const passedOptions = { short: { r: 'remove' } }; + const passedOptions = { remove: { short: 'r' } }; const expected = { flags: { remove: true, f: true }, values: { remove: undefined, f: undefined }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -125,7 +125,7 @@ test('handles short-option groups with "short" alias configured', function(t) { test('Everything after a bare `--` is considered a positional argument', function(t) { const passedArgs = ['--', 'barepositionals', 'mopositionals']; const expected = { flags: {}, values: {}, positionals: ['barepositionals', 'mopositionals'] }; - const args = parseArgs(passedArgs); + const args = parseArgs({ args: passedArgs }); t.deepEqual(args, expected, 'testing bare positionals'); @@ -135,7 +135,7 @@ test('Everything after a bare `--` is considered a positional argument', functio test('args are true', function(t) { const passedArgs = ['--foo', '--bar']; const expected = { flags: { foo: true, bar: true }, values: { foo: undefined, bar: undefined }, positionals: [] }; - const args = parseArgs(passedArgs); + const args = parseArgs({ args: passedArgs }); t.deepEqual(args, expected, 'args are true'); @@ -145,7 +145,7 @@ test('args are true', function(t) { test('arg is true and positional is identified', function(t) { const passedArgs = ['--foo=a', '--foo', 'b']; const expected = { flags: { foo: true }, values: { foo: undefined }, positionals: ['b'] }; - const args = parseArgs(passedArgs); + const args = parseArgs({ args: passedArgs }); t.deepEqual(args, expected, 'arg is true and positional is identified'); @@ -154,9 +154,9 @@ test('arg is true and positional is identified', function(t) { test('args equals are passed "withValue"', function(t) { const passedArgs = ['--so=wat']; - const passedOptions = { withValue: ['so'] }; + const passedOptions = { so: { withValue: true } }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'arg value is passed'); @@ -166,7 +166,7 @@ test('args equals are passed "withValue"', function(t) { test('when args include single dash then result stores dash as positional', function(t) { const passedArgs = ['-']; const expected = { flags: { }, values: { }, positionals: ['-'] }; - const args = parseArgs(passedArgs); + const args = parseArgs({ args: passedArgs }); t.deepEqual(args, expected); @@ -177,7 +177,7 @@ test('zero config args equals are parsed as if "withValue"', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'arg value is passed'); @@ -186,9 +186,9 @@ test('zero config args equals are parsed as if "withValue"', function(t) { test('same arg is passed twice "withValue" and last value is recorded', function(t) { const passedArgs = ['--foo=a', '--foo', 'b']; - const passedOptions = { withValue: ['foo'] }; + const passedOptions = { foo: { withValue: true } }; const expected = { flags: { foo: true }, values: { foo: 'b' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'last arg value is passed'); @@ -197,9 +197,9 @@ test('same arg is passed twice "withValue" and last value is recorded', function test('args equals pass string including more equals', function(t) { const passedArgs = ['--so=wat=bing']; - const passedOptions = { withValue: ['so'] }; + const passedOptions = { so: { withValue: true } }; const expected = { flags: { so: true }, values: { so: 'wat=bing' }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'arg value is passed'); @@ -208,9 +208,9 @@ test('args equals pass string including more equals', function(t) { test('first arg passed for "withValue" and "multiples" is in array', function(t) { const passedArgs = ['--foo=a']; - const passedOptions = { withValue: ['foo'], multiples: ['foo'] }; + const passedOptions = { foo: { withValue: true, multiples: true } }; const expected = { flags: { foo: true }, values: { foo: ['a'] }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'first multiple in array'); @@ -219,9 +219,14 @@ test('first arg passed for "withValue" and "multiples" is in array', function(t) test('args are passed "withValue" and "multiples"', function(t) { const passedArgs = ['--foo=a', '--foo', 'b']; - const passedOptions = { withValue: ['foo'], multiples: ['foo'] }; + const passedOptions = { + foo: { + withValue: true, + multiples: true, + }, + }; const expected = { flags: { foo: true }, values: { foo: ['a', 'b'] }, positionals: [] }; - const args = parseArgs(passedArgs, passedOptions); + const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'both arg values are passed'); @@ -231,11 +236,11 @@ test('args are passed "withValue" and "multiples"', function(t) { test('order of option and positional does not matter (per README)', function(t) { const passedArgs1 = ['--foo=bar', 'baz']; const passedArgs2 = ['baz', '--foo=bar']; - const passedOptions = { withValue: ['foo'] }; + const passedOptions = { foo: { withValue: true } }; const expected = { flags: { foo: true }, values: { foo: 'bar' }, positionals: ['baz'] }; - t.deepEqual(parseArgs(passedArgs1, passedOptions), expected, 'option then positional'); - t.deepEqual(parseArgs(passedArgs2, passedOptions), expected, 'positional then option'); + t.deepEqual(parseArgs({ args: passedArgs1, options: passedOptions }), expected, 'option then positional'); + t.deepEqual(parseArgs({ args: passedArgs2, options: passedOptions }), expected, 'positional then option'); t.end(); }); @@ -334,7 +339,7 @@ test('excess leading dashes on options are retained', function(t) { values: { '-triple': undefined }, positionals: [] }; - const result = parseArgs(passedArgs, passedOptions); + const result = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(result, expected, 'excess option dashes are retained'); @@ -345,19 +350,9 @@ test('excess leading dashes on options are retained', function(t) { test('invalid argument passed for options', function(t) { const passedArgs = ['--so=wat']; + const passedOptions = 'bad value'; - t.throws(function() { parseArgs(passedArgs, 'bad value'); }, { - code: 'ERR_INVALID_ARG_TYPE' - }); - - t.end(); -}); - -test('boolean passed to "withValue" option', function(t) { - const passedArgs = ['--so=wat']; - const passedOptions = { withValue: true }; - - t.throws(function() { parseArgs(passedArgs, passedOptions); }, { + t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_ARG_TYPE' }); @@ -366,9 +361,9 @@ test('boolean passed to "withValue" option', function(t) { test('string passed to "withValue" option', function(t) { const passedArgs = ['--so=wat']; - const passedOptions = { withValue: 'so' }; + const passedOptions = { foo: { withValue: 'bad value' } }; - t.throws(function() { parseArgs(passedArgs, passedOptions); }, { + t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_ARG_TYPE' }); diff --git a/validators.js b/validators.js index a8be5ff..2aad2ef 100644 --- a/validators.js +++ b/validators.js @@ -10,6 +10,18 @@ const { } } = require('./errors'); +function validateString(value, name) { + if (typeof value !== 'string') { + throw new ERR_INVALID_ARG_TYPE(name, 'String', value); + } +} + +function validateBoolean(value, name) { + if (typeof value !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE(name, 'Boolean', value); + } +} + function validateArray(value, name) { if (!ArrayIsArray(value)) { throw new ERR_INVALID_ARG_TYPE(name, 'Array', value); @@ -42,4 +54,6 @@ function validateObject(value, name, options) { module.exports = { validateArray, validateObject, + validateString, + validateBoolean, }; From 9e42db00720bb6a9af861d5b73c3577e364fa4e2 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 8 Feb 2022 18:33:13 -0800 Subject: [PATCH 02/30] chore: Remove debug comments --- index.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/index.js b/index.js index ba0e50e..0a05f8e 100644 --- a/index.js +++ b/index.js @@ -109,16 +109,12 @@ const parseArgs = ({ let arg = args[pos]; if (StringPrototypeStartsWith(arg, '-')) { - // e.g. `arg` is: - // '-' | '--' | '-f' | '-fo' | '--foo' | '-f=bar' | '--for=bar' if (arg === '-') { - // e.g. `arg` is: '-' // '-' commonly used to represent stdin/stdout, treat as positional result.positionals = ArrayPrototypeConcat(result.positionals, '-'); ++pos; continue; } else if (arg === '--') { - // e.g. `arg` is: '--' // Everything after a bare '--' is considered a positional argument // and is returned verbatim result.positionals = ArrayPrototypeConcat( @@ -127,7 +123,6 @@ const parseArgs = ({ ); return result; } else if (StringPrototypeCharAt(arg, 1) !== '-') { - // e.g. `arg` is: '-f' | '-foo' | '-f=bar' // Look for shortcodes: -fXzy and expand them to -f -X -z -y: if (arg.length > 2) { // `arg` is '-foo' @@ -152,7 +147,6 @@ const parseArgs = ({ } if (StringPrototypeIncludes(arg, '=')) { - // e.g. `arg` is: 'for=bar' | 'foo=bar=baz' // Store option=value same way independent of `withValue` as: // - looks like a value, store as a value // - match the intention of the user @@ -166,9 +160,6 @@ const parseArgs = ({ } else if (pos + 1 < args.length && !StringPrototypeStartsWith(args[pos + 1], '-') ) { - // If next arg is NOT a flag, check if the current arg is - // is configured to use `withValue` and store the next arg. - // withValue option should also support setting values when '= // isn't used ie. both --foo=b and --foo b should work From 36ef68cab9a2345834d4436ddf45272c9643b242 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 8 Feb 2022 18:34:11 -0800 Subject: [PATCH 03/30] chore: Remove debug comments --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index 0a05f8e..afe7743 100644 --- a/index.js +++ b/index.js @@ -125,7 +125,6 @@ const parseArgs = ({ } else if (StringPrototypeCharAt(arg, 1) !== '-') { // Look for shortcodes: -fXzy and expand them to -f -X -z -y: if (arg.length > 2) { - // `arg` is '-foo' for (let i = 2; i < arg.length; i++) { const short = StringPrototypeCharAt(arg, i); // Add 'i' to 'pos' such that short options are parsed in order From 67c0b034a7f4fed75d16bfd4cb5afc6c0caefc18 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 8 Feb 2022 21:41:47 -0800 Subject: [PATCH 04/30] chore: Alias args to argv to introduce less changes --- index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index afe7743..81549e6 100644 --- a/index.js +++ b/index.js @@ -79,10 +79,10 @@ function storeOptionValue(options, arg, value, result) { } const parseArgs = ({ - args = getMainArgs(), + args: argv = getMainArgs(), options = {} } = {}) => { - validateArray(args, 'args'); + validateArray(argv, 'argv'); validateObject(options, 'options'); for (const [arg, option] of ObjectEntries(options)) { validateObject(option, `options.${arg}`); @@ -105,8 +105,8 @@ const parseArgs = ({ }; let pos = 0; - while (pos < args.length) { - let arg = args[pos]; + while (pos < argv.length) { + let arg = argv[pos]; if (StringPrototypeStartsWith(arg, '-')) { if (arg === '-') { @@ -119,7 +119,7 @@ const parseArgs = ({ // and is returned verbatim result.positionals = ArrayPrototypeConcat( result.positionals, - ArrayPrototypeSlice(args, ++pos) + ArrayPrototypeSlice(argv, ++pos) ); return result; } else if (StringPrototypeCharAt(arg, 1) !== '-') { @@ -129,7 +129,7 @@ const parseArgs = ({ const short = StringPrototypeCharAt(arg, i); // Add 'i' to 'pos' such that short options are parsed in order // of definition: - ArrayPrototypeSplice(args, pos + (i - 1), 0, `-${short}`); + ArrayPrototypeSplice(argv, pos + (i - 1), 0, `-${short}`); } } @@ -156,8 +156,8 @@ const parseArgs = ({ StringPrototypeSlice(arg, 0, index), StringPrototypeSlice(arg, index + 1), result); - } else if (pos + 1 < args.length && - !StringPrototypeStartsWith(args[pos + 1], '-') + } else if (pos + 1 < argv.length && + !StringPrototypeStartsWith(argv[pos + 1], '-') ) { // withValue option should also support setting values when '= // isn't used ie. both --foo=b and --foo b should work @@ -167,7 +167,7 @@ const parseArgs = ({ // arg, else set value as undefined ie. --foo b --bar c, after setting // b as the value for foo, evaluate --bar next and skip 'b' const val = options[arg] && options[arg].withValue ? - args[++pos] : + argv[++pos] : undefined; storeOptionValue(options, arg, val, result); } else { From 082bec5a5a667ee4d1d7725e4b8fcfa2199d0b4c Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 12 Feb 2022 09:46:07 -0800 Subject: [PATCH 05/30] feat: Replace option with --- index.js | 21 +++++++++++--------- test/index.js | 55 ++++++++++++++++++++++++++++++--------------------- validators.js | 7 +++++++ 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/index.js b/index.js index 81549e6..d4c1f04 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ const { validateArray, validateObject, validateString, + validateUnion, validateBoolean, } = require('./validators'); @@ -87,14 +88,16 @@ const parseArgs = ({ for (const [arg, option] of ObjectEntries(options)) { validateObject(option, `options.${arg}`); + if (ObjectHasOwn(option, 'type')) { + validateUnion(option.type, `options.${arg}.type`, ['string', 'boolean']); + } + if (ObjectHasOwn(option, 'short')) { validateString(option.short, `options.${arg}.short`); } - for (const config of ['withValue', 'multiples']) { - if (ObjectHasOwn(option, config)) { - validateBoolean(option[config], `options.${arg}.${config}`); - } + if (ObjectHasOwn(option, 'multiples')) { + validateBoolean(option.multiples, `options.${arg}.multiples`); } } @@ -146,7 +149,7 @@ const parseArgs = ({ } if (StringPrototypeIncludes(arg, '=')) { - // Store option=value same way independent of `withValue` as: + // Store option=value same way independent of `type: "string"` as: // - looks like a value, store as a value // - match the intention of the user // - preserve information for author to process further @@ -159,14 +162,14 @@ const parseArgs = ({ } else if (pos + 1 < argv.length && !StringPrototypeStartsWith(argv[pos + 1], '-') ) { - // withValue option should also support setting values when '= + // `type: "string"` option should also support setting values when '=' // isn't used ie. both --foo=b and --foo b should work - // If withValue option is specified, take next position argument as - // value and then increment pos so that we don't re-evaluate that + // If `type: "string"` option is specified, take next position argument + // as value and then increment pos so that we don't re-evaluate that // arg, else set value as undefined ie. --foo b --bar c, after setting // b as the value for foo, evaluate --bar next and skip 'b' - const val = options[arg] && options[arg].withValue ? + const val = options[arg] && options[arg].type === 'string' ? argv[++pos] : undefined; storeOptionValue(options, arg, val, result); diff --git a/test/index.js b/test/index.js index cbd963e..912e17d 100644 --- a/test/index.js +++ b/test/index.js @@ -26,9 +26,9 @@ test('when short option used as flag before positional then stored as flag and p t.end(); }); -test('when short option withValue used with value then stored as value', function(t) { +test('when short option `type: "string"` used with value then stored as value', function(t) { const passedArgs = ['-f', 'bar']; - const passedOptions = { f: { withValue: true } }; + const passedOptions = { f: { type: 'string' } }; const expected = { flags: { f: true }, values: { f: 'bar' }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -48,9 +48,9 @@ test('when short option listed in short used as flag then long option stored as t.end(); }); -test('when short option listed in short and long listed in withValue and used with value then long option stored as value', function(t) { +test('when short option listed in short and long listed in `type: "string"` and used with value then long option stored as value', function(t) { const passedArgs = ['-f', 'bar']; - const passedOptions = { foo: { short: 'f', withValue: true } }; + const passedOptions = { foo: { short: 'f', type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'bar' }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -59,9 +59,9 @@ test('when short option listed in short and long listed in withValue and used wi t.end(); }); -test('when short option withValue used without value then stored as flag', function(t) { +test('when short option `type: "string"` used without value then stored as flag', function(t) { const passedArgs = ['-f']; - const passedOptions = { f: { withValue: true } }; + const passedOptions = { f: { type: 'string' } }; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -92,9 +92,9 @@ test('short option group does not consume subsequent positional', function(t) { }); // // See: Guideline 5 https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html -test('if terminal of short-option group configured withValue, subsequent positional is stored', function(t) { +test('if terminal of short-option group configured `type: "string"`, subsequent positional is stored', function(t) { const passedArgs = ['-rvf', 'foo']; - const passedOptions = { f: { withValue: true } }; + const passedOptions = { f: { type: 'string' } }; const expected = { flags: { r: true, f: true, v: true }, values: { r: undefined, v: undefined, f: 'foo' }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -104,7 +104,7 @@ test('if terminal of short-option group configured withValue, subsequent positio test('handles short-option groups in conjunction with long-options', function(t) { const passedArgs = ['-rf', '--foo', 'foo']; - const passedOptions = { foo: { withValue: true } }; + const passedOptions = { foo: { type: 'string' } }; const expected = { flags: { r: true, f: true, foo: true }, values: { r: undefined, f: undefined, foo: 'foo' }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -152,9 +152,9 @@ test('arg is true and positional is identified', function(t) { t.end(); }); -test('args equals are passed "withValue"', function(t) { +test('args equals are passed `type: "string"`', function(t) { const passedArgs = ['--so=wat']; - const passedOptions = { so: { withValue: true } }; + const passedOptions = { so: { type: 'string' } }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -173,7 +173,7 @@ test('when args include single dash then result stores dash as positional', func t.end(); }); -test('zero config args equals are parsed as if "withValue"', function(t) { +test('zero config args equals are parsed as if `type: "string"`', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; @@ -184,9 +184,9 @@ test('zero config args equals are parsed as if "withValue"', function(t) { t.end(); }); -test('same arg is passed twice "withValue" and last value is recorded', function(t) { +test('same arg is passed twice `type: "string"` and last value is recorded', function(t) { const passedArgs = ['--foo=a', '--foo', 'b']; - const passedOptions = { foo: { withValue: true } }; + const passedOptions = { foo: { type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'b' }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -197,7 +197,7 @@ test('same arg is passed twice "withValue" and last value is recorded', function test('args equals pass string including more equals', function(t) { const passedArgs = ['--so=wat=bing']; - const passedOptions = { so: { withValue: true } }; + const passedOptions = { so: { type: 'string' } }; const expected = { flags: { so: true }, values: { so: 'wat=bing' }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -206,9 +206,9 @@ test('args equals pass string including more equals', function(t) { t.end(); }); -test('first arg passed for "withValue" and "multiples" is in array', function(t) { +test('first arg passed for `type: "string"` and "multiples" is in array', function(t) { const passedArgs = ['--foo=a']; - const passedOptions = { foo: { withValue: true, multiples: true } }; + const passedOptions = { foo: { type: 'string', multiples: true } }; const expected = { flags: { foo: true }, values: { foo: ['a'] }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -217,11 +217,11 @@ test('first arg passed for "withValue" and "multiples" is in array', function(t) t.end(); }); -test('args are passed "withValue" and "multiples"', function(t) { +test('args are passed `type: "string"` and "multiples"', function(t) { const passedArgs = ['--foo=a', '--foo', 'b']; const passedOptions = { foo: { - withValue: true, + type: 'string', multiples: true, }, }; @@ -236,7 +236,7 @@ test('args are passed "withValue" and "multiples"', function(t) { test('order of option and positional does not matter (per README)', function(t) { const passedArgs1 = ['--foo=bar', 'baz']; const passedArgs2 = ['baz', '--foo=bar']; - const passedOptions = { foo: { withValue: true } }; + const passedOptions = { foo: { type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'bar' }, positionals: ['baz'] }; t.deepEqual(parseArgs({ args: passedArgs1, options: passedOptions }), expected, 'option then positional'); @@ -359,9 +359,20 @@ test('invalid argument passed for options', function(t) { t.end(); }); -test('string passed to "withValue" option', function(t) { +test('boolean passed to "type" option', function(t) { const passedArgs = ['--so=wat']; - const passedOptions = { foo: { withValue: 'bad value' } }; + const passedOptions = { foo: { type: true } }; + + t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + + t.end(); +}); + +test('invalid union value passed to "type" option', function(t) { + const passedArgs = ['--so=wat']; + const passedOptions = { foo: { type: 'str' } }; t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_ARG_TYPE' diff --git a/validators.js b/validators.js index 2aad2ef..3c7dd32 100644 --- a/validators.js +++ b/validators.js @@ -16,6 +16,12 @@ function validateString(value, name) { } } +function validateUnion(value, name, union) { + if (!union.includes(value)) { + throw new ERR_INVALID_ARG_TYPE(name, `[${union.join('|')}]`, value); + } +} + function validateBoolean(value, name) { if (typeof value !== 'boolean') { throw new ERR_INVALID_ARG_TYPE(name, 'Boolean', value); @@ -55,5 +61,6 @@ module.exports = { validateArray, validateObject, validateString, + validateUnion, validateBoolean, }; From 09090081a42c5eea90a338e6cd1a2d2bfe1ec978 Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 12 Feb 2022 10:21:28 -0800 Subject: [PATCH 06/30] docs: Update README to reflect updated implementation --- README.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a22a9b0..c2608c3 100644 --- a/README.md +++ b/README.md @@ -69,19 +69,16 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2) ---- -## 💡 `util.parseArgs([argv][, options])` Proposal +## 💡 `util.parseArgs([config])` Proposal -* `argv` {string[]} (Optional) Array of argument strings; defaults - to [`process.mainArgs`](process_argv) -* `options` {Object} (Optional) The `options` parameter is an +* `config` {Object} (Optional) The `config` parameter is an object supporting the following properties: - * `withValue` {string[]} (Optional) An `Array` of argument - strings which expect a value to be defined in `argv` (see [Options][] - for details) - * `multiples` {string[]} (Optional) An `Array` of argument - strings which, when appearing multiple times in `argv`, will be concatenated -into an `Array` - * `short` {Object} (Optional) An `Object` of key, value pairs of strings which map a "short" alias to an argument; When appearing multiples times in `argv`; Respects `withValue` & `multiples` + * `argv` {string[]} (Optional) Array of argument strings; defaults + to [`process.mainArgs`](process_argv) + * `options` {Object} (Optional) A collection of configuration objects for each `argv`; `options` keys are the long names of the `argv`, and the values are objects with the following properties: + * `type` {'string'|'boolean'} (Optional) Type of `argv`; defaults to `'boolean'`; + * `multiples` {boolean} (Optional) If true, when appearing multiple times in `argv`, will be concatenated into an `Array` + * `short` {string} (Optional) An alias to an `argv`; When appearing multiples times in `argv`; Respects the `multiples` configuration * `strict` {Boolean} (Optional) A `Boolean` on wheather or not to throw an error when unknown args are encountered * Returns: {Object} An object having properties: * `flags` {Object}, having properties and `Boolean` values corresponding to parsed options passed @@ -101,7 +98,7 @@ const { parseArgs } = require('@pkgjs/parseargs'); const { parseArgs } = require('@pkgjs/parseargs'); const argv = ['-f', '--foo=a', '--bar', 'b']; const options = {}; -const { flags, values, positionals } = parseArgs(argv, options); +const { flags, values, positionals } = parseArgs({ argv, options }); // flags = { f: true, bar: true } // values = { foo: 'a' } // positionals = ['b'] @@ -112,9 +109,11 @@ const { parseArgs } = require('@pkgjs/parseargs'); // withValue const argv = ['-f', '--foo=a', '--bar', 'b']; const options = { - withValue: ['bar'] + foo: { + type: 'string', + }, }; -const { flags, values, positionals } = parseArgs(argv, options); +const { flags, values, positionals } = parseArgs({ argv, options }); // flags = { f: true } // values = { foo: 'a', bar: 'b' } // positionals = [] @@ -125,10 +124,12 @@ const { parseArgs } = require('@pkgjs/parseargs'); // withValue & multiples const argv = ['-f', '--foo=a', '--foo', 'b']; const options = { - withValue: ['foo'], - multiples: ['foo'] + foo: { + type: 'string', + multiples: true, + }, }; -const { flags, values, positionals } = parseArgs(argv, options); +const { flags, values, positionals } = parseArgs({ argv, options }); // flags = { f: true } // values = { foo: ['a', 'b'] } // positionals = [] @@ -139,9 +140,11 @@ const { parseArgs } = require('@pkgjs/parseargs'); // shorts const argv = ['-f', 'b']; const options = { - short: { f: 'foo' } + foo: { + short: 'f', + }, }; -const { flags, values, positionals } = parseArgs(argv, options); +const { flags, values, positionals } = parseArgs({ argv, options }); // flags = { foo: true } // values = {} // positionals = ['b'] From 042480bd252836df476f1755d974ce816276a9da Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 12 Feb 2022 10:24:25 -0800 Subject: [PATCH 07/30] chore: Revert args options to argv --- index.js | 2 +- test/index.js | 54 +++++++++++++++++++++++++-------------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index d4c1f04..6378dfc 100644 --- a/index.js +++ b/index.js @@ -80,7 +80,7 @@ function storeOptionValue(options, arg, value, result) { } const parseArgs = ({ - args: argv = getMainArgs(), + argv = getMainArgs(), options = {} } = {}) => { validateArray(argv, 'argv'); diff --git a/test/index.js b/test/index.js index 912e17d..60cac04 100644 --- a/test/index.js +++ b/test/index.js @@ -9,7 +9,7 @@ const { parseArgs } = require('../index.js'); test('when short option used as flag then stored as flag', function(t) { const passedArgs = ['-f']; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ argv: passedArgs }); t.deepEqual(args, expected); @@ -19,7 +19,7 @@ test('when short option used as flag then stored as flag', function(t) { test('when short option used as flag before positional then stored as flag and positional (and not value)', function(t) { const passedArgs = ['-f', 'bar']; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [ 'bar' ] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ argv: passedArgs }); t.deepEqual(args, expected); @@ -30,7 +30,7 @@ test('when short option `type: "string"` used with value then stored as value', const passedArgs = ['-f', 'bar']; const passedOptions = { f: { type: 'string' } }; const expected = { flags: { f: true }, values: { f: 'bar' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -41,7 +41,7 @@ test('when short option listed in short used as flag then long option stored as const passedArgs = ['-f']; const passedOptions = { foo: { short: 'f' } }; const expected = { flags: { foo: true }, values: { foo: undefined }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -52,7 +52,7 @@ test('when short option listed in short and long listed in `type: "string"` and const passedArgs = ['-f', 'bar']; const passedOptions = { foo: { short: 'f', type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'bar' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -63,7 +63,7 @@ test('when short option `type: "string"` used without value then stored as flag' const passedArgs = ['-f']; const passedOptions = { f: { type: 'string' } }; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -74,7 +74,7 @@ test('short option group behaves like multiple short options', function(t) { const passedArgs = ['-rf']; const passedOptions = { }; const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -85,7 +85,7 @@ test('short option group does not consume subsequent positional', function(t) { const passedArgs = ['-rf', 'foo']; const passedOptions = { }; const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['foo'] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -96,7 +96,7 @@ test('if terminal of short-option group configured `type: "string"`, subsequent const passedArgs = ['-rvf', 'foo']; const passedOptions = { f: { type: 'string' } }; const expected = { flags: { r: true, f: true, v: true }, values: { r: undefined, v: undefined, f: 'foo' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -106,7 +106,7 @@ test('handles short-option groups in conjunction with long-options', function(t) const passedArgs = ['-rf', '--foo', 'foo']; const passedOptions = { foo: { type: 'string' } }; const expected = { flags: { r: true, f: true, foo: true }, values: { r: undefined, f: undefined, foo: 'foo' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -116,7 +116,7 @@ test('handles short-option groups with "short" alias configured', function(t) { const passedArgs = ['-rf']; const passedOptions = { remove: { short: 'r' } }; const expected = { flags: { remove: true, f: true }, values: { remove: undefined, f: undefined }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -125,7 +125,7 @@ test('handles short-option groups with "short" alias configured', function(t) { test('Everything after a bare `--` is considered a positional argument', function(t) { const passedArgs = ['--', 'barepositionals', 'mopositionals']; const expected = { flags: {}, values: {}, positionals: ['barepositionals', 'mopositionals'] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ argv: passedArgs }); t.deepEqual(args, expected, 'testing bare positionals'); @@ -135,7 +135,7 @@ test('Everything after a bare `--` is considered a positional argument', functio test('args are true', function(t) { const passedArgs = ['--foo', '--bar']; const expected = { flags: { foo: true, bar: true }, values: { foo: undefined, bar: undefined }, positionals: [] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ argv: passedArgs }); t.deepEqual(args, expected, 'args are true'); @@ -145,7 +145,7 @@ test('args are true', function(t) { test('arg is true and positional is identified', function(t) { const passedArgs = ['--foo=a', '--foo', 'b']; const expected = { flags: { foo: true }, values: { foo: undefined }, positionals: ['b'] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ argv: passedArgs }); t.deepEqual(args, expected, 'arg is true and positional is identified'); @@ -156,7 +156,7 @@ test('args equals are passed `type: "string"`', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { so: { type: 'string' } }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'arg value is passed'); @@ -166,7 +166,7 @@ test('args equals are passed `type: "string"`', function(t) { test('when args include single dash then result stores dash as positional', function(t) { const passedArgs = ['-']; const expected = { flags: { }, values: { }, positionals: ['-'] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ argv: passedArgs }); t.deepEqual(args, expected); @@ -177,7 +177,7 @@ test('zero config args equals are parsed as if `type: "string"`', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'arg value is passed'); @@ -188,7 +188,7 @@ test('same arg is passed twice `type: "string"` and last value is recorded', fun const passedArgs = ['--foo=a', '--foo', 'b']; const passedOptions = { foo: { type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'b' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'last arg value is passed'); @@ -199,7 +199,7 @@ test('args equals pass string including more equals', function(t) { const passedArgs = ['--so=wat=bing']; const passedOptions = { so: { type: 'string' } }; const expected = { flags: { so: true }, values: { so: 'wat=bing' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'arg value is passed'); @@ -210,7 +210,7 @@ test('first arg passed for `type: "string"` and "multiples" is in array', functi const passedArgs = ['--foo=a']; const passedOptions = { foo: { type: 'string', multiples: true } }; const expected = { flags: { foo: true }, values: { foo: ['a'] }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'first multiple in array'); @@ -226,7 +226,7 @@ test('args are passed `type: "string"` and "multiples"', function(t) { }, }; const expected = { flags: { foo: true }, values: { foo: ['a', 'b'] }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'both arg values are passed'); @@ -239,8 +239,8 @@ test('order of option and positional does not matter (per README)', function(t) const passedOptions = { foo: { type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'bar' }, positionals: ['baz'] }; - t.deepEqual(parseArgs({ args: passedArgs1, options: passedOptions }), expected, 'option then positional'); - t.deepEqual(parseArgs({ args: passedArgs2, options: passedOptions }), expected, 'positional then option'); + t.deepEqual(parseArgs({ argv: passedArgs1, options: passedOptions }), expected, 'option then positional'); + t.deepEqual(parseArgs({ argv: passedArgs2, options: passedOptions }), expected, 'positional then option'); t.end(); }); @@ -339,7 +339,7 @@ test('excess leading dashes on options are retained', function(t) { values: { '-triple': undefined }, positionals: [] }; - const result = parseArgs({ args: passedArgs, options: passedOptions }); + const result = parseArgs({ argv: passedArgs, options: passedOptions }); t.deepEqual(result, expected, 'excess option dashes are retained'); @@ -352,7 +352,7 @@ test('invalid argument passed for options', function(t) { const passedArgs = ['--so=wat']; const passedOptions = 'bad value'; - t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { + t.throws(function() { parseArgs({ argv: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_ARG_TYPE' }); @@ -363,7 +363,7 @@ test('boolean passed to "type" option', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { foo: { type: true } }; - t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { + t.throws(function() { parseArgs({ argv: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_ARG_TYPE' }); @@ -374,7 +374,7 @@ test('invalid union value passed to "type" option', function(t) { const passedArgs = ['--so=wat']; const passedOptions = { foo: { type: 'str' } }; - t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { + t.throws(function() { parseArgs({ argv: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_ARG_TYPE' }); From 10df671bfa210478a2d8ee4a4f8db0f4000eb624 Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 19 Feb 2022 10:55:33 -0800 Subject: [PATCH 08/30] feat: Add strict mode to parser --- errors.js | 8 ++++++++ index.js | 23 +++++++++++++++++++---- test/index.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/errors.js b/errors.js index 2220b66..518116e 100644 --- a/errors.js +++ b/errors.js @@ -7,8 +7,16 @@ class ERR_INVALID_ARG_TYPE extends TypeError { } } +class ERR_UNKNOWN_OPTION extends Error { + constructor(option) { + super(`Unknown option '${option}' is not permitted in strict mode`); + this.code = 'ERR_UNKNOWN_OPTION'; + } +} + module.exports = { codes: { + ERR_UNKNOWN_OPTION, ERR_INVALID_ARG_TYPE, } }; diff --git a/index.js b/index.js index 6378dfc..732761d 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,12 @@ const { validateBoolean, } = require('./validators'); +const { + codes: { + ERR_UNKNOWN_OPTION, + }, +} = require('./errors'); + function getMainArgs() { // This function is a placeholder for proposed process.mainArgs. // Work out where to slice process.argv for user supplied arguments. @@ -56,8 +62,14 @@ function getMainArgs() { return ArrayPrototypeSlice(process.argv, 2); } -function storeOptionValue(options, arg, value, result) { - const option = options[arg] || {}; +function storeOptionValue(strict, options, arg, value, result) { + let option = options[arg]; + + if (strict && !option) { + throw new ERR_UNKNOWN_OPTION(arg); + } else { + option = {}; + } // Flags result.flags[arg] = true; @@ -81,9 +93,11 @@ function storeOptionValue(options, arg, value, result) { const parseArgs = ({ argv = getMainArgs(), + strict = false, options = {} } = {}) => { validateArray(argv, 'argv'); + validateBoolean(strict, 'strict'); validateObject(options, 'options'); for (const [arg, option] of ObjectEntries(options)) { validateObject(option, `options.${arg}`); @@ -155,6 +169,7 @@ const parseArgs = ({ // - preserve information for author to process further const index = StringPrototypeIndexOf(arg, '='); storeOptionValue( + strict, options, StringPrototypeSlice(arg, 0, index), StringPrototypeSlice(arg, index + 1), @@ -172,12 +187,12 @@ const parseArgs = ({ const val = options[arg] && options[arg].type === 'string' ? argv[++pos] : undefined; - storeOptionValue(options, arg, val, result); + storeOptionValue(strict, options, arg, val, result); } else { // Cases when an arg is specified without a value, example // '--foo --bar' <- 'foo' and 'bar' flags should be set to true and // shave value as undefined - storeOptionValue(options, arg, undefined, result); + storeOptionValue(strict, options, arg, undefined, result); } } else { // Arguements without a dash prefix are considered "positional" diff --git a/test/index.js b/test/index.js index 60cac04..74865ee 100644 --- a/test/index.js +++ b/test/index.js @@ -380,3 +380,53 @@ test('invalid union value passed to "type" option', function(t) { t.end(); }); + +// Test strict mode + +test('unknown long option --bar', function(t) { + const passedArgs = ['--foo', '--bar']; + const passedOptions = { foo: { type: 'string' } }; + const strict = true; + + t.throws(function() { parseArgs({ strict, argv: passedArgs, options: passedOptions }); }, { + code: 'ERR_UNKNOWN_OPTION' + }); + + t.end(); +}); + +test('unknown short option --b', function(t) { + const passedArgs = ['--foo', '-b']; + const passedOptions = { foo: { type: 'string' } }; + const strict = true; + + t.throws(function() { parseArgs({ strict, argv: passedArgs, options: passedOptions }); }, { + code: 'ERR_UNKNOWN_OPTION' + }); + + t.end(); +}); + +test('unknown option -r in short option group -bar', function(t) { + const passedArgs = ['--foo', '-bar']; + const passedOptions = { foo: { type: 'string' }, b: { type: 'string' }, a: { type: 'string' } }; + const strict = true; + + t.throws(function() { parseArgs({ strict, argv: passedArgs, options: passedOptions }); }, { + code: 'ERR_UNKNOWN_OPTION' + }); + + t.end(); +}); + +test('unknown option with explicit value', function(t) { + const passedArgs = ['--foo', '--bar=baz']; + const passedOptions = { foo: { type: 'string' } }; + const strict = true; + + t.throws(function() { parseArgs({ strict, argv: passedArgs, options: passedOptions }); }, { + code: 'ERR_UNKNOWN_OPTION' + }); + + t.end(); +}); From e44c46c758cd8882ae7ad520323f514eac40691b Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Wed, 23 Feb 2022 18:02:35 -0800 Subject: [PATCH 09/30] docs: Update README from PR feedback --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c2608c3..22a374e 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,10 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2) object supporting the following properties: * `argv` {string[]} (Optional) Array of argument strings; defaults to [`process.mainArgs`](process_argv) - * `options` {Object} (Optional) A collection of configuration objects for each `argv`; `options` keys are the long names of the `argv`, and the values are objects with the following properties: - * `type` {'string'|'boolean'} (Optional) Type of `argv`; defaults to `'boolean'`; - * `multiples` {boolean} (Optional) If true, when appearing multiple times in `argv`, will be concatenated into an `Array` - * `short` {string} (Optional) An alias to an `argv`; When appearing multiples times in `argv`; Respects the `multiples` configuration + * `options` {Object} (Optional) An object describing the known options to look for in `argv`; `options` keys are the long names of the known options, and the values are objects with the following properties: + * `type` {'string'|'boolean'} (Optional) Type of known option; defaults to `'boolean'`; + * `multiples` {boolean} (Optional) If true, when appearing one or more times in `argv`, results are collected in an `Array` + * `short` {string} (Optional) A single character alias for an option; When appearing one or more times in `argv`; Respects the `multiples` configuration * `strict` {Boolean} (Optional) A `Boolean` on wheather or not to throw an error when unknown args are encountered * Returns: {Object} An object having properties: * `flags` {Object}, having properties and `Boolean` values corresponding to parsed options passed From a50e94005d4677815219b7bcb426e087b9585816 Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 26 Feb 2022 12:55:47 -0800 Subject: [PATCH 10/30] chore: Reduce changes --- index.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 85f970b..db0230e 100644 --- a/index.js +++ b/index.js @@ -56,26 +56,26 @@ function getMainArgs() { return ArrayPrototypeSlice(process.argv, 2); } -function storeOptionValue(options, arg, value, result) { - const option = options[arg] || {}; +function storeOptionValue(options, option, value, result) { + const optionConfig = options[option] || {}; // Flags - result.flags[arg] = true; + result.flags[option] = true; // Values - if (option.multiples) { + if (optionConfig.multiples) { // Always store value in array, including for flags. - // result.values[arg] starts out not present, + // result.values[option] starts out not present, // first value is added as new array [newValue], // subsequent values are pushed to existing array. const usedAsFlag = value === undefined; const newValue = usedAsFlag ? true : value; - if (result.values[arg] !== undefined) - ArrayPrototypePush(result.values[arg], newValue); + if (result.values[option] !== undefined) + ArrayPrototypePush(result.values[option], newValue); else - result.values[arg] = [newValue]; + result.values[option] = [newValue]; } else { - result.values[arg] = value; + result.values[option] = value; } } From 062bdc95055ab3709739c461676c2b8bc4c9e549 Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 26 Feb 2022 13:03:14 -0800 Subject: [PATCH 11/30] chore: Tidy up naming conventions --- index.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index db0230e..466b418 100644 --- a/index.js +++ b/index.js @@ -85,19 +85,19 @@ const parseArgs = ({ } = {}) => { validateArray(argv, 'argv'); validateObject(options, 'options'); - for (const [arg, option] of ObjectEntries(options)) { - validateObject(option, `options.${arg}`); + for (const [option, optionConfig] of ObjectEntries(options)) { + validateObject(optionConfig, `options.${option}`); - if (ObjectHasOwn(option, 'type')) { - validateUnion(option.type, `options.${arg}.type`, ['string', 'boolean']); + if (ObjectHasOwn(optionConfig, 'type')) { + validateUnion(optionConfig.type, `options.${option}.type`, ['string', 'boolean']); } - if (ObjectHasOwn(option, 'short')) { - validateString(option.short, `options.${arg}.short`); + if (ObjectHasOwn(optionConfig, 'short')) { + validateString(optionConfig.short, `options.${option}.short`); } - if (ObjectHasOwn(option, 'multiples')) { - validateBoolean(option.multiples, `options.${arg}.multiples`); + if (ObjectHasOwn(optionConfig, 'multiples')) { + validateBoolean(optionConfig.multiples, `options.${option}.multiples`); } } @@ -137,9 +137,9 @@ const parseArgs = ({ } arg = StringPrototypeCharAt(arg, 1); // short - for (const [longName, option] of ObjectEntries(options)) { - if (option.short === arg) { - arg = longName; + for (const [option, optionConfig] of ObjectEntries(options)) { + if (optionConfig.short === arg) { + arg = option; // now long! break; } } From ef06099fccebd868731945cff1b1ffd02cc625f7 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Sun, 27 Feb 2022 10:23:42 -0800 Subject: [PATCH 12/30] fix: Guard against prototype member access --- index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index f8d8299..fbb1a70 100644 --- a/index.js +++ b/index.js @@ -63,14 +63,14 @@ function getMainArgs() { } function storeOptionValue(strict, options, option, value, result) { - let optionConfig = options[option]; + const hasOptionConfig = ObjectHasOwn(options, option); - if (strict && !option) { + if (strict && !hasOptionConfig) { throw new ERR_UNKNOWN_OPTION(option); - } else { - optionConfig = {}; } + const optionConfig = hasOptionConfig ? options[option] : {}; + // Flags result.flags[option] = true; From dd4f7186b23fbf1c12d36169ace87520d9d4c368 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:58:56 -0800 Subject: [PATCH 13/30] feat: Add strict mode type validation --- index.js | 14 ++++++++++++-- test/index.js | 30 +++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index c80772b..aac0741 100644 --- a/index.js +++ b/index.js @@ -67,8 +67,18 @@ function getMainArgs() { function storeOptionValue(strict, options, longOption, value, result) { const hasOptionConfig = ObjectHasOwn(options, longOption); - if (strict && !hasOptionConfig) { - throw new ERR_UNKNOWN_OPTION(longOption); + if (strict) { + if (!hasOptionConfig) { + throw new ERR_UNKNOWN_OPTION(longOption); + } + + if (options[longOption].type === 'string' && value == null) { + throw new Error(`Missing value for 'string' option: --${longOption}`); + } + + if (options[longOption].type === 'boolean' && value != null) { + throw new Error(`Unexpected value for 'boolean' option: --${longOption}`); + } } const optionConfig = hasOptionConfig ? options[longOption] : {}; diff --git a/test/index.js b/test/index.js index 48e4ef4..e089575 100644 --- a/test/index.js +++ b/test/index.js @@ -385,7 +385,7 @@ test('invalid union value passed to "type" option', function(t) { test('unknown long option --bar', function(t) { const passedArgs = ['--foo', '--bar']; - const passedOptions = { foo: { type: 'string' } }; + const passedOptions = { foo: { type: 'boolean' } }; const strict = true; t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { @@ -395,9 +395,9 @@ test('unknown long option --bar', function(t) { t.end(); }); -test('unknown short option --b', function(t) { +test('unknown short option -b', function(t) { const passedArgs = ['--foo', '-b']; - const passedOptions = { foo: { type: 'string' } }; + const passedOptions = { foo: { type: 'boolean' } }; const strict = true; t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { @@ -409,7 +409,7 @@ test('unknown short option --b', function(t) { test('unknown option -r in short option group -bar', function(t) { const passedArgs = ['--foo', '-bar']; - const passedOptions = { foo: { type: 'string' }, b: { type: 'string' }, a: { type: 'string' } }; + const passedOptions = { foo: { type: 'boolean' }, b: { type: 'boolean' }, a: { type: 'boolean' } }; const strict = true; t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { @@ -421,7 +421,7 @@ test('unknown option -r in short option group -bar', function(t) { test('unknown option with explicit value', function(t) { const passedArgs = ['--foo', '--bar=baz']; - const passedOptions = { foo: { type: 'string' } }; + const passedOptions = { foo: { type: 'boolean' } }; const strict = true; t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { @@ -431,6 +431,26 @@ test('unknown option with explicit value', function(t) { t.end(); }); +test('string option used as boolean', function(t) { + const passedArgs = ['--foo']; + const passedOptions = { foo: { type: 'string' } }; + const strict = true; + + t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + + t.end(); +}); + +test('boolean option used with value', function(t) { + const passedArgs = ['--foo=bar']; + const passedOptions = { foo: { type: 'boolean' } }; + const strict = true; + + t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + + t.end(); +}); + test('invalid short option length', function(t) { const passedArgs = []; const passedOptions = { foo: { short: 'fo' } }; From 26df81c14967d75eee0a4ca34cf22cd5ef4135ea Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Thu, 3 Mar 2022 06:41:11 -0800 Subject: [PATCH 14/30] chore: Remove custom unknown option error --- errors.js | 8 -------- index.js | 8 +------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/errors.js b/errors.js index 518116e..2220b66 100644 --- a/errors.js +++ b/errors.js @@ -7,16 +7,8 @@ class ERR_INVALID_ARG_TYPE extends TypeError { } } -class ERR_UNKNOWN_OPTION extends Error { - constructor(option) { - super(`Unknown option '${option}' is not permitted in strict mode`); - this.code = 'ERR_UNKNOWN_OPTION'; - } -} - module.exports = { codes: { - ERR_UNKNOWN_OPTION, ERR_INVALID_ARG_TYPE, } }; diff --git a/index.js b/index.js index aac0741..d1fa1ce 100644 --- a/index.js +++ b/index.js @@ -24,12 +24,6 @@ const { validateBoolean, } = require('./validators'); -const { - codes: { - ERR_UNKNOWN_OPTION, - }, -} = require('./errors'); - function getMainArgs() { // This function is a placeholder for proposed process.mainArgs. // Work out where to slice process.argv for user supplied arguments. @@ -69,7 +63,7 @@ function storeOptionValue(strict, options, longOption, value, result) { if (strict) { if (!hasOptionConfig) { - throw new ERR_UNKNOWN_OPTION(longOption); + throw new Error(`Unknown option: --${longOption}`); } if (options[longOption].type === 'string' && value == null) { From a04d11c5149701516c79486c3c844d4f0e4e6f06 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Thu, 3 Mar 2022 06:59:18 -0800 Subject: [PATCH 15/30] chore: Remove unknown option error from test --- test/index.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/test/index.js b/test/index.js index e089575..fb02a3a 100644 --- a/test/index.js +++ b/test/index.js @@ -388,9 +388,7 @@ test('unknown long option --bar', function(t) { const passedOptions = { foo: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' - }); + t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); t.end(); }); @@ -400,9 +398,7 @@ test('unknown short option -b', function(t) { const passedOptions = { foo: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' - }); + t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); t.end(); }); @@ -412,9 +408,7 @@ test('unknown option -r in short option group -bar', function(t) { const passedOptions = { foo: { type: 'boolean' }, b: { type: 'boolean' }, a: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' - }); + t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); t.end(); }); @@ -424,9 +418,7 @@ test('unknown option with explicit value', function(t) { const passedOptions = { foo: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' - }); + t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); t.end(); }); From ca05b430e41c3056c367e6169329b5ea42d21d0b Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Thu, 3 Mar 2022 13:07:02 -0800 Subject: [PATCH 16/30] fix: Update value validation to check against undefined --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index d1fa1ce..f39b254 100644 --- a/index.js +++ b/index.js @@ -66,11 +66,11 @@ function storeOptionValue(strict, options, longOption, value, result) { throw new Error(`Unknown option: --${longOption}`); } - if (options[longOption].type === 'string' && value == null) { + if (options[longOption].type === 'string' && value === undefined) { throw new Error(`Missing value for 'string' option: --${longOption}`); } - if (options[longOption].type === 'boolean' && value != null) { + if (options[longOption].type === 'boolean' && value !== undefined) { throw new Error(`Unexpected value for 'boolean' option: --${longOption}`); } } From fa0775bb3f99a3eb52fbd60a97da028205d6d7f1 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Fri, 8 Apr 2022 22:13:11 -0700 Subject: [PATCH 17/30] fix: Update error conditionals to check for null or undefined values --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index b0d5a27..1d0c19f 100644 --- a/index.js +++ b/index.js @@ -74,11 +74,11 @@ function storeOptionValue(strict, options, longOption, value, result) { throw new Error(`Unknown option: --${longOption}`); } - if (options[longOption].type === 'string' && value === undefined) { + if (options[longOption].type === 'string' && value == null) { throw new Error(`Missing value for 'string' option: --${longOption}`); } - if (options[longOption].type === 'boolean' && value !== undefined) { + if (options[longOption].type === 'boolean' && value != null) { throw new Error(`Unexpected value for 'boolean' option: --${longOption}`); } } From 1d412dafd805ac9f334be666e4db18742411b06d Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 9 Apr 2022 18:18:13 -0700 Subject: [PATCH 18/30] feat: Add custom error classes for unknown and invalid options --- errors.js | 18 +++++++++++++++++- index.js | 8 +++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/errors.js b/errors.js index 1b9eb95..8d17d44 100644 --- a/errors.js +++ b/errors.js @@ -7,6 +7,13 @@ class ERR_INVALID_ARG_TYPE extends TypeError { } } +class ERR_INVALID_OPTION_VALUE extends Error { + constructor(message) { + super(message); + this.code = 'ERR_INVALID_OPTION_VALUE'; + } +} + class ERR_INVALID_SHORT_OPTION extends TypeError { constructor(longOption, shortOption) { super(`options.${longOption}.short must be a single character, got '${shortOption}'`); @@ -14,9 +21,18 @@ class ERR_INVALID_SHORT_OPTION extends TypeError { } } +class ERR_UNKNOWN_OPTION extends Error { + constructor(longOption) { + super(`Unknown option: --${longOption}`); + this.code = 'ERR_UNKNOWN_OPTION'; + } +} + module.exports = { codes: { ERR_INVALID_ARG_TYPE, - ERR_INVALID_SHORT_OPTION + ERR_INVALID_OPTION_VALUE, + ERR_INVALID_SHORT_OPTION, + ERR_UNKNOWN_OPTION, } }; diff --git a/index.js b/index.js index 7f11e4a..47328c8 100644 --- a/index.js +++ b/index.js @@ -34,7 +34,9 @@ const { const { codes: { + ERR_INVALID_OPTION_VALUE, ERR_INVALID_SHORT_OPTION, + ERR_UNKNOWN_OPTION, }, } = require('./errors'); @@ -79,15 +81,15 @@ function storeOptionValue(strict, options, longOption, value, result) { if (strict) { if (!hasOptionConfig) { - throw new Error(`Unknown option: --${longOption}`); + throw new ERR_UNKNOWN_OPTION(longOption); } if (options[longOption].type === 'string' && value == null) { - throw new Error(`Missing value for 'string' option: --${longOption}`); + throw new ERR_INVALID_OPTION_VALUE(`Missing value for '${longOption}' option with type:'string'`); } if (options[longOption].type === 'boolean' && value != null) { - throw new Error(`Unexpected value for 'boolean' option: --${longOption}`); + throw new ERR_INVALID_OPTION_VALUE(`Unexpected value '${value}' for '${longOption}' option with type:'boolean'`); } } From a16fc7ba4360bd1a89fee58250883a352e2b17c2 Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 9 Apr 2022 18:43:02 -0700 Subject: [PATCH 19/30] Adjust unknown option message to be agnostic of long or short options --- errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.js b/errors.js index 8d17d44..1a83ee2 100644 --- a/errors.js +++ b/errors.js @@ -23,7 +23,7 @@ class ERR_INVALID_SHORT_OPTION extends TypeError { class ERR_UNKNOWN_OPTION extends Error { constructor(longOption) { - super(`Unknown option: --${longOption}`); + super(`Unknown option '${longOption}'`); this.code = 'ERR_UNKNOWN_OPTION'; } } From c4271a065b722e027601c94e019dc2e8a7a04434 Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sat, 9 Apr 2022 20:36:38 -0700 Subject: [PATCH 20/30] feat: Pass shortOption to storeOption util for better error messages --- errors.js | 4 ++-- index.js | 51 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/errors.js b/errors.js index 1a83ee2..db02fd0 100644 --- a/errors.js +++ b/errors.js @@ -22,8 +22,8 @@ class ERR_INVALID_SHORT_OPTION extends TypeError { } class ERR_UNKNOWN_OPTION extends Error { - constructor(longOption) { - super(`Unknown option '${longOption}'`); + constructor(message) { + super(message); this.code = 'ERR_UNKNOWN_OPTION'; } } diff --git a/index.js b/index.js index 47328c8..5ee83d3 100644 --- a/index.js +++ b/index.js @@ -76,20 +76,31 @@ function getMainArgs() { const protoKey = '__proto__'; -function storeOptionValue(strict, options, longOption, value, result) { +function storeOption({ + strict, + options, + result, + longOption, + shortOption, + optionValue, +}) { const hasOptionConfig = ObjectHasOwn(options, longOption); if (strict) { + const longOrShortOption = shortOption == null ? + `long option '--${longOption}'` : + `short option '-${shortOption}'`; + if (!hasOptionConfig) { - throw new ERR_UNKNOWN_OPTION(longOption); + throw new ERR_UNKNOWN_OPTION(`Unknown ${longOrShortOption}`); } - if (options[longOption].type === 'string' && value == null) { - throw new ERR_INVALID_OPTION_VALUE(`Missing value for '${longOption}' option with type:'string'`); + if (options[longOption].type === 'string' && optionValue == null) { + throw new ERR_INVALID_OPTION_VALUE(`Missing value for ${longOrShortOption} with type:'string'`); } - if (options[longOption].type === 'boolean' && value != null) { - throw new ERR_INVALID_OPTION_VALUE(`Unexpected value '${value}' for '${longOption}' option with type:'boolean'`); + if (options[longOption].type === 'boolean' && optionValue != null) { + throw new ERR_INVALID_OPTION_VALUE(`Unexpected value '${optionValue}' for ${longOrShortOption} with type:'boolean'`); } } @@ -108,14 +119,14 @@ function storeOptionValue(strict, options, longOption, value, result) { // result.values[longOption] starts out not present, // first value is added as new array [newValue], // subsequent values are pushed to existing array. - const usedAsFlag = value === undefined; - const newValue = usedAsFlag ? true : value; + const usedAsFlag = optionValue === undefined; + const newValue = usedAsFlag ? true : optionValue; if (result.values[longOption] !== undefined) ArrayPrototypePush(result.values[longOption], newValue); else result.values[longOption] = [newValue]; } else { - result.values[longOption] = value; + result.values[longOption] = optionValue; } } @@ -181,7 +192,14 @@ const parseArgs = ({ // e.g. '-f', 'bar' optionValue = ArrayPrototypeShift(remainingArgs); } - storeOptionValue(strict, options, longOption, optionValue, result); + storeOption({ + strict, + options, + result, + longOption, + shortOption, + optionValue, + }); continue; } @@ -212,7 +230,14 @@ const parseArgs = ({ const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); const optionValue = StringPrototypeSlice(arg, 2); - storeOptionValue(strict, options, longOption, optionValue, result); + storeOption({ + strict, + options, + result, + longOption, + shortOption, + optionValue, + }); continue; } @@ -224,7 +249,7 @@ const parseArgs = ({ // e.g. '--foo', 'bar' optionValue = ArrayPrototypeShift(remainingArgs); } - storeOptionValue(strict, options, longOption, optionValue, result); + storeOption({ strict, options, result, longOption, optionValue }); continue; } @@ -233,7 +258,7 @@ const parseArgs = ({ const index = StringPrototypeIndexOf(arg, '='); const longOption = StringPrototypeSlice(arg, 2, index); const optionValue = StringPrototypeSlice(arg, index + 1); - storeOptionValue(strict, options, longOption, optionValue, result); + storeOption({ strict, options, result, longOption, optionValue }); continue; } From 948ca9e7270664cb9c558f040db5a29c743964e1 Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Sun, 10 Apr 2022 09:44:16 -0700 Subject: [PATCH 21/30] fix(WIP): Update checks to use the ERR_INVALID_ARG_VALUE class --- errors.js | 26 +++++--------------------- index.js | 28 +++++++++++++++++++--------- test/index.js | 44 ++++++++++++++++++++++++++++---------------- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/errors.js b/errors.js index db02fd0..1ea3277 100644 --- a/errors.js +++ b/errors.js @@ -7,32 +7,16 @@ class ERR_INVALID_ARG_TYPE extends TypeError { } } -class ERR_INVALID_OPTION_VALUE extends Error { - constructor(message) { - super(message); - this.code = 'ERR_INVALID_OPTION_VALUE'; - } -} - -class ERR_INVALID_SHORT_OPTION extends TypeError { - constructor(longOption, shortOption) { - super(`options.${longOption}.short must be a single character, got '${shortOption}'`); - this.code = 'ERR_INVALID_SHORT_OPTION'; - } -} - -class ERR_UNKNOWN_OPTION extends Error { - constructor(message) { - super(message); - this.code = 'ERR_UNKNOWN_OPTION'; +class ERR_INVALID_ARG_VALUE extends TypeError { + constructor(arg1, arg2, expected) { + super(`The property ${arg1} ${expected}. Received '${arg2}'`); + this.code = 'ERR_INVALID_ARG_VALUE'; } } module.exports = { codes: { ERR_INVALID_ARG_TYPE, - ERR_INVALID_OPTION_VALUE, - ERR_INVALID_SHORT_OPTION, - ERR_UNKNOWN_OPTION, + ERR_INVALID_ARG_VALUE, } }; diff --git a/index.js b/index.js index 54b32fe..b6ab304 100644 --- a/index.js +++ b/index.js @@ -34,9 +34,7 @@ const { const { codes: { - ERR_INVALID_OPTION_VALUE, - ERR_INVALID_SHORT_OPTION, - ERR_UNKNOWN_OPTION, + ERR_INVALID_ARG_VALUE, }, } = require('./errors'); @@ -88,19 +86,27 @@ function storeOption({ if (strict) { const longOrShortOption = shortOption == null ? - `long option '--${longOption}'` : - `short option '-${shortOption}'`; + `--${longOption}` : + `-${shortOption}`; if (!hasOptionConfig) { - throw new ERR_UNKNOWN_OPTION(`Unknown ${longOrShortOption}`); + throw new ERR_INVALID_ARG_VALUE(`option.${longOption}`, undefined, 'must be configured'); } if (options[longOption].type === 'string' && optionValue == null) { - throw new ERR_INVALID_OPTION_VALUE(`Missing value for ${longOrShortOption} with type:'string'`); + throw new ERR_INVALID_ARG_VALUE( + longOrShortOption, + optionValue, + 'must be provided a value', + ); } if (options[longOption].type === 'boolean' && optionValue != null) { - throw new ERR_INVALID_OPTION_VALUE(`Unexpected value '${optionValue}' for ${longOrShortOption} with type:'boolean'`); + throw new ERR_INVALID_ARG_VALUE( + longOrShortOption, + optionValue, + 'does not expect a value', + ); } } @@ -149,7 +155,11 @@ const parseArgs = ({ const shortOption = optionConfig.short; validateString(shortOption, `options.${longOption}.short`); if (shortOption.length !== 1) { - throw new ERR_INVALID_SHORT_OPTION(longOption, shortOption); + throw new ERR_INVALID_ARG_VALUE( + `options.${longOption}.short`, + shortOption, + 'must be a single character' + ); } } diff --git a/test/index.js b/test/index.js index 73f1b46..359840e 100644 --- a/test/index.js +++ b/test/index.js @@ -409,62 +409,74 @@ test('invalid union value passed to "type" option', (t) => { // Test strict mode -test('unknown long option --bar', function(t) { +test('unknown long option --bar', (t) => { const passedArgs = ['--foo', '--bar']; const passedOptions = { foo: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_VALUE' + }); t.end(); }); -test('unknown short option -b', function(t) { +test('unknown short option -b', (t) => { const passedArgs = ['--foo', '-b']; const passedOptions = { foo: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_VALUE' + }); t.end(); }); -test('unknown option -r in short option group -bar', function(t) { - const passedArgs = ['--foo', '-bar']; - const passedOptions = { foo: { type: 'boolean' }, b: { type: 'boolean' }, a: { type: 'boolean' } }; +test('unknown option -r in short option group -bar', (t) => { + const passedArgs = ['-bar']; + const passedOptions = { b: { type: 'boolean' }, a: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_VALUE' + }); t.end(); }); -test('unknown option with explicit value', function(t) { +test('unknown option with explicit value', (t) => { const passedArgs = ['--foo', '--bar=baz']; const passedOptions = { foo: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_VALUE' + }); t.end(); }); -test('string option used as boolean', function(t) { +test('string option used as boolean', (t) => { const passedArgs = ['--foo']; const passedOptions = { foo: { type: 'string' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_VALUE' + }); t.end(); }); -test('boolean option used with value', function(t) { +test('boolean option used with value', (t) => { const passedArgs = ['--foo=bar']; const passedOptions = { foo: { type: 'boolean' } }; const strict = true; - t.throws(function() { parseArgs({ strict, args: passedArgs, options: passedOptions }); }); + t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_VALUE' + }); t.end(); }); @@ -473,8 +485,8 @@ test('invalid short option length', (t) => { const passedArgs = []; const passedOptions = { foo: { short: 'fo', type: 'boolean' } }; - t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_SHORT_OPTION' + t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_ARG_VALUE' }); t.end(); From 99355dd466b3e28439c45bbdd518f7911780e2d2 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 11 Apr 2022 17:43:42 -0700 Subject: [PATCH 22/30] fix: Revert back to custom error classes and improve error messages --- errors.js | 16 ++++++++++++++++ index.js | 26 +++++++++----------------- test/index.js | 12 ++++++------ 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/errors.js b/errors.js index 1ea3277..1f53e8e 100644 --- a/errors.js +++ b/errors.js @@ -14,9 +14,25 @@ class ERR_INVALID_ARG_VALUE extends TypeError { } } +class ERR_INVALID_OPTION_VALUE extends Error { + constructor(message) { + super(message); + this.code = 'ERR_INVALID_OPTION_VALUE'; + } +} + +class ERR_UNKNOWN_OPTION extends Error { + constructor(option) { + super(`Unknown option '${option}'`); + this.code = 'ERR_UNKNOWN_OPTION'; + } +} + module.exports = { codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, + ERR_INVALID_OPTION_VALUE, + ERR_UNKNOWN_OPTION, } }; diff --git a/index.js b/index.js index 5110b6d..c18a939 100644 --- a/index.js +++ b/index.js @@ -35,6 +35,8 @@ const { const { codes: { ERR_INVALID_ARG_VALUE, + ERR_INVALID_OPTION_VALUE, + ERR_UNKNOWN_OPTION, }, } = require('./errors'); @@ -84,34 +86,24 @@ function storeOption({ }) { const hasOptionConfig = ObjectHasOwn(options, longOption); - if (strict) { - const longOrShortOption = shortOption == null ? - `--${longOption}` : - `-${shortOption}`; + const optionConfig = hasOptionConfig ? options[longOption] : {}; + if (strict) { if (!hasOptionConfig) { - throw new ERR_INVALID_ARG_VALUE(`option.${longOption}`, undefined, 'must be configured'); + throw new ERR_UNKNOWN_OPTION(shortOption == null ? `--${longOption}` : `-${shortOption}`); } + const shortOptionErr = optionConfig.short ? `-${optionConfig.short}, ` : ''; + if (options[longOption].type === 'string' && optionValue == null) { - throw new ERR_INVALID_ARG_VALUE( - longOrShortOption, - optionValue, - 'must be provided a value', - ); + throw new ERR_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption} <${longOption}>' argument missing`); } if (options[longOption].type === 'boolean' && optionValue != null) { - throw new ERR_INVALID_ARG_VALUE( - longOrShortOption, - optionValue, - 'does not expect a value', - ); + throw new ERR_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption}' does not take an argument`); } } - const optionConfig = hasOptionConfig ? options[longOption] : {}; - if (longOption === protoKey) { return; } diff --git a/test/index.js b/test/index.js index 03f4951..6ad935a 100644 --- a/test/index.js +++ b/test/index.js @@ -409,7 +409,7 @@ test('unknown long option --bar', (t) => { const strict = true; t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_ARG_VALUE' + code: 'ERR_UNKNOWN_OPTION' }); t.end(); @@ -421,7 +421,7 @@ test('unknown short option -b', (t) => { const strict = true; t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_ARG_VALUE' + code: 'ERR_UNKNOWN_OPTION' }); t.end(); @@ -433,7 +433,7 @@ test('unknown option -r in short option group -bar', (t) => { const strict = true; t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_ARG_VALUE' + code: 'ERR_UNKNOWN_OPTION' }); t.end(); @@ -445,7 +445,7 @@ test('unknown option with explicit value', (t) => { const strict = true; t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_ARG_VALUE' + code: 'ERR_UNKNOWN_OPTION' }); t.end(); @@ -457,7 +457,7 @@ test('string option used as boolean', (t) => { const strict = true; t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_ARG_VALUE' + code: 'ERR_INVALID_OPTION_VALUE' }); t.end(); @@ -469,7 +469,7 @@ test('boolean option used with value', (t) => { const strict = true; t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_ARG_VALUE' + code: 'ERR_INVALID_OPTION_VALUE' }); t.end(); From e66a8b35714b693a200fe5dc2b3fde9541c3ae42 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 11 Apr 2022 19:50:40 -0700 Subject: [PATCH 23/30] WIP: Default parseArgs to strict:true and update failing tests to strict:false --- index.js | 2 +- test/index.js | 52 ++++++++++++++++--------------------- test/prototype-pollution.js | 2 +- test/short-option-groups.js | 2 +- 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/index.js b/index.js index c18a939..25f9312 100644 --- a/index.js +++ b/index.js @@ -127,7 +127,7 @@ function storeOption({ const parseArgs = ({ args = getMainArgs(), - strict = false, + strict = true, options = {} } = {}) => { validateArray(args, 'args'); diff --git a/test/index.js b/test/index.js index 6ad935a..e5c276a 100644 --- a/test/index.js +++ b/test/index.js @@ -9,7 +9,7 @@ const { parseArgs } = require('../index.js'); test('when short option used as flag then stored as flag', (t) => { const passedArgs = ['-f']; const expected = { values: { f: true }, positionals: [] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ strict: false, args: passedArgs }); t.deepEqual(args, expected); @@ -19,7 +19,7 @@ test('when short option used as flag then stored as flag', (t) => { test('when short option used as flag before positional then stored as flag and positional (and not value)', (t) => { const passedArgs = ['-f', 'bar']; const expected = { values: { f: true }, positionals: [ 'bar' ] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ strict: false, args: passedArgs }); t.deepEqual(args, expected); @@ -63,7 +63,7 @@ test('when short option `type: "string"` used without value then stored as flag' const passedArgs = ['-f']; const passedOptions = { f: { type: 'string' } }; const expected = { values: { f: true }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -74,7 +74,7 @@ test('short option group behaves like multiple short options', (t) => { const passedArgs = ['-rf']; const passedOptions = { }; const expected = { values: { r: true, f: true }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -85,7 +85,7 @@ test('short option group does not consume subsequent positional', (t) => { const passedArgs = ['-rf', 'foo']; const passedOptions = { }; const expected = { values: { r: true, f: true }, positionals: ['foo'] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -96,7 +96,7 @@ test('if terminal of short-option group configured `type: "string"`, subsequent const passedArgs = ['-rvf', 'foo']; const passedOptions = { f: { type: 'string' } }; const expected = { values: { r: true, v: true, f: 'foo' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -106,7 +106,7 @@ test('handles short-option groups in conjunction with long-options', (t) => { const passedArgs = ['-rf', '--foo', 'foo']; const passedOptions = { foo: { type: 'string' } }; const expected = { values: { r: true, f: true, foo: 'foo' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -116,7 +116,7 @@ test('handles short-option groups with "short" alias configured', (t) => { const passedArgs = ['-rf']; const passedOptions = { remove: { short: 'r', type: 'boolean' } }; const expected = { values: { remove: true, f: true }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); t.end(); @@ -135,7 +135,7 @@ test('Everything after a bare `--` is considered a positional argument', (t) => test('args are true', (t) => { const passedArgs = ['--foo', '--bar']; const expected = { values: { foo: true, bar: true }, positionals: [] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ strict: false, args: passedArgs }); t.deepEqual(args, expected, 'args are true'); @@ -145,7 +145,7 @@ test('args are true', (t) => { test('arg is true and positional is identified', (t) => { const passedArgs = ['--foo=a', '--foo', 'b']; const expected = { values: { foo: true }, positionals: ['b'] }; - const args = parseArgs({ args: passedArgs }); + const args = parseArgs({ strict: false, args: passedArgs }); t.deepEqual(args, expected, 'arg is true and positional is identified'); @@ -177,7 +177,7 @@ test('zero config args equals are parsed as if `type: "string"`', (t) => { const passedArgs = ['--so=wat']; const passedOptions = { }; const expected = { values: { so: 'wat' }, positionals: [] }; - const args = parseArgs({ args: passedArgs, options: passedOptions }); + const args = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(args, expected, 'arg value is passed'); @@ -266,7 +266,7 @@ test('correct default args when use node -p', (t) => { process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; process.execArgv = ['-p', '0']; - const result = parseArgs(); + const result = parseArgs({ strict: false }); const expected = { values: { foo: true }, positionals: [] }; @@ -282,7 +282,7 @@ test('correct default args when use node --print', (t) => { process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; process.execArgv = ['--print', '0']; - const result = parseArgs(); + const result = parseArgs({ strict: false }); const expected = { values: { foo: true }, positionals: [] }; @@ -298,7 +298,7 @@ test('correct default args when use node -e', (t) => { process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; process.execArgv = ['-e', '0']; - const result = parseArgs(); + const result = parseArgs({ strict: false }); const expected = { values: { foo: true }, positionals: [] }; @@ -314,7 +314,7 @@ test('correct default args when use node --eval', (t) => { process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; process.execArgv = ['--eval', '0']; - const result = parseArgs(); + const result = parseArgs({ strict: false }); const expected = { values: { foo: true }, positionals: [] }; @@ -330,7 +330,7 @@ test('correct default args when normal arguments', (t) => { process.argv = [process.argv0, 'script.js', '--foo']; const holdExecArgv = process.execArgv; process.execArgv = []; - const result = parseArgs(); + const result = parseArgs({ strict: false }); const expected = { values: { foo: true }, positionals: [] }; @@ -349,7 +349,7 @@ test('excess leading dashes on options are retained', (t) => { values: { '-triple': true }, positionals: [] }; - const result = parseArgs({ args: passedArgs, options: passedOptions }); + const result = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(result, expected, 'excess option dashes are retained'); @@ -406,9 +406,8 @@ test('invalid union value passed to "type" option', (t) => { test('unknown long option --bar', (t) => { const passedArgs = ['--foo', '--bar']; const passedOptions = { foo: { type: 'boolean' } }; - const strict = true; - t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_UNKNOWN_OPTION' }); @@ -418,9 +417,8 @@ test('unknown long option --bar', (t) => { test('unknown short option -b', (t) => { const passedArgs = ['--foo', '-b']; const passedOptions = { foo: { type: 'boolean' } }; - const strict = true; - t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_UNKNOWN_OPTION' }); @@ -430,9 +428,8 @@ test('unknown short option -b', (t) => { test('unknown option -r in short option group -bar', (t) => { const passedArgs = ['-bar']; const passedOptions = { b: { type: 'boolean' }, a: { type: 'boolean' } }; - const strict = true; - t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_UNKNOWN_OPTION' }); @@ -442,9 +439,8 @@ test('unknown option -r in short option group -bar', (t) => { test('unknown option with explicit value', (t) => { const passedArgs = ['--foo', '--bar=baz']; const passedOptions = { foo: { type: 'boolean' } }; - const strict = true; - t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_UNKNOWN_OPTION' }); @@ -454,9 +450,8 @@ test('unknown option with explicit value', (t) => { test('string option used as boolean', (t) => { const passedArgs = ['--foo']; const passedOptions = { foo: { type: 'string' } }; - const strict = true; - t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_OPTION_VALUE' }); @@ -466,9 +461,8 @@ test('string option used as boolean', (t) => { test('boolean option used with value', (t) => { const passedArgs = ['--foo=bar']; const passedOptions = { foo: { type: 'boolean' } }; - const strict = true; - t.throws(() => { parseArgs({ strict, args: passedArgs, options: passedOptions }); }, { + t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_OPTION_VALUE' }); diff --git a/test/prototype-pollution.js b/test/prototype-pollution.js index 0edf48d..3f28f59 100644 --- a/test/prototype-pollution.js +++ b/test/prototype-pollution.js @@ -8,7 +8,7 @@ test('should not allow __proto__ key to be set on object', (t) => { const passedArgs = ['--__proto__=hello']; const expected = { values: {}, positionals: [] }; - const result = parseArgs({ args: passedArgs }); + const result = parseArgs({ strict: false, args: passedArgs }); t.deepEqual(result, expected); t.end(); diff --git a/test/short-option-groups.js b/test/short-option-groups.js index 8c24e1a..f95b6fa 100644 --- a/test/short-option-groups.js +++ b/test/short-option-groups.js @@ -9,7 +9,7 @@ test('when pass zero-config group of booleans then parsed as booleans', (t) => { const passedOptions = { }; const expected = { values: { r: true, f: true }, positionals: ['p'] }; - const result = parseArgs({ args: passedArgs, options: passedOptions }); + const result = parseArgs({ strict: false, args: passedArgs, options: passedOptions }); t.deepEqual(result, expected); t.end(); From a4fc92af7cea085a016af01e812b812af87c7db9 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 11 Apr 2022 20:39:58 -0700 Subject: [PATCH 24/30] Update README to reflect strict:true behavior --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 77b6396..e041870 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2) * `type` {'string'|'boolean'} (Required) Type of known option * `multiple` {boolean} (Optional) If true, when appearing one or more times in `args`, results are collected in an `Array` * `short` {string} (Optional) A single character alias for an option; When appearing one or more times in `args`; Respects the `multiple` configuration - * `strict` {Boolean} (Optional) A `Boolean` on wheather or not to throw an error when unknown args are encountered + * `strict` {Boolean} (Optional) A `Boolean` for whether or not to throw an error when unknown options are encountered, `type:'string'` options are missing an options-argument, or `type:'boolean'` options are passed an options-argument; defaults to `true` * Returns: {Object} An object having properties: * `values` {Object}, key:value for each option found. Value is a string for string options, or `true` for boolean options, or an array (of strings or booleans) for options configured as `multiple:true`. * `positionals` {string[]}, containing [Positionals][] @@ -97,24 +97,14 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2) const { parseArgs } = require('@pkgjs/parseargs'); ``` -```js -// unconfigured -const { parseArgs } = require('@pkgjs/parseargs'); -const args = ['-f', '--foo=a', '--bar', 'b']; -const options = {}; -const { values, positionals } = parseArgs({ args, options }); -// values = { f: true, foo: 'a', bar: true } -// positionals = ['b'] -``` - ```js const { parseArgs } = require('@pkgjs/parseargs'); // type:string const args = ['-f', '--foo=a', '--bar', 'b']; const options = { - bar: { - type: 'string', - }, + f: { type: 'boolean' }, + foo: { type: 'string'}, + bar: { type: 'string' }, }; const { values, positionals } = parseArgs({ args, options }); // values = { f: true, foo: 'a', bar: 'b' } @@ -126,6 +116,7 @@ const { parseArgs } = require('@pkgjs/parseargs'); // type:string & multiple const args = ['-f', '--foo=a', '--foo', 'b']; const options = { + f: { type: 'boolean' }, foo: { type: 'string', multiple: true, @@ -151,6 +142,17 @@ const { values, positionals } = parseArgs({ args, options }); // positionals = ['b'] ``` +```js +// unconfigured +const { parseArgs } = require('@pkgjs/parseargs'); +const args = ['-f', '--foo=a', '--bar', 'b']; +const options = {}; +const { values, positionals } = parseArgs({ strict: false, args, options }); +// values = { f: true, foo: 'a', bar: true } +// positionals = ['b'] +``` + + ### F.A.Qs - Is `cmd --foo=bar baz` the same as `cmd baz --foo=bar`? From 12fd0e6d58fda45d49f2d310cf4ec0090e03fcbe Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 12 Apr 2022 11:52:01 -0700 Subject: [PATCH 25/30] fix: Improve robustness of the short option error message --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 25f9312..7d1ce05 100644 --- a/index.js +++ b/index.js @@ -93,7 +93,7 @@ function storeOption({ throw new ERR_UNKNOWN_OPTION(shortOption == null ? `--${longOption}` : `-${shortOption}`); } - const shortOptionErr = optionConfig.short ? `-${optionConfig.short}, ` : ''; + const shortOptionErr = ObjectHasOwn(optionConfig, 'short') ? `-${optionConfig.short}, ` : ''; if (options[longOption].type === 'string' && optionValue == null) { throw new ERR_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption} <${longOption}>' argument missing`); From 870cffefd7d1cae28564c374b23eb21e6fb6c3c1 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 12 Apr 2022 11:53:36 -0700 Subject: [PATCH 26/30] fix: Improve error message for type:string options missing an argument --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 7d1ce05..1c73e60 100644 --- a/index.js +++ b/index.js @@ -96,7 +96,7 @@ function storeOption({ const shortOptionErr = ObjectHasOwn(optionConfig, 'short') ? `-${optionConfig.short}, ` : ''; if (options[longOption].type === 'string' && optionValue == null) { - throw new ERR_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption} <${longOption}>' argument missing`); + throw new ERR_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption} ' argument missing`); } if (options[longOption].type === 'boolean' && optionValue != null) { From 694c75d849e60f470d7c5aaa0fe929f9e64a7249 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 12 Apr 2022 11:58:03 -0700 Subject: [PATCH 27/30] fix: Namespace unique parseArgs error classes --- errors.js | 12 ++++++------ index.js | 10 +++++----- test/index.js | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/errors.js b/errors.js index 1f53e8e..689011e 100644 --- a/errors.js +++ b/errors.js @@ -14,17 +14,17 @@ class ERR_INVALID_ARG_VALUE extends TypeError { } } -class ERR_INVALID_OPTION_VALUE extends Error { +class ERR_PARSE_ARGS_INVALID_OPTION_VALUE extends Error { constructor(message) { super(message); - this.code = 'ERR_INVALID_OPTION_VALUE'; + this.code = 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE'; } } -class ERR_UNKNOWN_OPTION extends Error { +class ERR_PARSE_ARGS_UNKNOWN_OPTION extends Error { constructor(option) { super(`Unknown option '${option}'`); - this.code = 'ERR_UNKNOWN_OPTION'; + this.code = 'ERR_PARSE_ARGS_UNKNOWN_OPTION'; } } @@ -32,7 +32,7 @@ module.exports = { codes: { ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, - ERR_INVALID_OPTION_VALUE, - ERR_UNKNOWN_OPTION, + ERR_PARSE_ARGS_INVALID_OPTION_VALUE, + ERR_PARSE_ARGS_UNKNOWN_OPTION, } }; diff --git a/index.js b/index.js index 1c73e60..9cca17a 100644 --- a/index.js +++ b/index.js @@ -35,8 +35,8 @@ const { const { codes: { ERR_INVALID_ARG_VALUE, - ERR_INVALID_OPTION_VALUE, - ERR_UNKNOWN_OPTION, + ERR_PARSE_ARGS_INVALID_OPTION_VALUE, + ERR_PARSE_ARGS_UNKNOWN_OPTION, }, } = require('./errors'); @@ -90,17 +90,17 @@ function storeOption({ if (strict) { if (!hasOptionConfig) { - throw new ERR_UNKNOWN_OPTION(shortOption == null ? `--${longOption}` : `-${shortOption}`); + throw new ERR_PARSE_ARGS_UNKNOWN_OPTION(shortOption == null ? `--${longOption}` : `-${shortOption}`); } const shortOptionErr = ObjectHasOwn(optionConfig, 'short') ? `-${optionConfig.short}, ` : ''; if (options[longOption].type === 'string' && optionValue == null) { - throw new ERR_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption} ' argument missing`); + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption} ' argument missing`); } if (options[longOption].type === 'boolean' && optionValue != null) { - throw new ERR_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption}' does not take an argument`); + throw new ERR_PARSE_ARGS_INVALID_OPTION_VALUE(`Option '${shortOptionErr}--${longOption}' does not take an argument`); } } diff --git a/test/index.js b/test/index.js index e5c276a..947204c 100644 --- a/test/index.js +++ b/test/index.js @@ -408,7 +408,7 @@ test('unknown long option --bar', (t) => { const passedOptions = { foo: { type: 'boolean' } }; t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' + code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); t.end(); @@ -419,7 +419,7 @@ test('unknown short option -b', (t) => { const passedOptions = { foo: { type: 'boolean' } }; t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' + code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); t.end(); @@ -430,7 +430,7 @@ test('unknown option -r in short option group -bar', (t) => { const passedOptions = { b: { type: 'boolean' }, a: { type: 'boolean' } }; t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' + code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); t.end(); @@ -441,7 +441,7 @@ test('unknown option with explicit value', (t) => { const passedOptions = { foo: { type: 'boolean' } }; t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { - code: 'ERR_UNKNOWN_OPTION' + code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); t.end(); @@ -452,7 +452,7 @@ test('string option used as boolean', (t) => { const passedOptions = { foo: { type: 'string' } }; t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_OPTION_VALUE' + code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' }); t.end(); @@ -463,7 +463,7 @@ test('boolean option used with value', (t) => { const passedOptions = { foo: { type: 'boolean' } }; t.throws(() => { parseArgs({ args: passedArgs, options: passedOptions }); }, { - code: 'ERR_INVALID_OPTION_VALUE' + code: 'ERR_PARSE_ARGS_INVALID_OPTION_VALUE' }); t.end(); From 9b76d55779372e20cddbc67284787a4283f9a14f Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 12 Apr 2022 12:03:06 -0700 Subject: [PATCH 28/30] docs: Update README examples --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e041870..543d95c 100644 --- a/README.md +++ b/README.md @@ -99,31 +99,29 @@ const { parseArgs } = require('@pkgjs/parseargs'); ```js const { parseArgs } = require('@pkgjs/parseargs'); -// type:string -const args = ['-f', '--foo=a', '--bar', 'b']; +// specify the options that may be used const options = { - f: { type: 'boolean' }, foo: { type: 'string'}, - bar: { type: 'string' }, + bar: { type: 'boolean' }, }; +const args = ['--foo=a', '--bar']; const { values, positionals } = parseArgs({ args, options }); -// values = { f: true, foo: 'a', bar: 'b' } +// values = { foo: 'a', bar: true } // positionals = [] ``` ```js const { parseArgs } = require('@pkgjs/parseargs'); // type:string & multiple -const args = ['-f', '--foo=a', '--foo', 'b']; const options = { - f: { type: 'boolean' }, foo: { type: 'string', multiple: true, }, }; +const args = ['--foo=a', '--foo', 'b']; const { values, positionals } = parseArgs({ args, options }); -// values = { f: true, foo: [ 'a', 'b' ] } +// values = { foo: [ 'a', 'b' ] } // positionals = [] ``` From e56246c59179b3e1ca38654cde68636c05ac6c0e Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 12 Apr 2022 12:06:30 -0700 Subject: [PATCH 29/30] docs: Update remaing examples for consistency --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 543d95c..c0d31b5 100644 --- a/README.md +++ b/README.md @@ -128,23 +128,23 @@ const { values, positionals } = parseArgs({ args, options }); ```js const { parseArgs } = require('@pkgjs/parseargs'); // shorts -const args = ['-f', 'b']; const options = { foo: { short: 'f', type: 'boolean' }, }; +const args = ['-f', 'b']; const { values, positionals } = parseArgs({ args, options }); // values = { foo: true } // positionals = ['b'] ``` ```js -// unconfigured const { parseArgs } = require('@pkgjs/parseargs'); -const args = ['-f', '--foo=a', '--bar', 'b']; +// unconfigured const options = {}; +const args = ['-f', '--foo=a', '--bar', 'b']; const { values, positionals } = parseArgs({ strict: false, args, options }); // values = { f: true, foo: 'a', bar: true } // positionals = ['b'] From b4e0dafa99443a8a5cd905ff37366b361edca745 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 12 Apr 2022 22:37:40 -0700 Subject: [PATCH 30/30] Update index.js Co-authored-by: Benjamin E. Coe --- index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/index.js b/index.js index 9cca17a..9e25e05 100644 --- a/index.js +++ b/index.js @@ -85,7 +85,6 @@ function storeOption({ optionValue, }) { const hasOptionConfig = ObjectHasOwn(options, longOption); - const optionConfig = hasOptionConfig ? options[longOption] : {}; if (strict) {