From 2734d65ba28c2106cbf90d00e7bdb10a1310f6bc Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 12:58:00 +1300 Subject: [PATCH 01/36] Support multiple unit test files --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index fbef77d..4bdd5be 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,8 @@ "description": "Polyfill of future proposal for `util.parseArgs()`", "main": "index.js", "scripts": { - "coverage": "c8 --check-coverage node test/index.js", - "test": "c8 node test/index.js", - "posttest": "eslint .", + "coverage": "c8 --check-coverage tape 'test/*.js'", + "test": "c8 tape 'test/*.js'", "posttest": "eslint .", "fix": "npm run posttest -- --fix" }, "repository": { From 96f144755aa190a8831fde06b92048433513b496 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 13:05:51 +1300 Subject: [PATCH 02/36] Add isPossibleOptionValue --- test/is-possible-option-value.js | 52 ++++++++++++++++++++++++++++++++ util.js | 35 +++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 test/is-possible-option-value.js create mode 100644 util.js diff --git a/test/is-possible-option-value.js b/test/is-possible-option-value.js new file mode 100644 index 0000000..62c4d3c --- /dev/null +++ b/test/is-possible-option-value.js @@ -0,0 +1,52 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isPossibleOptionValue } = require('../util.js'); + +test('isPossibleOptionValue: when passed plain text then returns true', function(t) { + t.true(isPossibleOptionValue('abc')); + t.end(); +}); + +test('isPossibleOptionValue: when passed digits then returns true', function(t) { + t.true(isPossibleOptionValue(123)); + t.end(); +}); + +test('isPossibleOptionValue: when passed empty string then returns true', function(t) { + t.true(isPossibleOptionValue('')); + t.end(); +}); + +// Special case, used as stdin/stdout et al and not reason to reject +test('isPossibleOptionValue: when passed dash then returns true', function(t) { + t.true(isPossibleOptionValue('-')); + t.end(); +}); + +// Supporting undefined so can pass element off end of array without checking +test('isPossibleOptionValue: when passed undefined then returns false', function(t) { + t.false(isPossibleOptionValue(undefined)); + t.end(); +}); + +test('isPossibleOptionValue: when passed short option then returns false', function(t) { + t.false(isPossibleOptionValue('-a')); + t.end(); +}); + +test('isPossibleOptionValue: when passed short option group of short option with value then returns false', function(t) { + t.false(isPossibleOptionValue('-abd')); + t.end(); +}); + +test('isPossibleOptionValue: when passed long option then returns false', function(t) { + t.false(isPossibleOptionValue('--foo')); + t.end(); +}); + +test('isPossibleOptionValue: when passed long option with value then returns false', function(t) { + t.false(isPossibleOptionValue('--foo=bar')); + t.end(); +}); diff --git a/util.js b/util.js new file mode 100644 index 0000000..bee968f --- /dev/null +++ b/util.js @@ -0,0 +1,35 @@ +'use strict'; + +const { + StringPrototypeStartsWith, +} = require('./primordials'); + +// These are internal utilities to make the parsing logic easier to read. They +// are not for client use. They are in a separate file to allow unit testing, +// although that is not essential (this could be rolled into main file +// and just tested implicitly via API). + +/** + * Determines if the argument may be used as an option value. + * NB: We are choosing not to accept option-ish arguments. + * @example + * isPossibleOptionValue('V']) // returns true + * isPossibleOptionValue('-v') // returns false + * isPossibleOptionValue('--foo') // returns false + * isPossibleOptionValue(undefined) // returns false + */ +function isPossibleOptionValue(value) { + if (value === undefined) return false; + if (value === '-') return true; // e.g. representing stdin/stdout for file + + // Open Group Utility Conventions are that an option-argument may start + // with a dash, but we are currentlly rejecting these and prioritising the + // option-like appearance of the argument. Rejection allows more error + // detection for strict:true, but comes at the cost of rejecting intended + // values starting with a dash, especially negative numbers. + return !StringPrototypeStartsWith(value, '-'); +} + +module.exports = { + isPossibleOptionValue +}; From 9dfd7b64a74078b0cc92a3d540fe13063c18a115 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 13:17:07 +1300 Subject: [PATCH 03/36] Add .editorconfig so editor knows about lint settings --- .editorconfig | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f7d665 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +tab_width = 2 +# trim_trailing_whitespace = true From 638e07c09f5b93703ec6fa609edbe2f08c9b2964 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 13:20:14 +1300 Subject: [PATCH 04/36] Add isLoneShortOption --- test/is-lone-short-option.js | 45 ++++++++++++++++++++++++++++++++++++ util.js | 12 ++++++++++ 2 files changed, 57 insertions(+) create mode 100644 test/is-lone-short-option.js diff --git a/test/is-lone-short-option.js b/test/is-lone-short-option.js new file mode 100644 index 0000000..033fbe1 --- /dev/null +++ b/test/is-lone-short-option.js @@ -0,0 +1,45 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isLoneShortOption } = require('../util.js'); + +test('isLoneShortOption: when passed short option then returns true', function(t) { + t.true(isLoneShortOption('-s')); + t.end(); +}); + +test('isLoneShortOption: when passed short option group then returns false', function(t) { + t.false(isLoneShortOption('-abc')); + t.end(); +}); + +test('isLoneShortOption: when passed long option then returns false', function(t) { + t.false(isLoneShortOption('--foo')); + t.end(); +}); + +test('isLoneShortOption: when passed long option with value then returns false', function(t) { + t.false(isLoneShortOption('--foo=bar')); + t.end(); +}); + +test('isLoneShortOption: when passed empty string then returns false', function(t) { + t.false(isLoneShortOption('')); + t.end(); +}); + +test('isLoneShortOption: when passed plain text then returns false', function(t) { + t.false(isLoneShortOption('foo')); + t.end(); +}); + +test('isLoneShortOption: when passed single dash then returns false', function(t) { + t.false(isLoneShortOption('-')); + t.end(); +}); + +test('isLoneShortOption: when passed double dash then returns false', function(t) { + t.false(isLoneShortOption('--')); + t.end(); +}); diff --git a/util.js b/util.js index bee968f..9ba3b8a 100644 --- a/util.js +++ b/util.js @@ -1,6 +1,7 @@ 'use strict'; const { + StringPrototypeCharAt, StringPrototypeStartsWith, } = require('./primordials'); @@ -30,6 +31,17 @@ function isPossibleOptionValue(value) { return !StringPrototypeStartsWith(value, '-'); } +/** + * Determines if `arg` is a just a short option. + * @example '-f' + */ +function isLoneShortOption(arg) { + return arg.length === 2 && + StringPrototypeCharAt(arg, 0) === '-' && + StringPrototypeCharAt(arg, 1) !== '-'; +} + module.exports = { + isLoneShortOption, isPossibleOptionValue }; From c8c9e0c18a913b2065773ff9115924b637ff90dd Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 13:27:06 +1300 Subject: [PATCH 05/36] Add isLongOption --- test/is-long-option.js | 51 ++++++++++++++++++++++++++++++++++++++++++ util.js | 34 +++++++++++++++++++--------- 2 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 test/is-long-option.js diff --git a/test/is-long-option.js b/test/is-long-option.js new file mode 100644 index 0000000..5838602 --- /dev/null +++ b/test/is-long-option.js @@ -0,0 +1,51 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isLongOption } = require('../util.js'); + +test('isLongOption: when passed short option then returns false', function(t) { + t.false(isLongOption('-s')); + t.end(); +}); + +test('isLongOption: when passed short option group then returns false', function(t) { + t.false(isLongOption('-abc')); + t.end(); +}); + +test('isLongOption: when passed long option then returns true', function(t) { + t.true(isLongOption('--foo')); + t.end(); +}); + +test('isLongOption: when passed long option with value then returns true', function(t) { + t.true(isLongOption('--foo=bar')); + t.end(); +}); + +test('isLongOption: when passed empty string then returns false', function(t) { + t.false(isLongOption('')); + t.end(); +}); + +test('isLongOption: when passed plain text then returns false', function(t) { + t.false(isLongOption('foo')); + t.end(); +}); + +test('isLongOption: when passed single dash then returns false', function(t) { + t.false(isLongOption('-')); + t.end(); +}); + +test('isLongOption: when passed double dash then returns false', function(t) { + t.false(isLongOption('--')); + t.end(); +}); + +// This is a bit bogus, but simple consistent behaviour: long option follows double dash. +test('isLongOption: when passed arg starting with triple dash then returns true', function(t) { + t.true(isLongOption('---foo')); + t.end(); +}); diff --git a/util.js b/util.js index 9ba3b8a..b511680 100644 --- a/util.js +++ b/util.js @@ -11,14 +11,14 @@ const { // and just tested implicitly via API). /** - * Determines if the argument may be used as an option value. - * NB: We are choosing not to accept option-ish arguments. - * @example - * isPossibleOptionValue('V']) // returns true - * isPossibleOptionValue('-v') // returns false - * isPossibleOptionValue('--foo') // returns false - * isPossibleOptionValue(undefined) // returns false - */ + * Determines if the argument may be used as an option value. + * NB: We are choosing not to accept option-ish arguments. + * @example + * isPossibleOptionValue('V']) // returns true + * isPossibleOptionValue('-v') // returns false + * isPossibleOptionValue('--foo') // returns false + * isPossibleOptionValue(undefined) // returns false + */ function isPossibleOptionValue(value) { if (value === undefined) return false; if (value === '-') return true; // e.g. representing stdin/stdout for file @@ -32,16 +32,28 @@ function isPossibleOptionValue(value) { } /** - * Determines if `arg` is a just a short option. - * @example '-f' - */ + * Determines if `arg` is a just a short option. + * @example '-f' + */ function isLoneShortOption(arg) { return arg.length === 2 && StringPrototypeCharAt(arg, 0) === '-' && StringPrototypeCharAt(arg, 1) !== '-'; } +/** + * Determines if `arg` is a long option, which may have a trailing value. + * @example + * isLongOption('-a) // returns false + * isLongOption('--foo) // returns true + * isLongOption('--foo=bar) // returns true + */ +function isLongOption(arg) { + return arg.length > 2 && StringPrototypeStartsWith(arg, '--'); +} + module.exports = { + isLongOption, isLoneShortOption, isPossibleOptionValue }; From 04d4d954d9ae905ecac2dcd3992d7579d1f1c443 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 15:23:41 +1300 Subject: [PATCH 06/36] Add separate dash tests --- test/dash.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/dash.js diff --git a/test/dash.js b/test/dash.js new file mode 100644 index 0000000..95d3162 --- /dev/null +++ b/test/dash.js @@ -0,0 +1,31 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { parseArgs } = require('../index.js'); + +// The use of `-` as a positional is specifically mentioned in the Open Group Utility Conventions. +// The interpretation is up to the utility, and for a file positional (operand) the examples are +// '-' may stand for standard input (or standard output), or for a file named -. +// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html +test("dash: when args include '-' used as positional then result has '-' in positionals", (t) => { + const passedArgs = ['-']; + + const args = parseArgs(passedArgs); + const expected = { flags: {}, values: {}, positionals: ['-'] }; + t.deepEqual(args, expected); + + t.end(); +}); + +// If '-' is a valid positional, it is symmetrical to allow it as an option value too. +test("dash: when args include '-' used as space-separated option value then result has '-' in option value", (t) => { + const passedArgs = ['-v', '-']; + const options = { withValue: ['v'] }; + + const args = parseArgs(passedArgs, options); + const expected = { flags: { v: true }, values: { v: '-' }, positionals: [] }; + t.deepEqual(args, expected); + + t.end(); +}); From 512afe5fa0d66e77de5cf9e7cebb217d00db1788 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 15:24:52 +1300 Subject: [PATCH 07/36] Update signature for running new tests to arrow functions --- test/is-lone-short-option.js | 16 ++++++++-------- test/is-long-option.js | 18 +++++++++--------- test/is-possible-option-value.js | 18 +++++++++--------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/test/is-lone-short-option.js b/test/is-lone-short-option.js index 033fbe1..cda137f 100644 --- a/test/is-lone-short-option.js +++ b/test/is-lone-short-option.js @@ -4,42 +4,42 @@ const test = require('tape'); const { isLoneShortOption } = require('../util.js'); -test('isLoneShortOption: when passed short option then returns true', function(t) { +test('isLoneShortOption: when passed short option then returns true', (t) => { t.true(isLoneShortOption('-s')); t.end(); }); -test('isLoneShortOption: when passed short option group then returns false', function(t) { +test('isLoneShortOption: when passed short option group then returns false', (t) => { t.false(isLoneShortOption('-abc')); t.end(); }); -test('isLoneShortOption: when passed long option then returns false', function(t) { +test('isLoneShortOption: when passed long option then returns false', (t) => { t.false(isLoneShortOption('--foo')); t.end(); }); -test('isLoneShortOption: when passed long option with value then returns false', function(t) { +test('isLoneShortOption: when passed long option with value then returns false', (t) => { t.false(isLoneShortOption('--foo=bar')); t.end(); }); -test('isLoneShortOption: when passed empty string then returns false', function(t) { +test('isLoneShortOption: when passed empty string then returns false', (t) => { t.false(isLoneShortOption('')); t.end(); }); -test('isLoneShortOption: when passed plain text then returns false', function(t) { +test('isLoneShortOption: when passed plain text then returns false', (t) => { t.false(isLoneShortOption('foo')); t.end(); }); -test('isLoneShortOption: when passed single dash then returns false', function(t) { +test('isLoneShortOption: when passed single dash then returns false', (t) => { t.false(isLoneShortOption('-')); t.end(); }); -test('isLoneShortOption: when passed double dash then returns false', function(t) { +test('isLoneShortOption: when passed double dash then returns false', (t) => { t.false(isLoneShortOption('--')); t.end(); }); diff --git a/test/is-long-option.js b/test/is-long-option.js index 5838602..888d73d 100644 --- a/test/is-long-option.js +++ b/test/is-long-option.js @@ -4,48 +4,48 @@ const test = require('tape'); const { isLongOption } = require('../util.js'); -test('isLongOption: when passed short option then returns false', function(t) { +test('isLongOption: when passed short option then returns false', (t) => { t.false(isLongOption('-s')); t.end(); }); -test('isLongOption: when passed short option group then returns false', function(t) { +test('isLongOption: when passed short option group then returns false', (t) => { t.false(isLongOption('-abc')); t.end(); }); -test('isLongOption: when passed long option then returns true', function(t) { +test('isLongOption: when passed long option then returns true', (t) => { t.true(isLongOption('--foo')); t.end(); }); -test('isLongOption: when passed long option with value then returns true', function(t) { +test('isLongOption: when passed long option with value then returns true', (t) => { t.true(isLongOption('--foo=bar')); t.end(); }); -test('isLongOption: when passed empty string then returns false', function(t) { +test('isLongOption: when passed empty string then returns false', (t) => { t.false(isLongOption('')); t.end(); }); -test('isLongOption: when passed plain text then returns false', function(t) { +test('isLongOption: when passed plain text then returns false', (t) => { t.false(isLongOption('foo')); t.end(); }); -test('isLongOption: when passed single dash then returns false', function(t) { +test('isLongOption: when passed single dash then returns false', (t) => { t.false(isLongOption('-')); t.end(); }); -test('isLongOption: when passed double dash then returns false', function(t) { +test('isLongOption: when passed double dash then returns false', (t) => { t.false(isLongOption('--')); t.end(); }); // This is a bit bogus, but simple consistent behaviour: long option follows double dash. -test('isLongOption: when passed arg starting with triple dash then returns true', function(t) { +test('isLongOption: when passed arg starting with triple dash then returns true', (t) => { t.true(isLongOption('---foo')); t.end(); }); diff --git a/test/is-possible-option-value.js b/test/is-possible-option-value.js index 62c4d3c..0ca0ebf 100644 --- a/test/is-possible-option-value.js +++ b/test/is-possible-option-value.js @@ -4,49 +4,49 @@ const test = require('tape'); const { isPossibleOptionValue } = require('../util.js'); -test('isPossibleOptionValue: when passed plain text then returns true', function(t) { +test('isPossibleOptionValue: when passed plain text then returns true', (t) => { t.true(isPossibleOptionValue('abc')); t.end(); }); -test('isPossibleOptionValue: when passed digits then returns true', function(t) { +test('isPossibleOptionValue: when passed digits then returns true', (t) => { t.true(isPossibleOptionValue(123)); t.end(); }); -test('isPossibleOptionValue: when passed empty string then returns true', function(t) { +test('isPossibleOptionValue: when passed empty string then returns true', (t) => { t.true(isPossibleOptionValue('')); t.end(); }); // Special case, used as stdin/stdout et al and not reason to reject -test('isPossibleOptionValue: when passed dash then returns true', function(t) { +test('isPossibleOptionValue: when passed dash then returns true', (t) => { t.true(isPossibleOptionValue('-')); t.end(); }); // Supporting undefined so can pass element off end of array without checking -test('isPossibleOptionValue: when passed undefined then returns false', function(t) { +test('isPossibleOptionValue: when passed undefined then returns false', (t) => { t.false(isPossibleOptionValue(undefined)); t.end(); }); -test('isPossibleOptionValue: when passed short option then returns false', function(t) { +test('isPossibleOptionValue: when passed short option then returns false', (t) => { t.false(isPossibleOptionValue('-a')); t.end(); }); -test('isPossibleOptionValue: when passed short option group of short option with value then returns false', function(t) { +test('isPossibleOptionValue: when passed short option group of short option with value then returns false', (t) => { t.false(isPossibleOptionValue('-abd')); t.end(); }); -test('isPossibleOptionValue: when passed long option then returns false', function(t) { +test('isPossibleOptionValue: when passed long option then returns false', (t) => { t.false(isPossibleOptionValue('--foo')); t.end(); }); -test('isPossibleOptionValue: when passed long option with value then returns false', function(t) { +test('isPossibleOptionValue: when passed long option with value then returns false', (t) => { t.false(isPossibleOptionValue('--foo=bar')); t.end(); }); From 83f0e02b2ee1f44878348e1f94ec9b83804cb1de Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 2 Mar 2022 17:29:19 +1300 Subject: [PATCH 08/36] isShortOptionGroup --- test/is-short-option-group.js | 71 +++++++++++++++++++++++++++++++++ util.js | 74 ++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 test/is-short-option-group.js diff --git a/test/is-short-option-group.js b/test/is-short-option-group.js new file mode 100644 index 0000000..e575ae5 --- /dev/null +++ b/test/is-short-option-group.js @@ -0,0 +1,71 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isShortOptionGroup } = require('../util.js'); + +test('isShortOptionGroup: when passed lone short option then returns false', (t) => { + t.false(isShortOptionGroup('-s', {})); + t.end(); +}); + +test('isShortOptionGroup: when passed group with leading zero-config boolean then returns true', (t) => { + t.true(isShortOptionGroup('-ab', {})); + t.end(); +}); + +test('isShortOptionGroup: when passed group with leading configured implicit boolean then returns true', (t) => { + t.true(isShortOptionGroup('-ab', { aaa: { short: 'a' } })); + t.end(); +}); + +test('isShortOptionGroup: when passed group with leading configured explicit boolean then returns true', (t) => { + t.true(isShortOptionGroup('-ab', { aaa: { short: 'a', type: 'boolean' } })); + t.end(); +}); + +test('isShortOptionGroup: when passed group with leading configured string then returns false', (t) => { + t.false(isShortOptionGroup('-ab', { aaa: { short: 'a', type: 'string' } })); + t.end(); +}); + +test('isShortOptionGroup: when passed group with trailing configured string then returns true', (t) => { + t.true(isShortOptionGroup('-ab', { bbb: { short: 'b', type: 'string' } })); + t.end(); +}); + +// This one is dubious, but leave it to caller to handle. +test('isShortOptionGroup: when passed group with middle configured string then returns true', (t) => { + t.true(isShortOptionGroup('-abc', { bbb: { short: 'b', type: 'string' } })); + t.end(); +}); + +test('isShortOptionGroup: when passed long option then returns false', (t) => { + t.false(isShortOptionGroup('--foo', {})); + t.end(); +}); + +test('isShortOptionGroup: when passed long option with value then returns false', (t) => { + t.false(isShortOptionGroup('--foo=bar', {})); + t.end(); +}); + +test('isShortOptionGroup: when passed empty string then returns false', (t) => { + t.false(isShortOptionGroup('', {})); + t.end(); +}); + +test('isShortOptionGroup: when passed plain text then returns false', (t) => { + t.false(isShortOptionGroup('foo', {})); + t.end(); +}); + +test('isShortOptionGroup: when passed single dash then returns false', (t) => { + t.false(isShortOptionGroup('-', {})); + t.end(); +}); + +test('isShortOptionGroup: when passed double dash then returns false', (t) => { + t.false(isShortOptionGroup('--', {})); + t.end(); +}); diff --git a/util.js b/util.js index b511680..92ee4ee 100644 --- a/util.js +++ b/util.js @@ -1,6 +1,9 @@ 'use strict'; const { + ArrayPrototypeFind, + ObjectAssign, + ObjectValues, StringPrototypeCharAt, StringPrototypeStartsWith, } = require('./primordials'); @@ -52,8 +55,77 @@ function isLongOption(arg) { return arg.length > 2 && StringPrototypeStartsWith(arg, '--'); } +function getDefaultOptionConfig() { + return { + short: undefined, + type: 'boolean', + multiple: false + }; +} + +/** + * Lookup option config. Returns undefined if no match. + */ +function findOptionConfigFromShort(shortOption, options) { + const foundConfig = ArrayPrototypeFind( + ObjectValues(options), + (optionConfig) => optionConfig.short === shortOption + ); + return foundConfig; +} + +/** + * Populate an option config using options and defaults. + */ +function getOptionConfigFromShort(shortOption, options) { + const optionConfig = findOptionConfigFromShort(shortOption, options) || {}; + return ObjectAssign(getDefaultOptionConfig(), optionConfig); +} + +/** + * Return whether a short option is of boolean type, implicitly or explicitly. + */ +function isShortOfTypeBoolean(shortOption, options) { + if (!options) throw new Error('Internal error, missing options argument'); + + const optionConfig = getOptionConfigFromShort(shortOption, options); + return optionConfig.type === 'boolean'; +} + +/** + * Determines if `arg` is a short option group. + * + * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). + * One or more options without option-arguments, followed by at most one + * option that takes an option-argument, should be accepted when grouped + * behind one '-' delimiter. + * @example + * isShortOptionGroup('-a', {}) // returns false + * isShortOptionGroup('-ab', {}) // returns true + * // -fb is an option and a value, not a short option group + * isShortOptionGroup('-fb', { + * options: { f: { type: 'string' }} + * }) // returns false + * isShortOptionGroup('-bf', { + * options: { f: { type: 'string' }} + * }) // returns true + * // -bfb is an edge case, return true and caller sorts it out + * isShortOptionGroup('-bfb', { + * options: { f: { type: 'string' }} + * }) // returns true + */ +function isShortOptionGroup(arg, options) { + if (arg.length <= 2) return false; + if (StringPrototypeCharAt(arg, 0) !== '-') return false; + if (StringPrototypeCharAt(arg, 1) === '-') return false; + + const firstShort = StringPrototypeCharAt(arg, 1); + return isShortOfTypeBoolean(firstShort, options); +} + module.exports = { isLongOption, isLoneShortOption, - isPossibleOptionValue + isPossibleOptionValue, + isShortOptionGroup }; From bc45095167549eab1390cfe3f509a372932e39cd Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 3 Mar 2022 15:12:50 +1300 Subject: [PATCH 09/36] Update to new calling signature --- test/dash.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/dash.js b/test/dash.js index 95d3162..f21d336 100644 --- a/test/dash.js +++ b/test/dash.js @@ -11,9 +11,9 @@ const { parseArgs } = require('../index.js'); test("dash: when args include '-' used as positional then result has '-' in positionals", (t) => { const passedArgs = ['-']; - const args = parseArgs(passedArgs); + const result = parseArgs({ args: passedArgs }); const expected = { flags: {}, values: {}, positionals: ['-'] }; - t.deepEqual(args, expected); + t.deepEqual(result, expected); t.end(); }); @@ -21,11 +21,11 @@ test("dash: when args include '-' used as positional then result has '-' in posi // If '-' is a valid positional, it is symmetrical to allow it as an option value too. test("dash: when args include '-' used as space-separated option value then result has '-' in option value", (t) => { const passedArgs = ['-v', '-']; - const options = { withValue: ['v'] }; + const options = { v: { type: 'string' } }; - const args = parseArgs(passedArgs, options); + const result = parseArgs({ args: passedArgs, options }); const expected = { flags: { v: true }, values: { v: '-' }, positionals: [] }; - t.deepEqual(args, expected); + t.deepEqual(result, expected); t.end(); }); From 0a9c04c23883eed5a85d5daac5c341fea2e33a8c Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 3 Mar 2022 15:13:48 +1300 Subject: [PATCH 10/36] Add findLongOptionForShort --- test/find-long-option-for-short.js | 20 ++++++++++++++++++++ test/is-lone-short-option.js | 2 +- test/is-long-option.js | 2 +- test/is-possible-option-value.js | 2 +- test/is-short-option-group.js | 2 +- util.js => utils.js | 20 ++++++++++++++++++++ 6 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 test/find-long-option-for-short.js rename util.js => utils.js (86%) diff --git a/test/find-long-option-for-short.js b/test/find-long-option-for-short.js new file mode 100644 index 0000000..97b4603 --- /dev/null +++ b/test/find-long-option-for-short.js @@ -0,0 +1,20 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { findLongOptionForShort } = require('../utils.js'); + +test('findLongOptionForShort: when passed empty options then returns short', (t) => { + t.equal(findLongOptionForShort('a', {}), 'a'); + t.end(); +}); + +test('findLongOptionForShort: when passed short not present in options then returns short', (t) => { + t.equal(findLongOptionForShort('a', { foo: { short: 'f', type: 'string' } }), 'a'); + t.end(); +}); + +test('findLongOptionForShort: when passed short present in options then returns long', (t) => { + t.equal(findLongOptionForShort('a', { alpha: { short: 'a' } }), 'alpha'); + t.end(); +}); diff --git a/test/is-lone-short-option.js b/test/is-lone-short-option.js index cda137f..aa97de0 100644 --- a/test/is-lone-short-option.js +++ b/test/is-lone-short-option.js @@ -2,7 +2,7 @@ /* eslint max-len: 0 */ const test = require('tape'); -const { isLoneShortOption } = require('../util.js'); +const { isLoneShortOption } = require('../utils.js'); test('isLoneShortOption: when passed short option then returns true', (t) => { t.true(isLoneShortOption('-s')); diff --git a/test/is-long-option.js b/test/is-long-option.js index 888d73d..3062799 100644 --- a/test/is-long-option.js +++ b/test/is-long-option.js @@ -2,7 +2,7 @@ /* eslint max-len: 0 */ const test = require('tape'); -const { isLongOption } = require('../util.js'); +const { isLongOption } = require('../utils.js'); test('isLongOption: when passed short option then returns false', (t) => { t.false(isLongOption('-s')); diff --git a/test/is-possible-option-value.js b/test/is-possible-option-value.js index 0ca0ebf..651ba98 100644 --- a/test/is-possible-option-value.js +++ b/test/is-possible-option-value.js @@ -2,7 +2,7 @@ /* eslint max-len: 0 */ const test = require('tape'); -const { isPossibleOptionValue } = require('../util.js'); +const { isPossibleOptionValue } = require('../utils.js'); test('isPossibleOptionValue: when passed plain text then returns true', (t) => { t.true(isPossibleOptionValue('abc')); diff --git a/test/is-short-option-group.js b/test/is-short-option-group.js index e575ae5..56a5e00 100644 --- a/test/is-short-option-group.js +++ b/test/is-short-option-group.js @@ -2,7 +2,7 @@ /* eslint max-len: 0 */ const test = require('tape'); -const { isShortOptionGroup } = require('../util.js'); +const { isShortOptionGroup } = require('../utils.js'); test('isShortOptionGroup: when passed lone short option then returns false', (t) => { t.false(isShortOptionGroup('-s', {})); diff --git a/util.js b/utils.js similarity index 86% rename from util.js rename to utils.js index 92ee4ee..ec332d6 100644 --- a/util.js +++ b/utils.js @@ -3,6 +3,7 @@ const { ArrayPrototypeFind, ObjectAssign, + ObjectEntries, ObjectValues, StringPrototypeCharAt, StringPrototypeStartsWith, @@ -123,7 +124,26 @@ function isShortOptionGroup(arg, options) { return isShortOfTypeBoolean(firstShort, options); } +/** + * Find the key to use for a short option. Looks for a configured + * `short` and returns the short option itself it not found. + * @example + * findOptionsKeyForShort('a', {}) // returns 'a' + * findOptionsKeyForShort('b', { + * options: { bar: { short: 'b' }} + * }) // returns 'bar' + */ +function findLongOptionForShort(shortOption, options) { + if (!options) throw new Error('Internal error, missing options argument'); + const [longOption] = ArrayPrototypeFind( + ObjectEntries(options), + ([, optionConfig]) => optionConfig.short === shortOption + ) || []; + return longOption || shortOption; +} + module.exports = { + findLongOptionForShort, isLongOption, isLoneShortOption, isPossibleOptionValue, From 042d9575a9a5a2d1ec5fef91969894f6fc7f127b Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 3 Mar 2022 18:47:45 +1300 Subject: [PATCH 11/36] Start updating main parsing loop, and rework some utils. - remove now unused experimental code - split isLongOption - rename routines and files --- index.js | 131 ++++++++++++++----------------- test/is-lone-long-option.js | 62 +++++++++++++++ test/is-long-option-and-value.js | 62 +++++++++++++++ test/is-long-option.js | 51 ------------ test/is-option-value.js | 52 ++++++++++++ test/is-possible-option-value.js | 52 ------------ utils.js | 117 +++++++++++---------------- 7 files changed, 281 insertions(+), 246 deletions(-) create mode 100644 test/is-lone-long-option.js create mode 100644 test/is-long-option-and-value.js delete mode 100644 test/is-long-option.js create mode 100644 test/is-option-value.js delete mode 100644 test/is-possible-option-value.js diff --git a/index.js b/index.js index cf73e7c..7868abe 100644 --- a/index.js +++ b/index.js @@ -2,10 +2,8 @@ const { ArrayPrototypeConcat, - ArrayPrototypeFind, ArrayPrototypeForEach, ArrayPrototypeSlice, - ArrayPrototypeSplice, ArrayPrototypePush, ObjectHasOwn, ObjectEntries, @@ -13,7 +11,6 @@ const { StringPrototypeIncludes, StringPrototypeIndexOf, StringPrototypeSlice, - StringPrototypeStartsWith, } = require('./primordials'); const { @@ -24,6 +21,14 @@ const { validateBoolean, } = require('./validators'); +const { + findLongOptionForShort, + isLoneLongOption, + isLoneShortOption, + isLongOptionAndValue, + isOptionValue +} = require('./utils'); + function getMainArgs() { // This function is a placeholder for proposed process.mainArgs. // Work out where to slice process.argv for user supplied arguments. @@ -118,83 +123,61 @@ const parseArgs = ({ let pos = 0; while (pos < args.length) { - let arg = args[pos]; - - if (StringPrototypeStartsWith(arg, '-')) { - if (arg === '-') { - // '-' commonly used to represent stdin/stdout, treat as positional - result.positionals = ArrayPrototypeConcat(result.positionals, '-'); - ++pos; - continue; - } else if (arg === '--') { - // Everything after a bare '--' is considered a positional argument - // and is returned verbatim - result.positionals = ArrayPrototypeConcat( - result.positionals, - ArrayPrototypeSlice(args, ++pos) - ); - return result; - } else if (StringPrototypeCharAt(arg, 1) !== '-') { - // 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 shortOption = StringPrototypeCharAt(arg, i); - // Add 'i' to 'pos' such that short options are parsed in order - // of definition: - ArrayPrototypeSplice(args, pos + (i - 1), 0, `-${shortOption}`); - } - } - - arg = StringPrototypeCharAt(arg, 1); // short - - const [longOption] = ArrayPrototypeFind( - ObjectEntries(options), - ([, optionConfig]) => optionConfig.short === arg - ) || []; - - arg = longOption ?? arg; + const arg = args[pos]; + const nextArg = args[pos + 1]; + + // Check if `arg` is an options terminator. + // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html + if (arg === '--') { + // Everything after a bare '--' is considered a positional argument. + result.positionals = ArrayPrototypeConcat( + result.positionals, + ArrayPrototypeSlice(args, pos + 1) + ); + break; // Finished processing argv, leave while loop. + } - // ToDo: later code tests for `=` in arg and wrong for shorts - } else { - arg = StringPrototypeSlice(arg, 2); // remove leading -- + if (isLoneShortOption(arg)) { + // e.g. '-f' + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + let optionValue; + if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) { + // e.g. '-f', 'bar' + optionValue = nextArg; + pos++; } + storeOptionValue(options, longOption, optionValue, result); + pos++; + continue; + } - if (StringPrototypeIncludes(arg, '=')) { - // 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 - const index = StringPrototypeIndexOf(arg, '='); - storeOptionValue( - options, - StringPrototypeSlice(arg, 0, index), - StringPrototypeSlice(arg, index + 1), - result); - } 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 - - // 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].type === 'string' ? - args[++pos] : - undefined; - storeOptionValue(options, arg, val, result); - } else { - // Cases when an arg is specified without a value, example - // '--foo --bar' <- 'foo' and 'bar' flags should be set to true and - // save value as undefined - storeOptionValue(options, arg, undefined, result); + if (isLoneLongOption(arg)) { + // e.g. '--foo' + const longOption = StringPrototypeSlice(arg, 2); + let optionValue; + if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) { + // e.g. '-foo', 'bar' + optionValue = nextArg; + pos++; } - } else { - // Arguments without a dash prefix are considered "positional" - ArrayPrototypePush(result.positionals, arg); + storeOptionValue(options, longOption, optionValue, result); + pos++; + continue; + } + + if (isLongOptionAndValue(arg)) { + // e.g. --foo=bar + const index = StringPrototypeIndexOf(arg, '='); + const longOption = StringPrototypeSlice(arg, 2, index); + const optionValue = StringPrototypeSlice(arg, index + 1); + storeOptionValue(options, longOption, optionValue, result); + pos++; + continue; } + // Anything left is a positional + ArrayPrototypePush(result.positionals, arg); pos++; } diff --git a/test/is-lone-long-option.js b/test/is-lone-long-option.js new file mode 100644 index 0000000..deb95e8 --- /dev/null +++ b/test/is-lone-long-option.js @@ -0,0 +1,62 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isLoneLongOption } = require('../utils.js'); + +test('isLoneLongOption: when passed short option then returns false', (t) => { + t.false(isLoneLongOption('-s')); + t.end(); +}); + +test('isLoneLongOption: when passed short option group then returns false', (t) => { + t.false(isLoneLongOption('-abc')); + t.end(); +}); + +test('isLoneLongOption: when passed lone long option then returns true', (t) => { + t.true(isLoneLongOption('--foo')); + t.end(); +}); + +test('isLoneLongOption: when passed single character long option then returns true', (t) => { + t.true(isLoneLongOption('--f')); + t.end(); +}); + +test('isLoneLongOption: when passed long option and value then returns false', (t) => { + t.false(isLoneLongOption('--foo=bar')); + t.end(); +}); + +test('isLoneLongOption: when passed empty string then returns false', (t) => { + t.false(isLoneLongOption('')); + t.end(); +}); + +test('isLoneLongOption: when passed plain text then returns false', (t) => { + t.false(isLoneLongOption('foo')); + t.end(); +}); + +test('isLoneLongOption: when passed single dash then returns false', (t) => { + t.false(isLoneLongOption('-')); + t.end(); +}); + +test('isLoneLongOption: when passed double dash then returns false', (t) => { + t.false(isLoneLongOption('--')); + t.end(); +}); + +// This is a bit bogus, but simple consistent behaviour: long option follows double dash. +test('isLoneLongOption: when passed arg starting with triple dash then returns true', (t) => { + t.true(isLoneLongOption('---foo')); + t.end(); +}); + +// This is a bit bogus, but simple consistent behaviour: long option follows double dash. +test("isLoneLongOption: when passed '--=' then returns true", (t) => { + t.true(isLoneLongOption('--=')); + t.end(); +}); diff --git a/test/is-long-option-and-value.js b/test/is-long-option-and-value.js new file mode 100644 index 0000000..a4c9e1d --- /dev/null +++ b/test/is-long-option-and-value.js @@ -0,0 +1,62 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isLongOptionAndValue } = require('../utils.js'); + +test('isLongOptionAndValue: when passed short option then returns false', (t) => { + t.false(isLongOptionAndValue('-s')); + t.end(); +}); + +test('isLongOptionAndValue: when passed short option group then returns false', (t) => { + t.false(isLongOptionAndValue('-abc')); + t.end(); +}); + +test('isLongOptionAndValue: when passed lone long option then returns false', (t) => { + t.false(isLongOptionAndValue('--foo')); + t.end(); +}); + +test('isLongOptionAndValue: when passed long option and value then returns true', (t) => { + t.true(isLongOptionAndValue('--foo=bar')); + t.end(); +}); + +test('isLongOptionAndValue: when passed single character long option and value then returns true', (t) => { + t.true(isLongOptionAndValue('--f=bar')); + t.end(); +}); + +test('isLongOptionAndValue: when passed empty string then returns false', (t) => { + t.false(isLongOptionAndValue('')); + t.end(); +}); + +test('isLongOptionAndValue: when passed plain text then returns false', (t) => { + t.false(isLongOptionAndValue('foo')); + t.end(); +}); + +test('isLongOptionAndValue: when passed single dash then returns false', (t) => { + t.false(isLongOptionAndValue('-')); + t.end(); +}); + +test('isLongOptionAndValue: when passed double dash then returns false', (t) => { + t.false(isLongOptionAndValue('--')); + t.end(); +}); + +// This is a bit bogus, but simple consistent behaviour: long option follows double dash. +test('isLongOptionAndValue: when passed arg starting with triple dash and value then returns true', (t) => { + t.true(isLongOptionAndValue('---foo=bar')); + t.end(); +}); + +// This is a bit bogus, but simple consistent behaviour: long option follows double dash. +test("isLongOptionAndValue: when passed '--=' then returns false", (t) => { + t.false(isLongOptionAndValue('--=')); + t.end(); +}); diff --git a/test/is-long-option.js b/test/is-long-option.js deleted file mode 100644 index 3062799..0000000 --- a/test/is-long-option.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; -/* eslint max-len: 0 */ - -const test = require('tape'); -const { isLongOption } = require('../utils.js'); - -test('isLongOption: when passed short option then returns false', (t) => { - t.false(isLongOption('-s')); - t.end(); -}); - -test('isLongOption: when passed short option group then returns false', (t) => { - t.false(isLongOption('-abc')); - t.end(); -}); - -test('isLongOption: when passed long option then returns true', (t) => { - t.true(isLongOption('--foo')); - t.end(); -}); - -test('isLongOption: when passed long option with value then returns true', (t) => { - t.true(isLongOption('--foo=bar')); - t.end(); -}); - -test('isLongOption: when passed empty string then returns false', (t) => { - t.false(isLongOption('')); - t.end(); -}); - -test('isLongOption: when passed plain text then returns false', (t) => { - t.false(isLongOption('foo')); - t.end(); -}); - -test('isLongOption: when passed single dash then returns false', (t) => { - t.false(isLongOption('-')); - t.end(); -}); - -test('isLongOption: when passed double dash then returns false', (t) => { - t.false(isLongOption('--')); - t.end(); -}); - -// This is a bit bogus, but simple consistent behaviour: long option follows double dash. -test('isLongOption: when passed arg starting with triple dash then returns true', (t) => { - t.true(isLongOption('---foo')); - t.end(); -}); diff --git a/test/is-option-value.js b/test/is-option-value.js new file mode 100644 index 0000000..199bf30 --- /dev/null +++ b/test/is-option-value.js @@ -0,0 +1,52 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isOptionValue } = require('../utils.js'); + +test('isOptionValue: when passed plain text then returns true', (t) => { + t.true(isOptionValue('abc')); + t.end(); +}); + +test('isOptionValue: when passed digits then returns true', (t) => { + t.true(isOptionValue(123)); + t.end(); +}); + +test('isOptionValue: when passed empty string then returns true', (t) => { + t.true(isOptionValue('')); + t.end(); +}); + +// Special case, used as stdin/stdout et al and not reason to reject +test('isOptionValue: when passed dash then returns true', (t) => { + t.true(isOptionValue('-')); + t.end(); +}); + +// Supporting undefined so can pass element off end of array without checking +test('isOptionValue: when passed undefined then returns false', (t) => { + t.false(isOptionValue(undefined)); + t.end(); +}); + +test('isOptionValue: when passed short option then returns false', (t) => { + t.false(isOptionValue('-a')); + t.end(); +}); + +test('isOptionValue: when passed short option group of short option with value then returns false', (t) => { + t.false(isOptionValue('-abd')); + t.end(); +}); + +test('isOptionValue: when passed long option then returns false', (t) => { + t.false(isOptionValue('--foo')); + t.end(); +}); + +test('isOptionValue: when passed long option with value then returns false', (t) => { + t.false(isOptionValue('--foo=bar')); + t.end(); +}); diff --git a/test/is-possible-option-value.js b/test/is-possible-option-value.js deleted file mode 100644 index 651ba98..0000000 --- a/test/is-possible-option-value.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; -/* eslint max-len: 0 */ - -const test = require('tape'); -const { isPossibleOptionValue } = require('../utils.js'); - -test('isPossibleOptionValue: when passed plain text then returns true', (t) => { - t.true(isPossibleOptionValue('abc')); - t.end(); -}); - -test('isPossibleOptionValue: when passed digits then returns true', (t) => { - t.true(isPossibleOptionValue(123)); - t.end(); -}); - -test('isPossibleOptionValue: when passed empty string then returns true', (t) => { - t.true(isPossibleOptionValue('')); - t.end(); -}); - -// Special case, used as stdin/stdout et al and not reason to reject -test('isPossibleOptionValue: when passed dash then returns true', (t) => { - t.true(isPossibleOptionValue('-')); - t.end(); -}); - -// Supporting undefined so can pass element off end of array without checking -test('isPossibleOptionValue: when passed undefined then returns false', (t) => { - t.false(isPossibleOptionValue(undefined)); - t.end(); -}); - -test('isPossibleOptionValue: when passed short option then returns false', (t) => { - t.false(isPossibleOptionValue('-a')); - t.end(); -}); - -test('isPossibleOptionValue: when passed short option group of short option with value then returns false', (t) => { - t.false(isPossibleOptionValue('-abd')); - t.end(); -}); - -test('isPossibleOptionValue: when passed long option then returns false', (t) => { - t.false(isPossibleOptionValue('--foo')); - t.end(); -}); - -test('isPossibleOptionValue: when passed long option with value then returns false', (t) => { - t.false(isPossibleOptionValue('--foo=bar')); - t.end(); -}); diff --git a/utils.js b/utils.js index ec332d6..db18184 100644 --- a/utils.js +++ b/utils.js @@ -2,10 +2,9 @@ const { ArrayPrototypeFind, - ObjectAssign, ObjectEntries, - ObjectValues, StringPrototypeCharAt, + StringPrototypeIncludes, StringPrototypeStartsWith, } = require('./primordials'); @@ -18,12 +17,12 @@ const { * Determines if the argument may be used as an option value. * NB: We are choosing not to accept option-ish arguments. * @example - * isPossibleOptionValue('V']) // returns true - * isPossibleOptionValue('-v') // returns false - * isPossibleOptionValue('--foo') // returns false - * isPossibleOptionValue(undefined) // returns false + * isOptionValue('V']) // returns true + * isOptionValue('-v') // returns false + * isOptionValue('--foo') // returns false + * isOptionValue(undefined) // returns false */ -function isPossibleOptionValue(value) { +function isOptionValue(value) { if (value === undefined) return false; if (value === '-') return true; // e.g. representing stdin/stdout for file @@ -46,82 +45,61 @@ function isLoneShortOption(arg) { } /** - * Determines if `arg` is a long option, which may have a trailing value. + * Determines if `arg` is a lone long option. * @example - * isLongOption('-a) // returns false - * isLongOption('--foo) // returns true - * isLongOption('--foo=bar) // returns true + * isLoneLongOption('a') // returns false + * isLoneLongOption('-a') // returns false + * isLoneLongOption('--foo) // returns true + * isLoneLongOption('--foo=bar) // returns false */ -function isLongOption(arg) { - return arg.length > 2 && StringPrototypeStartsWith(arg, '--'); -} - -function getDefaultOptionConfig() { - return { - short: undefined, - type: 'boolean', - multiple: false - }; +function isLoneLongOption(arg) { + return arg.length > 2 && + StringPrototypeStartsWith(arg, '--') && + !StringPrototypeIncludes(arg.slice(3), '='); } /** - * Lookup option config. Returns undefined if no match. - */ -function findOptionConfigFromShort(shortOption, options) { - const foundConfig = ArrayPrototypeFind( - ObjectValues(options), - (optionConfig) => optionConfig.short === shortOption - ); - return foundConfig; -} - -/** - * Populate an option config using options and defaults. + * Determines if `arg` is a long option and value in same argument. + * @example + * isLongOptionAndValue('--foo) // returns true + * isLongOptionAndValue('--foo=bar) // returns false */ -function getOptionConfigFromShort(shortOption, options) { - const optionConfig = findOptionConfigFromShort(shortOption, options) || {}; - return ObjectAssign(getDefaultOptionConfig(), optionConfig); +function isLongOptionAndValue(arg) { + return arg.length > 2 && + StringPrototypeStartsWith(arg, '--') && + StringPrototypeIncludes(arg.slice(3), '='); } /** - * Return whether a short option is of boolean type, implicitly or explicitly. + * Determines if `arg` is a short option group. + * + * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). + * One or more options without option-arguments, followed by at most one + * option that takes an option-argument, should be accepted when grouped + * behind one '-' delimiter. + * @example + * isShortOptionGroup('-a', {}) // returns false + * isShortOptionGroup('-ab', {}) // returns true + * // -fb is an option and a value, not a short option group + * isShortOptionGroup('-fb', { + * options: { f: { type: 'string' }} + * }) // returns false + * isShortOptionGroup('-bf', { + * options: { f: { type: 'string' }} + * }) // returns true + * // -bfb is an edge case, return true and caller sorts it out + * isShortOptionGroup('-bfb', { + * options: { f: { type: 'string' }} + * }) // returns true */ -function isShortOfTypeBoolean(shortOption, options) { - if (!options) throw new Error('Internal error, missing options argument'); - - const optionConfig = getOptionConfigFromShort(shortOption, options); - return optionConfig.type === 'boolean'; -} - -/** - * Determines if `arg` is a short option group. - * - * See Guideline 5 of the [Open Group Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). - * One or more options without option-arguments, followed by at most one - * option that takes an option-argument, should be accepted when grouped - * behind one '-' delimiter. - * @example - * isShortOptionGroup('-a', {}) // returns false - * isShortOptionGroup('-ab', {}) // returns true - * // -fb is an option and a value, not a short option group - * isShortOptionGroup('-fb', { - * options: { f: { type: 'string' }} - * }) // returns false - * isShortOptionGroup('-bf', { - * options: { f: { type: 'string' }} - * }) // returns true - * // -bfb is an edge case, return true and caller sorts it out - * isShortOptionGroup('-bfb', { - * options: { f: { type: 'string' }} - * }) // returns true - */ function isShortOptionGroup(arg, options) { if (arg.length <= 2) return false; if (StringPrototypeCharAt(arg, 0) !== '-') return false; if (StringPrototypeCharAt(arg, 1) === '-') return false; const firstShort = StringPrototypeCharAt(arg, 1); - return isShortOfTypeBoolean(firstShort, options); + const longOption = findLongOptionForShort(firstShort, options); + return (options[longOption]?.type !== 'string'); } /** @@ -144,8 +122,9 @@ function findLongOptionForShort(shortOption, options) { module.exports = { findLongOptionForShort, - isLongOption, + isLoneLongOption, isLoneShortOption, - isPossibleOptionValue, + isLongOptionAndValue, + isOptionValue, isShortOptionGroup }; From 8f85ecd50a0b5ee1eedf6df44204950bb48c5e00 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 3 Mar 2022 19:03:27 +1300 Subject: [PATCH 12/36] Switch loop to shift --- index.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 7868abe..11545b6 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const { ArrayPrototypeConcat, ArrayPrototypeForEach, + ArrayPrototypeShift, ArrayPrototypeSlice, ArrayPrototypePush, ObjectHasOwn, @@ -121,10 +122,10 @@ const parseArgs = ({ positionals: [] }; - let pos = 0; - while (pos < args.length) { - const arg = args[pos]; - const nextArg = args[pos + 1]; + const remainingArgs = ArrayPrototypeSlice(args); + while (remainingArgs.length > 0) { + const arg = ArrayPrototypeShift(remainingArgs); + const nextArg = remainingArgs[0]; // Check if `arg` is an options terminator. // Guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html @@ -132,7 +133,7 @@ const parseArgs = ({ // Everything after a bare '--' is considered a positional argument. result.positionals = ArrayPrototypeConcat( result.positionals, - ArrayPrototypeSlice(args, pos + 1) + remainingArgs ); break; // Finished processing argv, leave while loop. } @@ -144,11 +145,9 @@ const parseArgs = ({ let optionValue; if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) { // e.g. '-f', 'bar' - optionValue = nextArg; - pos++; + optionValue = ArrayPrototypeShift(remainingArgs); } storeOptionValue(options, longOption, optionValue, result); - pos++; continue; } @@ -158,11 +157,9 @@ const parseArgs = ({ let optionValue; if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) { // e.g. '-foo', 'bar' - optionValue = nextArg; - pos++; + optionValue = ArrayPrototypeShift(remainingArgs); } storeOptionValue(options, longOption, optionValue, result); - pos++; continue; } @@ -172,13 +169,11 @@ const parseArgs = ({ const longOption = StringPrototypeSlice(arg, 2, index); const optionValue = StringPrototypeSlice(arg, index + 1); storeOptionValue(options, longOption, optionValue, result); - pos++; continue; } // Anything left is a positional ArrayPrototypePush(result.positionals, arg); - pos++; } return result; From cb93bfacf4f14f782e66caf2e9b50f15d109f3e7 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 3 Mar 2022 21:32:03 +1300 Subject: [PATCH 13/36] Add isShortOptionAndValue --- index.js | 35 +++++++++++++++++- test/is-short-option-and-value.js | 60 +++++++++++++++++++++++++++++++ utils.js | 21 +++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 test/is-short-option-and-value.js diff --git a/index.js b/index.js index 11545b6..60fe0ea 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,9 @@ const { isLoneLongOption, isLoneShortOption, isLongOptionAndValue, - isOptionValue + isOptionValue, + isShortOptionAndValue, + isShortOptionGroup } = require('./utils'); function getMainArgs() { @@ -151,6 +153,37 @@ const parseArgs = ({ continue; } + if (isShortOptionGroup(arg, options)) { + // e.g. '-abc' + for (let index = 1; index < arg.length; index++) { + const shortOption = StringPrototypeCharAt(arg, index); + const longOption = findLongOptionForShort(shortOption, options); + if (options[longOption]?.type !== 'string') { + // Store boolean option. + storeOptionValue(options, longOption, undefined, result); + } else if (index === arg.length - 1) { + // String option on end of group, process separately. + remainingArgs.unshift(`-${shortOption}`); + } else { + // String option in middle. Urk. + // ToDo: if strict then throw + // Eat remaining arg as value. + const optionValue = arg.slice(index + 1); + storeOptionValue(options, longOption, optionValue, result); + break; // finished short group + } + } + continue; + } + + if (isShortOptionAndValue(arg, options)) { + // e.g. -fFILE + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + const optionValue = arg.slice(2); + storeOptionValue(options, longOption, optionValue, result); + } + if (isLoneLongOption(arg)) { // e.g. '--foo' const longOption = StringPrototypeSlice(arg, 2); diff --git a/test/is-short-option-and-value.js b/test/is-short-option-and-value.js new file mode 100644 index 0000000..9b43b20 --- /dev/null +++ b/test/is-short-option-and-value.js @@ -0,0 +1,60 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { isShortOptionAndValue } = require('../utils.js'); + +test('isShortOptionAndValue: when passed lone short option then returns false', (t) => { + t.false(isShortOptionAndValue('-s', {})); + t.end(); +}); + +test('isShortOptionAndValue: when passed group with leading zero-config boolean then returns false', (t) => { + t.false(isShortOptionAndValue('-ab', {})); + t.end(); +}); + +test('isShortOptionAndValue: when passed group with leading configured implicit boolean then returns false', (t) => { + t.false(isShortOptionAndValue('-ab', { aaa: { short: 'a' } })); + t.end(); +}); + +test('isShortOptionAndValue: when passed group with leading configured explicit boolean then returns false', (t) => { + t.false(isShortOptionAndValue('-ab', { aaa: { short: 'a', type: 'boolean' } })); + t.end(); +}); + +test('isShortOptionAndValue: when passed group with leading configured string then returns true', (t) => { + t.true(isShortOptionAndValue('-ab', { aaa: { short: 'a', type: 'string' } })); + t.end(); +}); + +test('isShortOptionAndValue: when passed long option then returns false', (t) => { + t.false(isShortOptionAndValue('--foo', {})); + t.end(); +}); + +test('isShortOptionAndValue: when passed long option with value then returns false', (t) => { + t.false(isShortOptionAndValue('--foo=bar', {})); + t.end(); +}); + +test('isShortOptionAndValue: when passed empty string then returns false', (t) => { + t.false(isShortOptionAndValue('', {})); + t.end(); +}); + +test('isShortOptionAndValue: when passed plain text then returns false', (t) => { + t.false(isShortOptionAndValue('foo', {})); + t.end(); +}); + +test('isShortOptionAndValue: when passed single dash then returns false', (t) => { + t.false(isShortOptionAndValue('-', {})); + t.end(); +}); + +test('isShortOptionAndValue: when passed double dash then returns false', (t) => { + t.false(isShortOptionAndValue('--', {})); + t.end(); +}); diff --git a/utils.js b/utils.js index db18184..b6e94b4 100644 --- a/utils.js +++ b/utils.js @@ -102,6 +102,26 @@ function isShortOptionGroup(arg, options) { return (options[longOption]?.type !== 'string'); } +/** + * Determine is arg is a short string option followed by its value. + * @example + * isShortOptionAndValue('-a, {}); // returns false + * isShortOptionAndValue('-ab, {}); // returns false + * isShortOptionAndValue('-fFILE', { + * options: { foo: { short: 'f', type: 'string' }} + * }) // returns true + */ +function isShortOptionAndValue(arg, options) { + if (!options) throw new Error('Internal error, missing options argument'); + if (arg.length <= 2) return false; + if (StringPrototypeCharAt(arg, 0) !== '-') return false; + if (StringPrototypeCharAt(arg, 1) === '-') return false; + + const shortOption = StringPrototypeCharAt(arg, 1); + const longOption = findLongOptionForShort(shortOption, options); + return (options[longOption]?.type === 'string'); +} + /** * Find the key to use for a short option. Looks for a configured * `short` and returns the short option itself it not found. @@ -126,5 +146,6 @@ module.exports = { isLoneShortOption, isLongOptionAndValue, isOptionValue, + isShortOptionAndValue, isShortOptionGroup }; From 4683053f8c8027c41ae2abb79e3df278a4f82e65 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 08:14:20 +1300 Subject: [PATCH 14/36] Form expanded, clearer --- index.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 60fe0ea..09cbcdc 100644 --- a/index.js +++ b/index.js @@ -154,24 +154,21 @@ const parseArgs = ({ } if (isShortOptionGroup(arg, options)) { - // e.g. '-abc' + // Expand -fXzy to -f -X -z -y + const expanded = []; for (let index = 1; index < arg.length; index++) { const shortOption = StringPrototypeCharAt(arg, index); const longOption = findLongOptionForShort(shortOption, options); - if (options[longOption]?.type !== 'string') { - // Store boolean option. - storeOptionValue(options, longOption, undefined, result); - } else if (index === arg.length - 1) { - // String option on end of group, process separately. - remainingArgs.unshift(`-${shortOption}`); + if (options[longOption]?.type !== 'string' || + index === arg.length - 1) { + // Boolean option, or last short in group. Well formed. + ArrayPrototypePush(expanded, `-${shortOption}`); } else { - // String option in middle. Urk. + // String option in middle. Yuck. // ToDo: if strict then throw - // Eat remaining arg as value. - const optionValue = arg.slice(index + 1); - storeOptionValue(options, longOption, optionValue, result); + // Expand -abfFILE to -a -b -fFILE + ArrayPrototypePush(expanded, ArrayPrototypeSlice(index)); break; // finished short group - } } continue; } From aeff8890c7c327eb435586eb5ee346c46a808120 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 08:20:30 +1300 Subject: [PATCH 15/36] Fixes --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 09cbcdc..f3ee818 100644 --- a/index.js +++ b/index.js @@ -124,7 +124,7 @@ const parseArgs = ({ positionals: [] }; - const remainingArgs = ArrayPrototypeSlice(args); + let remainingArgs = ArrayPrototypeSlice(args); while (remainingArgs.length > 0) { const arg = ArrayPrototypeShift(remainingArgs); const nextArg = remainingArgs[0]; @@ -169,7 +169,9 @@ const parseArgs = ({ // Expand -abfFILE to -a -b -fFILE ArrayPrototypePush(expanded, ArrayPrototypeSlice(index)); break; // finished short group + } } + remainingArgs = ArrayPrototypeConcat(expanded, remainingArgs); continue; } From 0c399bcfd7c7298a0352a80b353ee56410b68c63 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 09:02:57 +1300 Subject: [PATCH 16/36] Improve comments --- utils.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/utils.js b/utils.js index b6e94b4..14d1aec 100644 --- a/utils.js +++ b/utils.js @@ -8,10 +8,12 @@ const { StringPrototypeStartsWith, } = require('./primordials'); -// These are internal utilities to make the parsing logic easier to read. They -// are not for client use. They are in a separate file to allow unit testing, -// although that is not essential (this could be rolled into main file -// and just tested implicitly via API). +// 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 +// main file and just tested implicitly via API). +// +// These routines are for internal use, not for export to client. /** * Determines if the argument may be used as an option value. @@ -26,8 +28,9 @@ function isOptionValue(value) { if (value === undefined) return false; if (value === '-') return true; // e.g. representing stdin/stdout for file - // Open Group Utility Conventions are that an option-argument may start - // with a dash, but we are currentlly rejecting these and prioritising the + // Open Group Utility Conventions are that an option-argument + // is the argument after the option, and may start with a dash. + // However, we are currently rejecting these and prioritising the // option-like appearance of the argument. Rejection allows more error // detection for strict:true, but comes at the cost of rejecting intended // values starting with a dash, especially negative numbers. @@ -123,8 +126,8 @@ function isShortOptionAndValue(arg, options) { } /** - * Find the key to use for a short option. Looks for a configured - * `short` and returns the short option itself it not found. + * 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. * @example * findOptionsKeyForShort('a', {}) // returns 'a' * findOptionsKeyForShort('b', { From db3e06ee1f3209d0d3b35b853912713981a514c8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 09:38:49 +1300 Subject: [PATCH 17/36] New tests for short option group (and fixes) --- index.js | 5 ++- test/short-option-groups.js | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 test/short-option-groups.js diff --git a/index.js b/index.js index f3ee818..64e6304 100644 --- a/index.js +++ b/index.js @@ -167,7 +167,7 @@ const parseArgs = ({ // String option in middle. Yuck. // ToDo: if strict then throw // Expand -abfFILE to -a -b -fFILE - ArrayPrototypePush(expanded, ArrayPrototypeSlice(index)); + ArrayPrototypePush(expanded, `-${StringPrototypeSlice(arg, index)}`); break; // finished short group } } @@ -179,8 +179,9 @@ const parseArgs = ({ // e.g. -fFILE const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); - const optionValue = arg.slice(2); + const optionValue = StringPrototypeSlice(arg, 2); storeOptionValue(options, longOption, optionValue, result); + continue; } if (isLoneLongOption(arg)) { diff --git a/test/short-option-groups.js b/test/short-option-groups.js new file mode 100644 index 0000000..6b6392d --- /dev/null +++ b/test/short-option-groups.js @@ -0,0 +1,77 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { parseArgs } = require('../index.js'); + +test('when pass zero-config group of booleans then parsed as booleans', (t) => { + const passedArgs = ['-rf', 'p']; + const passedOptions = { }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when pass low-config group of booleans then parsed as booleans', (t) => { + const passedArgs = ['-rf', 'p']; + const passedOptions = { r: {}, f: {} }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when pass full-config group of booleans then parsed as booleans', (t) => { + const passedArgs = ['-rf', 'p']; + const passedOptions = { r: { type: 'boolean' }, f: { type: 'boolean' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when pass group with string option on end then parsed as booleans and string option', (t) => { + const passedArgs = ['-rf', 'p']; + const passedOptions = { r: { type: 'boolean' }, f: { type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { r: true, f: true }, values: { r: undefined, f: 'p' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when pass group with string option in middle and strict:false then parsed as booleans and string option with trailing value', (t) => { + const passedArgs = ['-afb', 'p']; + const passedOptions = { f: { type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions, strict: false }); + const expected = { flags: { a: true, f: true }, values: { a: undefined, f: 'b' }, positionals: ['p'] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +// Hopefully coming: +// test('when pass group with string option in middle and strict:true then error', (t) => { +// const passedArgs = ['-afb', 'p']; +// const passedOptions = { f: { type: 'string' } }; +// +// t.throws(() => { +// parseArgs({ args: passedArgs, options: passedOptions, strict: true }); +// }); +// +// t.end(); +// }); From 3a7ea3c1bfdfb972c791196c29d136a4536a9f69 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 13:31:14 +1300 Subject: [PATCH 18/36] Add tests for combining short and value --- test/short-option-combined-with-value.js | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 test/short-option-combined-with-value.js diff --git a/test/short-option-combined-with-value.js b/test/short-option-combined-with-value.js new file mode 100644 index 0000000..70561c9 --- /dev/null +++ b/test/short-option-combined-with-value.js @@ -0,0 +1,90 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { parseArgs } = require('../index.js'); + +test('when combine string short with plain text then parsed as value', (t) => { + const passedArgs = ['-aHELLO']; + const passedOptions = { alpha: { short: 'a', type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { alpha: true }, values: { alpha: 'HELLO' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when combine low-config string short with plain text then parsed as value', (t) => { + const passedArgs = ['-aHELLO']; + const passedOptions = { a: { type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { a: true }, values: { a: 'HELLO' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when combine string short with value like short option then parsed as value', (t) => { + const passedArgs = ['-a-b']; + const passedOptions = { alpha: { short: 'a', type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { alpha: true }, values: { alpha: '-b' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when combine string short with value like long option then parsed as value', (t) => { + const passedArgs = ['-a--bar']; + const passedOptions = { alpha: { short: 'a', type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { alpha: true }, values: { alpha: '--bar' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when combine string short with value like negative number then parsed as value', (t) => { + const passedArgs = ['-a-5']; + const passedOptions = { alpha: { short: 'a', type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { alpha: true }, values: { alpha: '-5' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); + + +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 result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { alpha: true }, values: { alpha: 'f' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); + +test('when combine string short with value including equals then parsed with equals in value', (t) => { + const passedArgs = ['-a=5']; + const passedOptions = { alpha: { short: 'a', type: 'string' } }; + + const result = parseArgs({ args: passedArgs, options: passedOptions }); + const expected = { flags: { alpha: true }, values: { alpha: '=5' }, positionals: [] }; + + t.deepEqual(result, expected); + + t.end(); +}); From bb22e5a9e0ef0929db4d6a0419d75adc32537e87 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 16:21:24 +1300 Subject: [PATCH 19/36] Update package.json Co-authored-by: Jordan Harband --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4bdd5be..2488d50 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "coverage": "c8 --check-coverage tape 'test/*.js'", - "test": "c8 tape 'test/*.js'", "posttest": "eslint .", + "test": "c8 tape 'test/*.js'", + "posttest": "eslint .", "fix": "npm run posttest -- --fix" }, "repository": { From eac5ecf2fca2675c5efd912d17f3d5397963def7 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 16:21:44 +1300 Subject: [PATCH 20/36] Update utils.js Co-authored-by: Jordan Harband --- utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.js b/utils.js index 14d1aec..f1209cf 100644 --- a/utils.js +++ b/utils.js @@ -58,7 +58,7 @@ function isLoneShortOption(arg) { function isLoneLongOption(arg) { return arg.length > 2 && StringPrototypeStartsWith(arg, '--') && - !StringPrototypeIncludes(arg.slice(3), '='); + !StringPrototypeIncludes(StringPrototypeSlice(arg, 3), '='); } /** From 419060c2c317fab5687398bae771513dfacd09bc Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 16:21:51 +1300 Subject: [PATCH 21/36] Update utils.js Co-authored-by: Jordan Harband --- utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.js b/utils.js index f1209cf..ed12fa0 100644 --- a/utils.js +++ b/utils.js @@ -70,7 +70,7 @@ function isLoneLongOption(arg) { function isLongOptionAndValue(arg) { return arg.length > 2 && StringPrototypeStartsWith(arg, '--') && - StringPrototypeIncludes(arg.slice(3), '='); + StringPrototypeIncludes(StringPrototypeSlice(arg, 3), '='); } /** From 11bd06f9dcca8e6c32083bcd849be9d2e701ee28 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 16:24:30 +1300 Subject: [PATCH 22/36] Add import --- utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/utils.js b/utils.js index ed12fa0..1f896f0 100644 --- a/utils.js +++ b/utils.js @@ -5,6 +5,7 @@ const { ObjectEntries, StringPrototypeCharAt, StringPrototypeIncludes, + StringPrototypeSlice, StringPrototypeStartsWith, } = require('./primordials'); From 1c1d047725eb84eaa7dacdc5abda38e649cbeb44 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 16:30:27 +1300 Subject: [PATCH 23/36] Add exports to keep utils private --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2488d50..26b5602 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.3.0", "description": "Polyfill of future proposal for `util.parseArgs()`", "main": "index.js", + "exports": "./index.js", "scripts": { "coverage": "c8 --check-coverage tape 'test/*.js'", "test": "c8 tape 'test/*.js'", From 3fcdcb3fc6f75869e4ff5285b3dc938222213aa1 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 16:48:19 +1300 Subject: [PATCH 24/36] AAA: Arrange, Act, Assert --- test/dash.js | 8 ++++---- test/short-option-combined-with-value.js | 21 +++++++-------------- test/short-option-groups.js | 16 +++++----------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/test/dash.js b/test/dash.js index f21d336..71bda19 100644 --- a/test/dash.js +++ b/test/dash.js @@ -10,11 +10,11 @@ const { parseArgs } = require('../index.js'); // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html test("dash: when args include '-' used as positional then result has '-' in positionals", (t) => { const passedArgs = ['-']; + const expected = { flags: {}, values: {}, positionals: ['-'] }; const result = parseArgs({ args: passedArgs }); - const expected = { flags: {}, values: {}, positionals: ['-'] }; - t.deepEqual(result, expected); + t.deepEqual(result, expected); t.end(); }); @@ -22,10 +22,10 @@ test("dash: when args include '-' used as positional then result has '-' in posi test("dash: when args include '-' used as space-separated option value then result has '-' in option value", (t) => { const passedArgs = ['-v', '-']; const options = { v: { type: 'string' } }; + const expected = { flags: { v: true }, values: { v: '-' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options }); - const expected = { flags: { v: true }, values: { v: '-' }, positionals: [] }; - t.deepEqual(result, expected); + 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 70561c9..66fb5d2 100644 --- a/test/short-option-combined-with-value.js +++ b/test/short-option-combined-with-value.js @@ -7,60 +7,55 @@ const { parseArgs } = require('../index.js'); test('when combine string short with plain text then parsed as value', (t) => { const passedArgs = ['-aHELLO']; const passedOptions = { alpha: { short: 'a', type: 'string' } }; + const expected = { flags: { alpha: true }, values: { alpha: 'HELLO' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { alpha: true }, values: { alpha: 'HELLO' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); test('when combine low-config string short with plain text then parsed as value', (t) => { const passedArgs = ['-aHELLO']; const passedOptions = { a: { type: 'string' } }; + const expected = { flags: { a: true }, values: { a: 'HELLO' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { a: true }, values: { a: 'HELLO' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); test('when combine string short with value like short option then parsed as value', (t) => { const passedArgs = ['-a-b']; const passedOptions = { alpha: { short: 'a', type: 'string' } }; + const expected = { flags: { alpha: true }, values: { alpha: '-b' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { alpha: true }, values: { alpha: '-b' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); test('when combine string short with value like long option then parsed as value', (t) => { const passedArgs = ['-a--bar']; const passedOptions = { alpha: { short: 'a', type: 'string' } }; + const expected = { flags: { alpha: true }, values: { alpha: '--bar' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { alpha: true }, values: { alpha: '--bar' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); test('when combine string short with value like negative number then parsed as value', (t) => { const passedArgs = ['-a-5']; const passedOptions = { alpha: { short: 'a', type: 'string' } }; + const expected = { flags: { alpha: true }, values: { alpha: '-5' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { alpha: true }, values: { alpha: '-5' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); @@ -68,23 +63,21 @@ 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 expected = { flags: { alpha: true }, values: { alpha: 'f' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { alpha: true }, values: { alpha: 'f' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); test('when combine string short with value including equals then parsed with equals in value', (t) => { const passedArgs = ['-a=5']; const passedOptions = { alpha: { short: 'a', type: 'string' } }; + const expected = { flags: { alpha: true }, values: { alpha: '=5' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { alpha: true }, values: { alpha: '=5' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); diff --git a/test/short-option-groups.js b/test/short-option-groups.js index 6b6392d..f849b50 100644 --- a/test/short-option-groups.js +++ b/test/short-option-groups.js @@ -7,60 +7,55 @@ const { parseArgs } = require('../index.js'); test('when pass zero-config group of booleans then parsed as booleans', (t) => { const passedArgs = ['-rf', 'p']; const passedOptions = { }; + const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; t.deepEqual(result, expected); - t.end(); }); test('when pass low-config group of booleans then parsed as booleans', (t) => { const passedArgs = ['-rf', 'p']; const passedOptions = { r: {}, f: {} }; + const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; t.deepEqual(result, expected); - t.end(); }); test('when pass full-config group of booleans then parsed as booleans', (t) => { const passedArgs = ['-rf', 'p']; 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 }); - const expected = { flags: { r: true, f: true }, values: { r: undefined, f: undefined }, positionals: ['p'] }; t.deepEqual(result, expected); - t.end(); }); test('when pass group with string option on end then parsed as booleans and string option', (t) => { const passedArgs = ['-rf', 'p']; const passedOptions = { r: { type: 'boolean' }, f: { type: 'string' } }; + const expected = { flags: { r: true, f: true }, values: { r: undefined, f: 'p' }, positionals: [] }; const result = parseArgs({ args: passedArgs, options: passedOptions }); - const expected = { flags: { r: true, f: true }, values: { r: undefined, f: 'p' }, positionals: [] }; t.deepEqual(result, expected); - t.end(); }); test('when pass group with string option in middle and strict:false then parsed as booleans and string option with trailing value', (t) => { const passedArgs = ['-afb', 'p']; const passedOptions = { f: { type: 'string' } }; + const expected = { flags: { a: true, f: true }, values: { a: undefined, f: 'b' }, positionals: ['p'] }; const result = parseArgs({ args: passedArgs, options: passedOptions, strict: false }); - const expected = { flags: { a: true, f: true }, values: { a: undefined, f: 'b' }, positionals: ['p'] }; t.deepEqual(result, expected); - t.end(); }); @@ -72,6 +67,5 @@ test('when pass group with string option in middle and strict:false then parsed // t.throws(() => { // parseArgs({ args: passedArgs, options: passedOptions, strict: true }); // }); -// // t.end(); // }); From cf48248010d0cc789b5d385101e987a90bbaa3fa Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 17:30:00 +1300 Subject: [PATCH 25/36] Add tests for failure duck typing --- test/store-user-intent.js | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/store-user-intent.js diff --git a/test/store-user-intent.js b/test/store-user-intent.js new file mode 100644 index 0000000..9c75d4a --- /dev/null +++ b/test/store-user-intent.js @@ -0,0 +1,52 @@ +'use strict'; +/* eslint max-len: 0 */ + +const test = require('tape'); +const { parseArgs } = require('../index.js'); + + +// Rationale +// +// John Gee: +// Looks like a boolean option, stored like a flag. Looks like a string option, stored like a string option. +// No loss of information. No new pattern to learn in result. +// +// Jordan Harband: In other words, the way they're stored matches the intention of the user, +// not the configurer, which will ensure the configurer can most accurately respond to the +// user's intentions. + +test('when use string short option used as boolean then result as if boolean', (t) => { + const passedArgs = ['-o']; + const stringOptions = { opt: { short: 'o', type: 'string' } }; + const booleanOptions = { opt: { short: 'o', type: 'boolean' } }; + + const stringConfigResult = parseArgs({ args: passedArgs, options: stringOptions, strict: false }); + const booleanConfigResult = parseArgs({ args: passedArgs, options: booleanOptions, strict: false }); + + t.deepEqual(stringConfigResult, booleanConfigResult); + t.end(); +}); + +test('when use string long option used as boolean then result as if boolean', (t) => { + const passedArgs = ['--opt']; + const stringOptions = { opt: { short: 'o', type: 'string' } }; + const booleanOptions = { opt: { short: 'o', type: 'boolean' } }; + + const stringConfigResult = parseArgs({ args: passedArgs, options: stringOptions, strict: false }); + const booleanConfigResult = parseArgs({ args: passedArgs, options: booleanOptions, strict: false }); + + t.deepEqual(stringConfigResult, booleanConfigResult); + t.end(); +}); + +test('when use boolean long option used as string then result as if string', (t) => { + const passedArgs = ['--bool=OOPS']; + const stringOptions = { bool: { short: 'b', type: 'string' } }; + const booleanOptions = { bool: { short: 'b', type: 'boolean' } }; + + const stringConfigResult = parseArgs({ args: passedArgs, options: stringOptions, strict: false }); + const booleanConfigResult = parseArgs({ args: passedArgs, options: booleanOptions, strict: false }); + + t.deepEqual(booleanConfigResult, stringConfigResult); + t.end(); +}); From d2a1bc4b66556ed8fba0e9cac3a333172f96def4 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 17:37:35 +1300 Subject: [PATCH 26/36] Add another dash example --- test/dash.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/dash.js b/test/dash.js index 71bda19..2457414 100644 --- a/test/dash.js +++ b/test/dash.js @@ -8,6 +8,9 @@ const { parseArgs } = require('../index.js'); // The interpretation is up to the utility, and for a file positional (operand) the examples are // '-' may stand for standard input (or standard output), or for a file named -. // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html +// +// A different usage and example is `git switch -` to switch back to the previous branch. + test("dash: when args include '-' used as positional then result has '-' in positionals", (t) => { const passedArgs = ['-']; const expected = { flags: {}, values: {}, positionals: ['-'] }; From 22fd53826d025273d050fb9b0a96d1433b7ab178 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 4 Mar 2022 17:43:32 +1300 Subject: [PATCH 27/36] Make test for undefined more robust --- utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.js b/utils.js index 1f896f0..ff609e0 100644 --- a/utils.js +++ b/utils.js @@ -26,7 +26,7 @@ const { * isOptionValue(undefined) // returns false */ function isOptionValue(value) { - if (value === undefined) return false; + if (value == null) return false; if (value === '-') return true; // e.g. representing stdin/stdout for file // Open Group Utility Conventions are that an option-argument From a409102299c478e1c5cf74d8915f9bd8c3832280 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 08:21:15 +1300 Subject: [PATCH 28/36] Update utils.js Co-authored-by: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> --- utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils.js b/utils.js index ff609e0..58f4714 100644 --- a/utils.js +++ b/utils.js @@ -65,8 +65,8 @@ function isLoneLongOption(arg) { /** * Determines if `arg` is a long option and value in same argument. * @example - * isLongOptionAndValue('--foo) // returns true - * isLongOptionAndValue('--foo=bar) // returns false + * isLongOptionAndValue('--foo) // returns false + * isLongOptionAndValue('--foo=bar) // returns true */ function isLongOptionAndValue(arg) { return arg.length > 2 && From 1bf4cfbb55419a05122de70c4298ba204351783c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 14:11:40 +1300 Subject: [PATCH 29/36] Update utils.js Co-authored-by: Jordan Harband --- utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.js b/utils.js index 58f4714..e019950 100644 --- a/utils.js +++ b/utils.js @@ -123,7 +123,7 @@ function isShortOptionAndValue(arg, options) { const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); - return (options[longOption]?.type === 'string'); + return options[longOption]?.type === 'string'; } /** From 228056b9974a79e1a9b70afe34261164c6bcf435 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 14:11:50 +1300 Subject: [PATCH 30/36] Update utils.js Co-authored-by: Jordan Harband --- utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.js b/utils.js index e019950..a983a2f 100644 --- a/utils.js +++ b/utils.js @@ -103,7 +103,7 @@ function isShortOptionGroup(arg, options) { const firstShort = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(firstShort, options); - return (options[longOption]?.type !== 'string'); + return options[longOption]?.type !== 'string'; } /** From bc6dae7a8fab2b912bb3b106fdd342b96be501da Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 14:12:02 +1300 Subject: [PATCH 31/36] Update package.json Co-authored-by: Jordan Harband --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 26b5602..a54bb82 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "version": "0.3.0", "description": "Polyfill of future proposal for `util.parseArgs()`", "main": "index.js", - "exports": "./index.js", + "exports": { + ".": "./index.js", + "./package.json": "./package.json" + }, "scripts": { "coverage": "c8 --check-coverage tape 'test/*.js'", "test": "c8 tape 'test/*.js'", From 6153fd2527975156710e88e4cf89128ca452b9d8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 20:29:37 +1300 Subject: [PATCH 32/36] Update index.js Co-authored-by: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 64e6304..61bffd7 100644 --- a/index.js +++ b/index.js @@ -137,7 +137,7 @@ const parseArgs = ({ result.positionals, remainingArgs ); - break; // Finished processing argv, leave while loop. + break; // Finished processing args, leave while loop. } if (isLoneShortOption(arg)) { From eff783edfad2d3472417d84eb61bcf7ea706b9d0 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 20:31:08 +1300 Subject: [PATCH 33/36] Comment improvements --- test/store-user-intent.js | 7 ++++--- utils.js | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/store-user-intent.js b/test/store-user-intent.js index 9c75d4a..d5340a9 100644 --- a/test/store-user-intent.js +++ b/test/store-user-intent.js @@ -8,7 +8,8 @@ const { parseArgs } = require('../index.js'); // Rationale // // John Gee: -// Looks like a boolean option, stored like a flag. Looks like a string option, stored like a string option. +// - Looks like a boolean option, stored like a boolean option. +// - Looks like a string option, stored like a string option. // No loss of information. No new pattern to learn in result. // // Jordan Harband: In other words, the way they're stored matches the intention of the user, @@ -41,8 +42,8 @@ test('when use string long option used as boolean then result as if boolean', (t test('when use boolean long option used as string then result as if string', (t) => { const passedArgs = ['--bool=OOPS']; - const stringOptions = { bool: { short: 'b', type: 'string' } }; - const booleanOptions = { bool: { short: 'b', type: 'boolean' } }; + const stringOptions = { bool: { type: 'string' } }; + const booleanOptions = { bool: { type: 'boolean' } }; const stringConfigResult = parseArgs({ args: passedArgs, options: stringOptions, strict: false }); const booleanConfigResult = parseArgs({ args: passedArgs, options: booleanOptions, strict: false }); diff --git a/utils.js b/utils.js index a983a2f..eca8711 100644 --- a/utils.js +++ b/utils.js @@ -130,8 +130,8 @@ 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. * @example - * findOptionsKeyForShort('a', {}) // returns 'a' - * findOptionsKeyForShort('b', { + * findLongOptionForShort('a', {}) // returns 'a' + * findLongOptionForShort('b', { * options: { bar: { short: 'b' }} * }) // returns 'bar' */ From 90f986497c39d616b8e5e49a138477eeb600bb1d Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 20:32:45 +1300 Subject: [PATCH 34/36] Update index.js Co-authored-by: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 61bffd7..56da055 100644 --- a/index.js +++ b/index.js @@ -189,7 +189,7 @@ const parseArgs = ({ const longOption = StringPrototypeSlice(arg, 2); let optionValue; if (options[longOption]?.type === 'string' && isOptionValue(nextArg)) { - // e.g. '-foo', 'bar' + // e.g. '--foo', 'bar' optionValue = ArrayPrototypeShift(remainingArgs); } storeOptionValue(options, longOption, optionValue, result); From 6013dc459fe84ff4c984d30943bc845d50afa8c6 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 20:33:16 +1300 Subject: [PATCH 35/36] Update test/dash.js Co-authored-by: Aaron Casanova <32409546+aaronccasanova@users.noreply.github.com> --- test/dash.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dash.js b/test/dash.js index 2457414..c727e0a 100644 --- a/test/dash.js +++ b/test/dash.js @@ -24,10 +24,10 @@ test("dash: when args include '-' used as positional then result has '-' in posi // If '-' is a valid positional, it is symmetrical to allow it as an option value too. test("dash: when args include '-' used as space-separated option value then result has '-' in option value", (t) => { const passedArgs = ['-v', '-']; - const options = { v: { type: 'string' } }; + const passedOptions = { v: { type: 'string' } }; const expected = { flags: { v: true }, values: { v: '-' }, positionals: [] }; - const result = parseArgs({ args: passedArgs, options }); + const result = parseArgs({ args: passedArgs, options: passedOptions }); t.deepEqual(result, expected); t.end(); From e4b4f2852a276de44f42f0e217e140a49ecddcdd Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 5 Mar 2022 20:45:40 +1300 Subject: [PATCH 36/36] Expand test description per feedback --- test/is-lone-short-option.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/is-lone-short-option.js b/test/is-lone-short-option.js index aa97de0..baea02b 100644 --- a/test/is-lone-short-option.js +++ b/test/is-lone-short-option.js @@ -9,7 +9,7 @@ test('isLoneShortOption: when passed short option then returns true', (t) => { t.end(); }); -test('isLoneShortOption: when passed short option group then returns false', (t) => { +test('isLoneShortOption: when passed short option group (or might be short and value) then returns false', (t) => { t.false(isLoneShortOption('-abc')); t.end(); });