diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd64c4..fcc76ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.5.0](https://github.com/pkgjs/parseargs/compare/v0.4.0...v0.5.0) (2022-04-10) + + +### ⚠ BREAKING CHANGES + +* Require type to be specified for each supplied option (#95) + +### Features + +* Require type to be specified for each supplied option ([#95](https://github.com/pkgjs/parseargs/issues/95)) ([02cd018](https://github.com/pkgjs/parseargs/commit/02cd01885b8aaa59f2db8308f2d4479e64340068)) + ## [0.4.0](https://github.com/pkgjs/parseargs/compare/v0.3.0...v0.4.0) (2022-03-12) diff --git a/README.md b/README.md index 088297f..c5ff5da 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2) * `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 `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'`; + * `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 @@ -147,6 +147,7 @@ const args = ['-f', 'b']; const options = { foo: { short: 'f', + type: 'boolean' }, }; const { flags, values, positionals } = parseArgs({ args, options }); diff --git a/errors.js b/errors.js index 2220b66..1b9eb95 100644 --- a/errors.js +++ b/errors.js @@ -7,8 +7,16 @@ class ERR_INVALID_ARG_TYPE extends TypeError { } } +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'; + } +} + module.exports = { codes: { ERR_INVALID_ARG_TYPE, + ERR_INVALID_SHORT_OPTION } }; diff --git a/index.js b/index.js index 56da055..6fb591e 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,7 @@ const { ArrayPrototypeShift, ArrayPrototypeSlice, ArrayPrototypePush, - ObjectHasOwn, + ObjectPrototypeHasOwnProperty: ObjectHasOwn, ObjectEntries, StringPrototypeCharAt, StringPrototypeIncludes, @@ -32,6 +32,12 @@ const { isShortOptionGroup } = require('./utils'); +const { + codes: { + ERR_INVALID_SHORT_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. @@ -66,12 +72,17 @@ function getMainArgs() { return ArrayPrototypeSlice(process.argv, 2); } +const protoKey = '__proto__'; function storeOptionValue(options, longOption, value, result) { const optionConfig = options[longOption] || {}; // Flags result.flags[longOption] = true; + if (longOption === protoKey) { + return; + } + // Values if (optionConfig.multiple) { // Always store value in array, including for flags. @@ -97,18 +108,16 @@ const parseArgs = ({ validateObject(options, 'options'); ArrayPrototypeForEach( ObjectEntries(options), - ([longOption, optionConfig]) => { + ({ 0: longOption, 1: optionConfig }) => { validateObject(optionConfig, `options.${longOption}`); - if (ObjectHasOwn(optionConfig, 'type')) { - validateUnion(optionConfig.type, `options.${longOption}.type`, ['string', 'boolean']); - } + validateUnion(optionConfig.type, `options.${longOption}.type`, ['string', 'boolean']); 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}'`); + throw new ERR_INVALID_SHORT_OPTION(longOption, shortOption); } } diff --git a/package-lock.json b/package-lock.json index e37f423..2f8643e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@pkgjs/parseargs", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 52d0da9..93def3b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pkgjs/parseargs", - "version": "0.4.0", + "version": "0.5.0", "description": "Polyfill of future proposal for `util.parseArgs()`", "main": "index.js", "exports": { diff --git a/test/index.js b/test/index.js index a393bf8..93fe85c 100644 --- a/test/index.js +++ b/test/index.js @@ -6,7 +6,7 @@ const { parseArgs } = require('../index.js'); // Test results are as we expect -test('when short option used as flag then stored as flag', function(t) { +test('when short option used as flag then stored as flag', (t) => { const passedArgs = ['-f']; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [] }; const args = parseArgs({ args: passedArgs }); @@ -16,7 +16,7 @@ test('when short option used as flag then stored as flag', function(t) { t.end(); }); -test('when short option used as flag before positional then stored as flag and positional (and not value)', function(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 = { flags: { f: true }, values: { f: undefined }, positionals: [ 'bar' ] }; const args = parseArgs({ args: passedArgs }); @@ -26,7 +26,7 @@ test('when short option used as flag before positional then stored as flag and p t.end(); }); -test('when short option `type: "string"` used with value then stored as value', function(t) { +test('when short option `type: "string"` used with value then stored as value', (t) => { const passedArgs = ['-f', 'bar']; const passedOptions = { f: { type: 'string' } }; const expected = { flags: { f: true }, values: { f: 'bar' }, positionals: [] }; @@ -37,9 +37,9 @@ test('when short option `type: "string"` used with value then stored as value', t.end(); }); -test('when short option listed in short used as flag then long option stored as flag', function(t) { +test('when short option listed in short used as flag then long option stored as flag', (t) => { const passedArgs = ['-f']; - const passedOptions = { foo: { short: 'f' } }; + const passedOptions = { foo: { short: 'f', type: 'boolean' } }; const expected = { flags: { foo: true }, values: { foo: undefined }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); @@ -48,7 +48,7 @@ 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 `type: "string"` 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', (t) => { const passedArgs = ['-f', 'bar']; const passedOptions = { foo: { short: 'f', type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'bar' }, positionals: [] }; @@ -59,7 +59,7 @@ test('when short option listed in short and long listed in `type: "string"` and t.end(); }); -test('when short option `type: "string"` used without value then stored as flag', function(t) { +test('when short option `type: "string"` used without value then stored as flag', (t) => { const passedArgs = ['-f']; const passedOptions = { f: { type: 'string' } }; const expected = { flags: { f: true }, values: { f: undefined }, positionals: [] }; @@ -70,7 +70,7 @@ test('when short option `type: "string"` used without value then stored as flag' t.end(); }); -test('short option group behaves like multiple short options', function(t) { +test('short option group behaves like multiple short options', (t) => { const passedArgs = ['-rf']; const passedOptions = { }; const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: [] }; @@ -81,7 +81,7 @@ test('short option group behaves like multiple short options', function(t) { t.end(); }); -test('short option group does not consume subsequent positional', function(t) { +test('short option group does not consume subsequent positional', (t) => { const passedArgs = ['-rf', 'foo']; const passedOptions = { }; const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['foo'] }; @@ -92,7 +92,7 @@ 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 `type: "string"`, subsequent positional is stored', function(t) { +test('if terminal of short-option group configured `type: "string"`, subsequent positional is stored', (t) => { 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: [] }; @@ -102,7 +102,7 @@ test('if terminal of short-option group configured `type: "string"`, subsequent t.end(); }); -test('handles short-option groups in conjunction with long-options', function(t) { +test('handles short-option groups in conjunction with long-options', (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: [] }; @@ -112,9 +112,9 @@ test('handles short-option groups in conjunction with long-options', function(t) t.end(); }); -test('handles short-option groups with "short" alias configured', function(t) { +test('handles short-option groups with "short" alias configured', (t) => { const passedArgs = ['-rf']; - const passedOptions = { remove: { short: 'r' } }; + const passedOptions = { remove: { short: 'r', type: 'boolean' } }; const expected = { flags: { remove: true, f: true }, values: { remove: undefined, f: undefined }, positionals: [] }; const args = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(args, expected); @@ -122,7 +122,7 @@ test('handles short-option groups with "short" alias configured', function(t) { t.end(); }); -test('Everything after a bare `--` is considered a positional argument', function(t) { +test('Everything after a bare `--` is considered a positional argument', (t) => { const passedArgs = ['--', 'barepositionals', 'mopositionals']; const expected = { flags: {}, values: {}, positionals: ['barepositionals', 'mopositionals'] }; const args = parseArgs({ args: passedArgs }); @@ -132,7 +132,7 @@ test('Everything after a bare `--` is considered a positional argument', functio t.end(); }); -test('args are true', function(t) { +test('args are true', (t) => { const passedArgs = ['--foo', '--bar']; const expected = { flags: { foo: true, bar: true }, values: { foo: undefined, bar: undefined }, positionals: [] }; const args = parseArgs({ args: passedArgs }); @@ -142,7 +142,7 @@ test('args are true', function(t) { t.end(); }); -test('arg is true and positional is identified', function(t) { +test('arg is true and positional is identified', (t) => { const passedArgs = ['--foo=a', '--foo', 'b']; const expected = { flags: { foo: true }, values: { foo: undefined }, positionals: ['b'] }; const args = parseArgs({ args: passedArgs }); @@ -152,7 +152,7 @@ test('arg is true and positional is identified', function(t) { t.end(); }); -test('args equals are passed `type: "string"`', function(t) { +test('args equals are passed `type: "string"`', (t) => { const passedArgs = ['--so=wat']; const passedOptions = { so: { type: 'string' } }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; @@ -163,7 +163,7 @@ test('args equals are passed `type: "string"`', function(t) { t.end(); }); -test('when args include single dash then result stores dash as positional', function(t) { +test('when args include single dash then result stores dash as positional', (t) => { const passedArgs = ['-']; const expected = { flags: { }, values: { }, positionals: ['-'] }; const args = parseArgs({ args: passedArgs }); @@ -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 `type: "string"`', function(t) { +test('zero config args equals are parsed as if `type: "string"`', (t) => { const passedArgs = ['--so=wat']; const passedOptions = { }; const expected = { flags: { so: true }, values: { so: 'wat' }, positionals: [] }; @@ -184,7 +184,7 @@ test('zero config args equals are parsed as if `type: "string"`', function(t) { t.end(); }); -test('same arg is passed twice `type: "string"` and last value is recorded', function(t) { +test('same arg is passed twice `type: "string"` and last value is recorded', (t) => { const passedArgs = ['--foo=a', '--foo', 'b']; const passedOptions = { foo: { type: 'string' } }; const expected = { flags: { foo: true }, values: { foo: 'b' }, positionals: [] }; @@ -195,7 +195,7 @@ test('same arg is passed twice `type: "string"` and last value is recorded', fun t.end(); }); -test('args equals pass string including more equals', function(t) { +test('args equals pass string including more equals', (t) => { const passedArgs = ['--so=wat=bing']; const passedOptions = { so: { type: 'string' } }; const expected = { flags: { so: true }, values: { so: 'wat=bing' }, positionals: [] }; @@ -206,7 +206,7 @@ test('args equals pass string including more equals', function(t) { t.end(); }); -test('first arg passed for `type: "string"` and "multiple" is in array', function(t) { +test('first arg passed for `type: "string"` and "multiple" is in array', (t) => { const passedArgs = ['--foo=a']; const passedOptions = { foo: { type: 'string', multiple: true } }; const expected = { flags: { foo: true }, values: { foo: ['a'] }, positionals: [] }; @@ -217,7 +217,7 @@ test('first arg passed for `type: "string"` and "multiple" is in array', functio t.end(); }); -test('args are passed `type: "string"` and "multiple"', function(t) { +test('args are passed `type: "string"` and "multiple"', (t) => { const passedArgs = ['--foo=a', '--foo', 'b']; const passedOptions = { foo: { @@ -233,6 +233,22 @@ test('args are passed `type: "string"` and "multiple"', function(t) { t.end(); }); +test('when expecting `multiple:true` boolean option and option used multiple times then result includes array of booleans matching usage', (t) => { + const passedArgs = ['--foo', '--foo']; + const passedOptions = { + foo: { + type: 'boolean', + multiple: true, + }, + }; + const expected = { flags: { foo: true }, values: { foo: [true, true] }, positionals: [] }; + const args = parseArgs({ args: passedArgs, options: passedOptions }); + + t.deepEqual(args, expected); + + t.end(); +}); + test('order of option and positional does not matter (per README)', function(t) { const passedArgs1 = ['--foo=bar', 'baz']; const passedArgs2 = ['baz', '--foo=bar']; @@ -245,7 +261,7 @@ test('order of option and positional does not matter (per README)', function(t) t.end(); }); -test('correct default args when use node -p', function(t) { +test('correct default args when use node -p', (t) => { const holdArgv = process.argv; process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; @@ -262,7 +278,7 @@ test('correct default args when use node -p', function(t) { process.execArgv = holdExecArgv; }); -test('correct default args when use node --print', function(t) { +test('correct default args when use node --print', (t) => { const holdArgv = process.argv; process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; @@ -279,7 +295,7 @@ test('correct default args when use node --print', function(t) { process.execArgv = holdExecArgv; }); -test('correct default args when use node -e', function(t) { +test('correct default args when use node -e', (t) => { const holdArgv = process.argv; process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; @@ -296,7 +312,7 @@ test('correct default args when use node -e', function(t) { process.execArgv = holdExecArgv; }); -test('correct default args when use node --eval', function(t) { +test('correct default args when use node --eval', (t) => { const holdArgv = process.argv; process.argv = [process.argv0, '--foo']; const holdExecArgv = process.execArgv; @@ -313,7 +329,7 @@ test('correct default args when use node --eval', function(t) { process.execArgv = holdExecArgv; }); -test('correct default args when normal arguments', function(t) { +test('correct default args when normal arguments', (t) => { const holdArgv = process.argv; process.argv = [process.argv0, 'script.js', '--foo']; const holdExecArgv = process.execArgv; @@ -330,7 +346,7 @@ test('correct default args when normal arguments', function(t) { process.execArgv = holdExecArgv; }); -test('excess leading dashes on options are retained', function(t) { +test('excess leading dashes on options are retained', (t) => { // Enforce a design decision for an edge case. const passedArgs = ['---triple']; const passedOptions = { }; @@ -348,7 +364,7 @@ test('excess leading dashes on options are retained', function(t) { // Test bad inputs -test('invalid argument passed for options', function(t) { +test('invalid argument passed for options', (t) => { const passedArgs = ['--so=wat']; const passedOptions = 'bad value'; @@ -359,7 +375,17 @@ test('invalid argument passed for options', function(t) { t.end(); }); -test('boolean passed to "type" option', function(t) { +test('then type property missing for option then throw', function(t) { + const knownOptions = { foo: { } }; + + t.throws(function() { parseArgs({ options: knownOptions }); }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + + t.end(); +}); + +test('boolean passed to "type" option', (t) => { const passedArgs = ['--so=wat']; const passedOptions = { foo: { type: true } }; @@ -370,7 +396,7 @@ test('boolean passed to "type" option', function(t) { t.end(); }); -test('invalid union value passed to "type" option', function(t) { +test('invalid union value passed to "type" option', (t) => { const passedArgs = ['--so=wat']; const passedOptions = { foo: { type: 'str' } }; @@ -381,11 +407,13 @@ test('invalid union value passed to "type" option', function(t) { t.end(); }); -test('invalid short option length', function(t) { +test('invalid short option length', (t) => { const passedArgs = []; - const passedOptions = { foo: { short: 'fo' } }; + const passedOptions = { foo: { short: 'fo', type: 'boolean' } }; - t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }); + t.throws(function() { parseArgs({ args: passedArgs, options: passedOptions }); }, { + code: 'ERR_INVALID_SHORT_OPTION' + }); t.end(); }); diff --git a/test/prototype-pollution.js b/test/prototype-pollution.js new file mode 100644 index 0000000..83b47e5 --- /dev/null +++ b/test/prototype-pollution.js @@ -0,0 +1,15 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { parseArgs } = require('../index.js'); + +test('should not allow __proto__ key to be set on object', (t) => { + const passedArgs = ['--__proto__=hello']; + const expected = { flags: {}, values: {}, positionals: [] }; + + const result = parseArgs({ args: passedArgs }); + + t.deepEqual(result, expected); + t.end(); +}); diff --git a/test/short-option-combined-with-value.js b/test/short-option-combined-with-value.js index 66fb5d2..fd5dfc6 100644 --- a/test/short-option-combined-with-value.js +++ b/test/short-option-combined-with-value.js @@ -62,7 +62,7 @@ test('when combine string short with value like negative number then parsed as v test('when combine string short with value which matches configured flag then parsed as value', (t) => { const passedArgs = ['-af']; - const passedOptions = { alpha: { short: 'a', type: 'string' }, file: { short: 'f' } }; + const passedOptions = { alpha: { short: 'a', type: 'string' }, file: { short: 'f', type: 'boolean' } }; const expected = { flags: { alpha: true }, values: { alpha: 'f' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); diff --git a/test/short-option-groups.js b/test/short-option-groups.js index f849b50..bff6609 100644 --- a/test/short-option-groups.js +++ b/test/short-option-groups.js @@ -17,7 +17,7 @@ test('when pass zero-config group of booleans then parsed as booleans', (t) => { test('when pass low-config group of booleans then parsed as booleans', (t) => { const passedArgs = ['-rf', 'p']; - const passedOptions = { r: {}, f: {} }; + const passedOptions = { r: { type: 'boolean' }, f: { type: 'boolean' } }; const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); diff --git a/utils.js b/utils.js index eca8711..b921b96 100644 --- a/utils.js +++ b/utils.js @@ -9,6 +9,10 @@ const { StringPrototypeStartsWith, } = require('./primordials'); +const { + validateObject +} = require('./validators'); + // These are internal utilities to make the parsing logic easier to read, and // add lots of detail for the curious. They are in a separate file to allow // unit testing, although that is not essential (this could be rolled into @@ -20,7 +24,7 @@ const { * Determines if the argument may be used as an option value. * NB: We are choosing not to accept option-ish arguments. * @example - * isOptionValue('V']) // returns true + * isOptionValue('V') // returns true * isOptionValue('-v') // returns false * isOptionValue('--foo') // returns false * isOptionValue(undefined) // returns false @@ -54,7 +58,7 @@ function isLoneShortOption(arg) { * isLoneLongOption('a') // returns false * isLoneLongOption('-a') // returns false * isLoneLongOption('--foo) // returns true - * isLoneLongOption('--foo=bar) // returns false + * isLoneLongOption('--foo=bar') // returns false */ function isLoneLongOption(arg) { return arg.length > 2 && @@ -63,10 +67,10 @@ function isLoneLongOption(arg) { } /** - * Determines if `arg` is a long option and value in same argument. + * Determines if `arg` is a long option and value in the same argument. * @example * isLongOptionAndValue('--foo) // returns false - * isLongOptionAndValue('--foo=bar) // returns true + * isLongOptionAndValue('--foo=bar') // returns true */ function isLongOptionAndValue(arg) { return arg.length > 2 && @@ -116,7 +120,8 @@ function isShortOptionGroup(arg, options) { * }) // returns true */ function isShortOptionAndValue(arg, options) { - if (!options) throw new Error('Internal error, missing options argument'); + validateObject(options, 'options'); + if (arg.length <= 2) return false; if (StringPrototypeCharAt(arg, 0) !== '-') return false; if (StringPrototypeCharAt(arg, 1) === '-') return false; @@ -128,18 +133,18 @@ function isShortOptionAndValue(arg, options) { /** * Find the long option associated with a short option. Looks for a configured - * `short` and returns the short option itself if long option not found. + * `short` and returns the short option itself if a long option is not found. * @example * findLongOptionForShort('a', {}) // returns 'a' * findLongOptionForShort('b', { - * options: { bar: { short: 'b' }} + * options: { bar: { short: 'b' } } * }) // returns 'bar' */ function findLongOptionForShort(shortOption, options) { - if (!options) throw new Error('Internal error, missing options argument'); - const [longOption] = ArrayPrototypeFind( + validateObject(options, 'options'); + const { 0: longOption } = ArrayPrototypeFind( ObjectEntries(options), - ([, optionConfig]) => optionConfig.short === shortOption + ({ 1: optionConfig }) => optionConfig.short === shortOption ) || []; return longOption || shortOption; }