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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 e44c46c758cd8882ae7ad520323f514eac40691b Mon Sep 17 00:00:00 2001 From: Aaron Casanova Date: Wed, 23 Feb 2022 18:02:35 -0800 Subject: [PATCH 08/21] 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 09/21] 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 10/21] 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 4436ef61c1b15d1aacc21f27f3523bc7c8c1f741 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 1 Mar 2022 10:20:11 +1300 Subject: [PATCH 11/21] Rename argv propery to args (#2) --- README.md | 26 ++++++++++++------------- index.js | 18 ++++++++--------- test/index.js | 54 +++++++++++++++++++++++++-------------------------- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 420e739..5122922 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ It is exceedingly difficult to provide an API which would both be friendly to th - [🙌 Contributing](#-contributing) - [💡 `process.mainArgs` Proposal](#-processmainargs-proposal) - [Implementation:](#implementation) -- [💡 `util.parseArgs(argv)` Proposal](#-utilparseargsargv-proposal) +- [💡 `util.parseArgs([config])` Proposal](#-utilparseargsconfig-proposal) - [📃 Examples](#-examples) - [F.A.Qs](#faqs) @@ -78,12 +78,12 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2) * `config` {Object} (Optional) The `config` parameter is an object supporting the following properties: - * `argv` {string[]} (Optional) Array of argument strings; defaults + * `args` {string[]} (Optional) Array of argument strings; defaults to [`process.mainArgs`](process_argv) - * `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: + * `options` {Object} (Optional) An object describing the known options to look for in `args`; `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 + * `multiples` {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 `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,9 +101,9 @@ const { parseArgs } = require('@pkgjs/parseargs'); ```js // unconfigured const { parseArgs } = require('@pkgjs/parseargs'); -const argv = ['-f', '--foo=a', '--bar', 'b']; +const args = ['-f', '--foo=a', '--bar', 'b']; const options = {}; -const { flags, values, positionals } = parseArgs({ argv, options }); +const { flags, values, positionals } = parseArgs({ args, options }); // flags = { f: true, bar: true } // values = { foo: 'a' } // positionals = ['b'] @@ -112,13 +112,13 @@ const { flags, values, positionals } = parseArgs({ argv, options }); ```js const { parseArgs } = require('@pkgjs/parseargs'); // withValue -const argv = ['-f', '--foo=a', '--bar', 'b']; +const args = ['-f', '--foo=a', '--bar', 'b']; const options = { foo: { type: 'string', }, }; -const { flags, values, positionals } = parseArgs({ argv, options }); +const { flags, values, positionals } = parseArgs({ args, options }); // flags = { f: true } // values = { foo: 'a', bar: 'b' } // positionals = [] @@ -127,14 +127,14 @@ const { flags, values, positionals } = parseArgs({ argv, options }); ```js const { parseArgs } = require('@pkgjs/parseargs'); // withValue & multiples -const argv = ['-f', '--foo=a', '--foo', 'b']; +const args = ['-f', '--foo=a', '--foo', 'b']; const options = { foo: { type: 'string', multiples: true, }, }; -const { flags, values, positionals } = parseArgs({ argv, options }); +const { flags, values, positionals } = parseArgs({ args, options }); // flags = { f: true } // values = { foo: ['a', 'b'] } // positionals = [] @@ -143,13 +143,13 @@ const { flags, values, positionals } = parseArgs({ argv, options }); ```js const { parseArgs } = require('@pkgjs/parseargs'); // shorts -const argv = ['-f', 'b']; +const args = ['-f', 'b']; const options = { foo: { short: 'f', }, }; -const { flags, values, positionals } = parseArgs({ argv, options }); +const { flags, values, positionals } = parseArgs({ args, options }); // flags = { foo: true } // values = {} // positionals = ['b'] diff --git a/index.js b/index.js index 466b418..39a83a8 100644 --- a/index.js +++ b/index.js @@ -80,10 +80,10 @@ function storeOptionValue(options, option, value, result) { } const parseArgs = ({ - argv = getMainArgs(), + args = getMainArgs(), options = {} } = {}) => { - validateArray(argv, 'argv'); + validateArray(args, 'args'); validateObject(options, 'options'); for (const [option, optionConfig] of ObjectEntries(options)) { validateObject(optionConfig, `options.${option}`); @@ -108,8 +108,8 @@ const parseArgs = ({ }; let pos = 0; - while (pos < argv.length) { - let arg = argv[pos]; + while (pos < args.length) { + let arg = args[pos]; if (StringPrototypeStartsWith(arg, '-')) { if (arg === '-') { @@ -122,7 +122,7 @@ const parseArgs = ({ // and is returned verbatim result.positionals = ArrayPrototypeConcat( result.positionals, - ArrayPrototypeSlice(argv, ++pos) + ArrayPrototypeSlice(args, ++pos) ); return result; } else if (StringPrototypeCharAt(arg, 1) !== '-') { @@ -132,7 +132,7 @@ const parseArgs = ({ 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}`); } } @@ -159,8 +159,8 @@ 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], '-') ) { // `type: "string"` option should also support setting values when '=' // isn't used ie. both --foo=b and --foo b should work @@ -170,7 +170,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].type === 'string' ? - argv[++pos] : + args[++pos] : undefined; storeOptionValue(options, arg, val, result); } else { diff --git a/test/index.js b/test/index.js index 60cac04..912e17d 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({ argv: 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({ argv: passedArgs }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ 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 = { flags: { r: true, f: true, v: true }, values: { r: undefined, v: undefined, f: 'foo' }, positionals: [] }; - const args = parseArgs({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: 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({ argv: 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({ argv: 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({ argv: passedArgs }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: 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 `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({ argv: passedArgs }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); + const args = parseArgs({ args: 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({ argv: passedArgs1, options: passedOptions }), expected, 'option then positional'); - t.deepEqual(parseArgs({ argv: passedArgs2, options: 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(); }); @@ -339,7 +339,7 @@ test('excess leading dashes on options are retained', function(t) { values: { '-triple': undefined }, positionals: [] }; - const result = parseArgs({ argv: passedArgs, options: passedOptions }); + const result = parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); }, { + t.throws(function() { parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); }, { + t.throws(function() { parseArgs({ args: 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({ argv: passedArgs, options: passedOptions }); }, { + t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { code: 'ERR_INVALID_ARG_TYPE' }); From dc58a4ab21ab411e1984abf48440552ed42e73e1 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 13:26:12 -0800 Subject: [PATCH 12/21] feat: Rename multiples to multiple --- README.md | 8 ++++---- index.js | 6 +++--- test/index.js | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5122922..088297f 100644 --- a/README.md +++ b/README.md @@ -82,8 +82,8 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2) to [`process.mainArgs`](process_argv) * `options` {Object} (Optional) An object describing the known options to look for in `args`; `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 `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 `multiples` configuration + * `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 * Returns: {Object} An object having properties: * `flags` {Object}, having properties and `Boolean` values corresponding to parsed options passed @@ -126,12 +126,12 @@ const { flags, values, positionals } = parseArgs({ args, options }); ```js const { parseArgs } = require('@pkgjs/parseargs'); -// withValue & multiples +// withValue & multiple const args = ['-f', '--foo=a', '--foo', 'b']; const options = { foo: { type: 'string', - multiples: true, + multiple: true, }, }; const { flags, values, positionals } = parseArgs({ args, options }); diff --git a/index.js b/index.js index 39a83a8..0c5676a 100644 --- a/index.js +++ b/index.js @@ -63,7 +63,7 @@ function storeOptionValue(options, option, value, result) { result.flags[option] = true; // Values - if (optionConfig.multiples) { + if (optionConfig.multiple) { // Always store value in array, including for flags. // result.values[option] starts out not present, // first value is added as new array [newValue], @@ -96,8 +96,8 @@ const parseArgs = ({ validateString(optionConfig.short, `options.${option}.short`); } - if (ObjectHasOwn(optionConfig, 'multiples')) { - validateBoolean(optionConfig.multiples, `options.${option}.multiples`); + if (ObjectHasOwn(optionConfig, 'multiple')) { + validateBoolean(optionConfig.multiple, `options.${option}.multiple`); } } diff --git a/test/index.js b/test/index.js index 912e17d..9d3c8aa 100644 --- a/test/index.js +++ b/test/index.js @@ -206,9 +206,9 @@ test('args equals pass string including more equals', function(t) { t.end(); }); -test('first arg passed for `type: "string"` and "multiples" is in array', function(t) { +test('first arg passed for `type: "string"` and "multiple" is in array', function(t) { const passedArgs = ['--foo=a']; - const passedOptions = { foo: { type: 'string', multiples: true } }; + const passedOptions = { foo: { type: 'string', multiple: true } }; const expected = { flags: { foo: true }, values: { foo: ['a'] }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -217,12 +217,12 @@ test('first arg passed for `type: "string"` and "multiples" is in array', functi t.end(); }); -test('args are passed `type: "string"` and "multiples"', function(t) { +test('args are passed `type: "string"` and "multiple"', function(t) { const passedArgs = ['--foo=a', '--foo', 'b']; const passedOptions = { foo: { type: 'string', - multiples: true, + multiple: true, }, }; const expected = { flags: { foo: true }, values: { foo: ['a', 'b'] }, positionals: [] }; From 14866a3d4e8a7063f003df0bd2abe6698c7e17a5 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 13:27:30 -0800 Subject: [PATCH 13/21] fix: Update union error formatting --- validators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validators.js b/validators.js index 3c7dd32..a9e9aec 100644 --- a/validators.js +++ b/validators.js @@ -18,7 +18,7 @@ function validateString(value, name) { function validateUnion(value, name, union) { if (!union.includes(value)) { - throw new ERR_INVALID_ARG_TYPE(name, `[${union.join('|')}]`, value); + throw new ERR_INVALID_ARG_TYPE(name, `('${union.join('|')}')`, value); } } From b0b71c43ca10677ac9048cb9f7b1eff2c1d35987 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 13:51:20 -0800 Subject: [PATCH 14/21] feat: Guard against short options longer than 1 char --- index.js | 6 +++++- test/index.js | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 0c5676a..74b2dbe 100644 --- a/index.js +++ b/index.js @@ -93,7 +93,11 @@ const parseArgs = ({ } if (ObjectHasOwn(optionConfig, 'short')) { - validateString(optionConfig.short, `options.${option}.short`); + const short = optionConfig.short; + validateString(short, `options.${option}.short`); + if (short.length !== 1) { + throw new Error(`options.${option}.short must be a single character got "${short}"`); + } } if (ObjectHasOwn(optionConfig, 'multiple')) { diff --git a/test/index.js b/test/index.js index 9d3c8aa..a393bf8 100644 --- a/test/index.js +++ b/test/index.js @@ -380,3 +380,12 @@ test('invalid union value passed to "type" option', function(t) { t.end(); }); + +test('invalid short option length', function(t) { + const passedArgs = []; + const passedOptions = { foo: { short: 'fo' } }; + + t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }); + + t.end(); +}); From 92811f4189a39e061cfa7c7d37ce390a56afe556 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 13:52:51 -0800 Subject: [PATCH 15/21] Update validators.js Co-authored-by: John Gee --- validators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validators.js b/validators.js index a9e9aec..243fec7 100644 --- a/validators.js +++ b/validators.js @@ -17,7 +17,7 @@ function validateString(value, name) { } function validateUnion(value, name, union) { - if (!union.includes(value)) { + if (!ArrayPrototypeIncludes(union, value)) { throw new ERR_INVALID_ARG_TYPE(name, `('${union.join('|')}')`, value); } } From 0d5a35e7868e8437005c97481b0a779af07470bf Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 13:55:09 -0800 Subject: [PATCH 16/21] chore: Add missing primordial --- validators.js | 1 + 1 file changed, 1 insertion(+) diff --git a/validators.js b/validators.js index 243fec7..a082abe 100644 --- a/validators.js +++ b/validators.js @@ -2,6 +2,7 @@ const { ArrayIsArray, + ArrayPrototypeIncludes, } = require('./primordials'); const { From ecf1fb09a855ccef0f4a4eaba410edb9e90610ed Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:14:29 -0800 Subject: [PATCH 17/21] fix: Replace for..of with primordial --- index.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 74b2dbe..e773332 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,8 @@ const { ArrayPrototypeConcat, + ArrayPrototypeFind, + ArrayPrototypeForEach, ArrayPrototypeSlice, ArrayPrototypeSplice, ArrayPrototypePush, @@ -85,7 +87,7 @@ const parseArgs = ({ } = {}) => { validateArray(args, 'args'); validateObject(options, 'options'); - for (const [option, optionConfig] of ObjectEntries(options)) { + ArrayPrototypeForEach(ObjectEntries(options), ([option, optionConfig]) => { validateObject(optionConfig, `options.${option}`); if (ObjectHasOwn(optionConfig, 'type')) { @@ -103,7 +105,7 @@ const parseArgs = ({ if (ObjectHasOwn(optionConfig, 'multiple')) { validateBoolean(optionConfig.multiple, `options.${option}.multiple`); } - } + }); const result = { flags: {}, @@ -141,12 +143,14 @@ const parseArgs = ({ } arg = StringPrototypeCharAt(arg, 1); // short - for (const [option, optionConfig] of ObjectEntries(options)) { - if (optionConfig.short === arg) { - arg = option; // now long! - break; - } - } + + const [ longOption ] = ArrayPrototypeFind( + ObjectEntries(options), + ([, optionConfig]) => optionConfig.short === arg + ) || []; + + arg = longOption ?? arg; + // ToDo: later code tests for `=` in arg and wrong for shorts } else { arg = StringPrototypeSlice(arg, 2); // remove leading -- From e0d0c3356eb61154ad826cf6fd85b49596589778 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:18:24 -0800 Subject: [PATCH 18/21] fix: Update validator to use primoridial --- validators.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/validators.js b/validators.js index a082abe..9dae8e9 100644 --- a/validators.js +++ b/validators.js @@ -3,6 +3,7 @@ const { ArrayIsArray, ArrayPrototypeIncludes, + ArrayPrototypeJoin, } = require('./primordials'); const { @@ -19,7 +20,7 @@ function validateString(value, name) { function validateUnion(value, name, union) { if (!ArrayPrototypeIncludes(union, value)) { - throw new ERR_INVALID_ARG_TYPE(name, `('${union.join('|')}')`, value); + throw new ERR_INVALID_ARG_TYPE(name, `('${ArrayPrototypeJoin(union, '|')}')`, value); } } From 50d4f4d605c9e0da1f201b482961d039e40571ee Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:41:14 -0800 Subject: [PATCH 19/21] fix: Clear up ambiguity around long and short option usage --- index.js | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index e773332..a996f9c 100644 --- a/index.js +++ b/index.js @@ -58,26 +58,26 @@ function getMainArgs() { return ArrayPrototypeSlice(process.argv, 2); } -function storeOptionValue(options, option, value, result) { - const optionConfig = options[option] || {}; +function storeOptionValue(options, longOption, value, result) { + const optionConfig = options[longOption] || {}; // Flags - result.flags[option] = true; + result.flags[longOption] = true; // Values if (optionConfig.multiple) { // Always store value in array, including for flags. - // result.values[option] starts out not present, + // 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; - if (result.values[option] !== undefined) - ArrayPrototypePush(result.values[option], newValue); + if (result.values[longOption] !== undefined) + ArrayPrototypePush(result.values[longOption], newValue); else - result.values[option] = [newValue]; + result.values[longOption] = [newValue]; } else { - result.values[option] = value; + result.values[longOption] = value; } } @@ -87,25 +87,28 @@ const parseArgs = ({ } = {}) => { validateArray(args, 'args'); validateObject(options, 'options'); - ArrayPrototypeForEach(ObjectEntries(options), ([option, optionConfig]) => { - validateObject(optionConfig, `options.${option}`); + ArrayPrototypeForEach( + ObjectEntries(options), + ([longOption, optionConfig]) => { + validateObject(optionConfig, `options.${longOption}`); - if (ObjectHasOwn(optionConfig, 'type')) { - validateUnion(optionConfig.type, `options.${option}.type`, ['string', 'boolean']); - } + if (ObjectHasOwn(optionConfig, 'type')) { + validateUnion(optionConfig.type, `options.${longOption}.type`, ['string', 'boolean']); + } - if (ObjectHasOwn(optionConfig, 'short')) { - const short = optionConfig.short; - validateString(short, `options.${option}.short`); - if (short.length !== 1) { - throw new Error(`options.${option}.short must be a single character got "${short}"`); + if (ObjectHasOwn(optionConfig, 'short')) { + const shortOption = optionConfig.short; + validateString(shortOption, `options.${longOption}.short`); + if (shortOption.length !== 1) { + throw new Error(`options.${longOption}.short must be a single character got "${shortOption}"`); + } } - } - if (ObjectHasOwn(optionConfig, 'multiple')) { - validateBoolean(optionConfig.multiple, `options.${option}.multiple`); + if (ObjectHasOwn(optionConfig, 'multiple')) { + validateBoolean(optionConfig.multiple, `options.${longOption}.multiple`); + } } - }); + ); const result = { flags: {}, @@ -135,10 +138,10 @@ const parseArgs = ({ // Look for shortcodes: -fXzy and expand them to -f -X -z -y: if (arg.length > 2) { for (let i = 2; i < arg.length; i++) { - const short = StringPrototypeCharAt(arg, i); + const shortOption = 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(args, pos + (i - 1), 0, `-${shortOption}`); } } From b41fd9bee71f7e6aa6dc63560a739eec3e2ca0b0 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Mon, 28 Feb 2022 18:54:43 -0800 Subject: [PATCH 20/21] fix: Update error formatting --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index a996f9c..beff9e7 100644 --- a/index.js +++ b/index.js @@ -100,7 +100,7 @@ const parseArgs = ({ const shortOption = optionConfig.short; validateString(shortOption, `options.${longOption}.short`); if (shortOption.length !== 1) { - throw new Error(`options.${longOption}.short must be a single character got "${shortOption}"`); + throw new Error(`options.${longOption}.short must be a single character, got '${shortOption}'`); } } From f533a980bfde55edc8b93c43622d8394ba700937 Mon Sep 17 00:00:00 2001 From: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> Date: Tue, 1 Mar 2022 05:07:20 -0800 Subject: [PATCH 21/21] Update index.js Co-authored-by: Jordan Harband --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index beff9e7..cf73e7c 100644 --- a/index.js +++ b/index.js @@ -147,7 +147,7 @@ const parseArgs = ({ arg = StringPrototypeCharAt(arg, 1); // short - const [ longOption ] = ArrayPrototypeFind( + const [longOption] = ArrayPrototypeFind( ObjectEntries(options), ([, optionConfig]) => optionConfig.short === arg ) || [];