Skip to content

Commit

Permalink
refactor!: restructure configuration to take options bag (#63)
Browse files Browse the repository at this point in the history
Per the discussion in #45 this PR restructures the current options API where each option is configured in three separate list and instead allows options to be configured in a single object.

The goal being to make the API more intuitive for configuring options (e.g. short, withValue, and multiples) while creating a path forward for introducing more configuration options in the future (e.g. default).

Co-authored-by: Benjamin E. Coe <bencoe@google.com>
Co-authored-by: John Gee <john@ruru.gen.nz>
Co-authored-by: Jordan Harband <ljharb@gmail.com>
  • Loading branch information
4 people authored Mar 2, 2022
1 parent 81eca14 commit b412095
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 115 deletions.
53 changes: 28 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ It is exceedingly difficult to provide an API which would both be friendly to th
- [🙌 Contributing](#-contributing)
- [💡 `process.mainArgs` Proposal](#-processmainargs-proposal)
- [Implementation:](#implementation)
- [💡 `util.parseArgs(argv)` Proposal](#-utilparseargsargv-proposal)
- [💡 `util.parseArgs([config])` Proposal](#-utilparseargsconfig-proposal)
- [📃 Examples](#-examples)
- [F.A.Qs](#faqs)

Expand Down Expand Up @@ -74,19 +74,16 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2)
----
## 💡 `util.parseArgs([argv][, options])` Proposal
## 💡 `util.parseArgs([config])` Proposal
* `argv` {string[]} (Optional) Array of argument strings; defaults
to [`process.mainArgs`](process_argv)
* `options` {Object} (Optional) The `options` parameter is an
* `config` {Object} (Optional) The `config` parameter is an
object supporting the following properties:
* `withValue` {string[]} (Optional) An `Array` of argument
strings which expect a value to be defined in `argv` (see [Options][]
for details)
* `multiples` {string[]} (Optional) An `Array` of argument
strings which, when appearing multiple times in `argv`, will be concatenated
into an `Array`
* `short` {Object} (Optional) An `Object` of key, value pairs of strings which map a "short" alias to an argument; When appearing multiples times in `argv`; Respects `withValue` & `multiples`
* `args` {string[]} (Optional) Array of argument strings; defaults
to [`process.mainArgs`](process_argv)
* `options` {Object} (Optional) An object describing the known options to look for in `args`; `options` keys are the long names of the known options, and the values are objects with the following properties:
* `type` {'string'|'boolean'} (Optional) Type of known option; defaults to `'boolean'`;
* `multiple` {boolean} (Optional) If true, when appearing one or more times in `args`, results are collected in an `Array`
* `short` {string} (Optional) A single character alias for an option; When appearing one or more times in `args`; Respects the `multiple` configuration
* `strict` {Boolean} (Optional) A `Boolean` on wheather or not to throw an error when unknown args are encountered
* Returns: {Object} An object having properties:
* `flags` {Object}, having properties and `Boolean` values corresponding to parsed options passed
Expand All @@ -104,9 +101,9 @@ const { parseArgs } = require('@pkgjs/parseargs');
```js
// unconfigured
const { parseArgs } = require('@pkgjs/parseargs');
const argv = ['-f', '--foo=a', '--bar', 'b'];
const args = ['-f', '--foo=a', '--bar', 'b'];
const options = {};
const { flags, values, positionals } = parseArgs(argv, options);
const { flags, values, positionals } = parseArgs({ args, options });
// flags = { f: true, bar: true }
// values = { foo: 'a' }
// positionals = ['b']
Expand All @@ -115,25 +112,29 @@ const { flags, values, positionals } = parseArgs(argv, options);
```js
const { parseArgs } = require('@pkgjs/parseargs');
// withValue
const argv = ['-f', '--foo=a', '--bar', 'b'];
const args = ['-f', '--foo=a', '--bar', 'b'];
const options = {
withValue: ['bar']
foo: {
type: 'string',
},
};
const { flags, values, positionals } = parseArgs(argv, options);
const { flags, values, positionals } = parseArgs({ args, options });
// flags = { f: true }
// values = { foo: 'a', bar: 'b' }
// positionals = []
```
```js
const { parseArgs } = require('@pkgjs/parseargs');
// withValue & multiples
const argv = ['-f', '--foo=a', '--foo', 'b'];
// withValue & multiple
const args = ['-f', '--foo=a', '--foo', 'b'];
const options = {
withValue: ['foo'],
multiples: ['foo']
foo: {
type: 'string',
multiple: true,
},
};
const { flags, values, positionals } = parseArgs(argv, options);
const { flags, values, positionals } = parseArgs({ args, options });
// flags = { f: true }
// values = { foo: ['a', 'b'] }
// positionals = []
Expand All @@ -142,11 +143,13 @@ const { flags, values, positionals } = parseArgs(argv, options);
```js
const { parseArgs } = require('@pkgjs/parseargs');
// shorts
const argv = ['-f', 'b'];
const args = ['-f', 'b'];
const options = {
short: { f: 'foo' }
foo: {
short: 'f',
},
};
const { flags, values, positionals } = parseArgs(argv, options);
const { flags, values, positionals } = parseArgs({ args, options });
// flags = { foo: true }
// values = {}
// positionals = ['b']
Expand Down
98 changes: 62 additions & 36 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

const {
ArrayPrototypeConcat,
ArrayPrototypeIncludes,
ArrayPrototypeFind,
ArrayPrototypeForEach,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ArrayPrototypePush,
ObjectHasOwn,
ObjectEntries,
StringPrototypeCharAt,
StringPrototypeIncludes,
StringPrototypeIndexOf,
Expand All @@ -16,7 +18,10 @@ const {

const {
validateArray,
validateObject
validateObject,
validateString,
validateUnion,
validateBoolean,
} = require('./validators');

function getMainArgs() {
Expand Down Expand Up @@ -53,41 +58,57 @@ function getMainArgs() {
return ArrayPrototypeSlice(process.argv, 2);
}

function storeOptionValue(parseOptions, option, value, result) {
const multiple = parseOptions.multiples &&
ArrayPrototypeIncludes(parseOptions.multiples, option);
function storeOptionValue(options, longOption, value, result) {
const optionConfig = options[longOption] || {};

// Flags
result.flags[option] = true;
result.flags[longOption] = true;

// Values
if (multiple) {
if (optionConfig.multiple) {
// Always store value in array, including for flags.
// result.values[option] starts out not present,
// result.values[longOption] starts out not present,
// first value is added as new array [newValue],
// subsequent values are pushed to existing array.
const usedAsFlag = value === undefined;
const newValue = usedAsFlag ? true : value;
if (result.values[option] !== undefined)
ArrayPrototypePush(result.values[option], newValue);
if (result.values[longOption] !== undefined)
ArrayPrototypePush(result.values[longOption], newValue);
else
result.values[option] = [newValue];
result.values[longOption] = [newValue];
} else {
result.values[option] = value;
result.values[longOption] = value;
}
}

const parseArgs = (
argv = getMainArgs(),
const parseArgs = ({
args = getMainArgs(),
options = {}
) => {
validateArray(argv, 'argv');
} = {}) => {
validateArray(args, 'args');
validateObject(options, 'options');
for (const key of ['withValue', 'multiples']) {
if (ObjectHasOwn(options, key)) {
validateArray(options[key], `options.${key}`);
ArrayPrototypeForEach(
ObjectEntries(options),
([longOption, optionConfig]) => {
validateObject(optionConfig, `options.${longOption}`);

if (ObjectHasOwn(optionConfig, 'type')) {
validateUnion(optionConfig.type, `options.${longOption}.type`, ['string', 'boolean']);
}

if (ObjectHasOwn(optionConfig, 'short')) {
const shortOption = optionConfig.short;
validateString(shortOption, `options.${longOption}.short`);
if (shortOption.length !== 1) {
throw new Error(`options.${longOption}.short must be a single character, got '${shortOption}'`);
}
}

if (ObjectHasOwn(optionConfig, 'multiple')) {
validateBoolean(optionConfig.multiple, `options.${longOption}.multiple`);
}
}
}
);

const result = {
flags: {},
Expand All @@ -96,8 +117,8 @@ const parseArgs = (
};

let pos = 0;
while (pos < argv.length) {
let arg = argv[pos];
while (pos < args.length) {
let arg = args[pos];

if (StringPrototypeStartsWith(arg, '-')) {
if (arg === '-') {
Expand All @@ -110,30 +131,36 @@ const parseArgs = (
// and is returned verbatim
result.positionals = ArrayPrototypeConcat(
result.positionals,
ArrayPrototypeSlice(argv, ++pos)
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 short = StringPrototypeCharAt(arg, i);
const shortOption = StringPrototypeCharAt(arg, i);
// Add 'i' to 'pos' such that short options are parsed in order
// of definition:
ArrayPrototypeSplice(argv, pos + (i - 1), 0, `-${short}`);
ArrayPrototypeSplice(args, pos + (i - 1), 0, `-${shortOption}`);
}
}

arg = StringPrototypeCharAt(arg, 1); // short
if (options.short && options.short[arg])
arg = options.short[arg]; // now long!

const [longOption] = ArrayPrototypeFind(
ObjectEntries(options),
([, optionConfig]) => optionConfig.short === arg
) || [];

arg = longOption ?? arg;

// ToDo: later code tests for `=` in arg and wrong for shorts
} else {
arg = StringPrototypeSlice(arg, 2); // remove leading --
}

if (StringPrototypeIncludes(arg, '=')) {
// Store option=value same way independent of `withValue` as:
// Store option=value same way independent of `type: "string"` as:
// - looks like a value, store as a value
// - match the intention of the user
// - preserve information for author to process further
Expand All @@ -143,18 +170,18 @@ const parseArgs = (
StringPrototypeSlice(arg, 0, index),
StringPrototypeSlice(arg, index + 1),
result);
} else if (pos + 1 < argv.length &&
!StringPrototypeStartsWith(argv[pos + 1], '-')
} else if (pos + 1 < args.length &&
!StringPrototypeStartsWith(args[pos + 1], '-')
) {
// withValue option should also support setting values when '=
// `type: "string"` option should also support setting values when '='
// isn't used ie. both --foo=b and --foo b should work

// If withValue option is specified, take next position argument as
// value and then increment pos so that we don't re-evaluate that
// If `type: "string"` option is specified, take next position argument
// as value and then increment pos so that we don't re-evaluate that
// arg, else set value as undefined ie. --foo b --bar c, after setting
// b as the value for foo, evaluate --bar next and skip 'b'
const val = options.withValue &&
ArrayPrototypeIncludes(options.withValue, arg) ? argv[++pos] :
const val = options[arg] && options[arg].type === 'string' ?
args[++pos] :
undefined;
storeOptionValue(options, arg, val, result);
} else {
Expand All @@ -163,7 +190,6 @@ const parseArgs = (
// save value as undefined
storeOptionValue(options, arg, undefined, result);
}

} else {
// Arguments without a dash prefix are considered "positional"
ArrayPrototypePush(result.positionals, arg);
Expand Down
Loading

0 comments on commit b412095

Please sign in to comment.