Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: restructure configuration to take options bag #63

Merged
merged 23 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
697493f
feat: Restructure options API
aaronccasanova Feb 9, 2022
9e42db0
chore: Remove debug comments
aaronccasanova Feb 9, 2022
36ef68c
chore: Remove debug comments
aaronccasanova Feb 9, 2022
67c0b03
chore: Alias args to argv to introduce less changes
aaronccasanova Feb 9, 2022
082bec5
feat: Replace option with
Feb 12, 2022
0909008
docs: Update README to reflect updated implementation
Feb 12, 2022
042480b
chore: Revert args options to argv
Feb 12, 2022
e44c46c
docs: Update README from PR feedback
aaronccasanova Feb 24, 2022
4063751
Merge branch 'main' of https://github.com/aaronccasanova/parseargs in…
aaronccasanova Feb 26, 2022
a50e940
chore: Reduce changes
aaronccasanova Feb 26, 2022
062bdc9
chore: Tidy up naming conventions
aaronccasanova Feb 26, 2022
28ae7b8
Merge branch 'main' into feat/restructure-options-api
bcoe Feb 27, 2022
4436ef6
Rename argv propery to args (#2)
shadowspawn Feb 28, 2022
dc58a4a
feat: Rename multiples to multiple
aaronccasanova Feb 28, 2022
14866a3
fix: Update union error formatting
aaronccasanova Feb 28, 2022
b0b71c4
feat: Guard against short options longer than 1 char
aaronccasanova Feb 28, 2022
92811f4
Update validators.js
aaronccasanova Feb 28, 2022
0d5a35e
chore: Add missing primordial
aaronccasanova Feb 28, 2022
ecf1fb0
fix: Replace for..of with primordial
aaronccasanova Mar 1, 2022
e0d0c33
fix: Update validator to use primoridial
aaronccasanova Mar 1, 2022
50d4f4d
fix: Clear up ambiguity around long and short option usage
aaronccasanova Mar 1, 2022
b41fd9b
fix: Update error formatting
aaronccasanova Mar 1, 2022
f533a98
Update index.js
aaronccasanova Mar 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
aaronccasanova marked this conversation as resolved.
Show resolved Hide resolved
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(
aaronccasanova marked this conversation as resolved.
Show resolved Hide resolved
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