From b4d0e1caf13b107a29961c556028157f0764b56c Mon Sep 17 00:00:00 2001 From: bcoe Date: Sat, 5 Feb 2022 18:44:03 -0500 Subject: [PATCH] feat(parser): support short-option groups --- README.md | 3 ++- errors.js | 8 -------- index.js | 16 ++++++++-------- test/index.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f050690..b037b15 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,8 @@ const { flags, values, positionals } = parseArgs(argv, options); - Does the API specify whether a `--` was present/relevant? - no - Is `-foo` the same as `--foo`? - - no, `-foo` is a short option or options (WIP: https://github.com/pkgjs/parseargs/issues/2) + - no, `-foo` is a short option or options, with expansion logic that follows the + [Utility Syntax Guidelines in POSIX.1-2017](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). `--foo bar`, `withValue: ['foo']`, expands to `-f`, `-o`, `-o bar`. - Is `---foo` the same as `--foo`? - no - the first flag would be parsed as `'-foo'` diff --git a/errors.js b/errors.js index e16a094..2220b66 100644 --- a/errors.js +++ b/errors.js @@ -7,16 +7,8 @@ class ERR_INVALID_ARG_TYPE extends TypeError { } } -class ERR_NOT_IMPLEMENTED extends Error { - constructor(feature) { - super(`${feature} not implemented`); - this.code = 'ERR_NOT_IMPLEMENTED'; - } -} - module.exports = { codes: { ERR_INVALID_ARG_TYPE, - ERR_NOT_IMPLEMENTED } }; diff --git a/index.js b/index.js index 84882e7..6948332 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const { ArrayPrototypeConcat, ArrayPrototypeIncludes, ArrayPrototypeSlice, + ArrayPrototypeSplice, ArrayPrototypePush, ObjectHasOwn, StringPrototypeCharAt, @@ -13,12 +14,6 @@ const { StringPrototypeStartsWith, } = require('./primordials'); -const { - codes: { - ERR_NOT_IMPLEMENTED - } -} = require('./errors'); - const { validateArray, validateObject @@ -119,9 +114,14 @@ const parseArgs = ( ); return result; } else if (StringPrototypeCharAt(arg, 1) !== '-') { - // Look for shortcodes: -fXzy + // Look for shortcodes: -fXzy and expand them to -f -X -z -y: if (arg.length > 2) { - throw new ERR_NOT_IMPLEMENTED('short option groups'); + 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}`); + } } arg = StringPrototypeCharAt(arg, 1); // short diff --git a/test/index.js b/test/index.js index 50957f2..9ce91c7 100644 --- a/test/index.js +++ b/test/index.js @@ -70,6 +70,58 @@ test('when short option withValue used without value then stored as flag', funct t.end(); }); +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); + + t.deepEqual(args, expected); + + t.end(); +}); + +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); + t.deepEqual(args, expected); + + t.end(); +}); + +// 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 expected = { flags: { r: true, f: true, v: true }, values: { r: undefined, v: undefined, f: 'foo' }, positionals: [] }; + const args = parseArgs(passedArgs, passedOptions); + t.deepEqual(args, expected); + + t.end(); +}); + +test('handles short-option groups in conjunction with long-options', function(t) { + const passedArgs = ['-rf', '--foo', 'foo']; + const passedOptions = { withValue: ['foo'] }; + const expected = { flags: { r: true, f: true, foo: true }, values: { r: undefined, f: undefined, foo: 'foo' }, positionals: [] }; + const args = parseArgs(passedArgs, passedOptions); + t.deepEqual(args, expected); + + t.end(); +}); + +test('handles short-option groups with "short" alias configured', function(t) { + const passedArgs = ['-rf']; + const passedOptions = { short: { r: 'remove' } }; + const expected = { flags: { remove: true, f: true }, values: { remove: undefined, f: undefined }, positionals: [] }; + const args = parseArgs(passedArgs, passedOptions); + t.deepEqual(args, expected); + + t.end(); +}); + test('Everything after a bare `--` is considered a positional argument', function(t) { const passedArgs = ['--', 'barepositionals', 'mopositionals']; const expected = { flags: {}, values: {}, positionals: ['barepositionals', 'mopositionals'] };