Skip to content

Commit b412095

Browse files
aaronccasanovabcoeshadowspawnljharb
authored
refactor!: restructure configuration to take options bag (#63)
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 <[email protected]> Co-authored-by: John Gee <[email protected]> Co-authored-by: Jordan Harband <[email protected]>
1 parent 81eca14 commit b412095

File tree

4 files changed

+182
-115
lines changed

4 files changed

+182
-115
lines changed

README.md

+28-25
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ It is exceedingly difficult to provide an API which would both be friendly to th
2828
- [🙌 Contributing](#-contributing)
2929
- [💡 `process.mainArgs` Proposal](#-processmainargs-proposal)
3030
- [Implementation:](#implementation)
31-
- [💡 `util.parseArgs(argv)` Proposal](#-utilparseargsargv-proposal)
31+
- [💡 `util.parseArgs([config])` Proposal](#-utilparseargsconfig-proposal)
3232
- [📃 Examples](#-examples)
3333
- [F.A.Qs](#faqs)
3434

@@ -74,19 +74,16 @@ process.mainArgs = process.argv.slice(process._exec ? 1 : 2)
7474

7575
----
7676

77-
## 💡 `util.parseArgs([argv][, options])` Proposal
77+
## 💡 `util.parseArgs([config])` Proposal
7878

79-
* `argv` {string[]} (Optional) Array of argument strings; defaults
80-
to [`process.mainArgs`](process_argv)
81-
* `options` {Object} (Optional) The `options` parameter is an
79+
* `config` {Object} (Optional) The `config` parameter is an
8280
object supporting the following properties:
83-
* `withValue` {string[]} (Optional) An `Array` of argument
84-
strings which expect a value to be defined in `argv` (see [Options][]
85-
for details)
86-
* `multiples` {string[]} (Optional) An `Array` of argument
87-
strings which, when appearing multiple times in `argv`, will be concatenated
88-
into an `Array`
89-
* `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`
81+
* `args` {string[]} (Optional) Array of argument strings; defaults
82+
to [`process.mainArgs`](process_argv)
83+
* `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:
84+
* `type` {'string'|'boolean'} (Optional) Type of known option; defaults to `'boolean'`;
85+
* `multiple` {boolean} (Optional) If true, when appearing one or more times in `args`, results are collected in an `Array`
86+
* `short` {string} (Optional) A single character alias for an option; When appearing one or more times in `args`; Respects the `multiple` configuration
9087
* `strict` {Boolean} (Optional) A `Boolean` on wheather or not to throw an error when unknown args are encountered
9188
* Returns: {Object} An object having properties:
9289
* `flags` {Object}, having properties and `Boolean` values corresponding to parsed options passed
@@ -104,9 +101,9 @@ const { parseArgs } = require('@pkgjs/parseargs');
104101
```js
105102
// unconfigured
106103
const { parseArgs } = require('@pkgjs/parseargs');
107-
const argv = ['-f', '--foo=a', '--bar', 'b'];
104+
const args = ['-f', '--foo=a', '--bar', 'b'];
108105
const options = {};
109-
const { flags, values, positionals } = parseArgs(argv, options);
106+
const { flags, values, positionals } = parseArgs({ args, options });
110107
// flags = { f: true, bar: true }
111108
// values = { foo: 'a' }
112109
// positionals = ['b']
@@ -115,25 +112,29 @@ const { flags, values, positionals } = parseArgs(argv, options);
115112
```js
116113
const { parseArgs } = require('@pkgjs/parseargs');
117114
// withValue
118-
const argv = ['-f', '--foo=a', '--bar', 'b'];
115+
const args = ['-f', '--foo=a', '--bar', 'b'];
119116
const options = {
120-
withValue: ['bar']
117+
foo: {
118+
type: 'string',
119+
},
121120
};
122-
const { flags, values, positionals } = parseArgs(argv, options);
121+
const { flags, values, positionals } = parseArgs({ args, options });
123122
// flags = { f: true }
124123
// values = { foo: 'a', bar: 'b' }
125124
// positionals = []
126125
```
127126

128127
```js
129128
const { parseArgs } = require('@pkgjs/parseargs');
130-
// withValue & multiples
131-
const argv = ['-f', '--foo=a', '--foo', 'b'];
129+
// withValue & multiple
130+
const args = ['-f', '--foo=a', '--foo', 'b'];
132131
const options = {
133-
withValue: ['foo'],
134-
multiples: ['foo']
132+
foo: {
133+
type: 'string',
134+
multiple: true,
135+
},
135136
};
136-
const { flags, values, positionals } = parseArgs(argv, options);
137+
const { flags, values, positionals } = parseArgs({ args, options });
137138
// flags = { f: true }
138139
// values = { foo: ['a', 'b'] }
139140
// positionals = []
@@ -142,11 +143,13 @@ const { flags, values, positionals } = parseArgs(argv, options);
142143
```js
143144
const { parseArgs } = require('@pkgjs/parseargs');
144145
// shorts
145-
const argv = ['-f', 'b'];
146+
const args = ['-f', 'b'];
146147
const options = {
147-
short: { f: 'foo' }
148+
foo: {
149+
short: 'f',
150+
},
148151
};
149-
const { flags, values, positionals } = parseArgs(argv, options);
152+
const { flags, values, positionals } = parseArgs({ args, options });
150153
// flags = { foo: true }
151154
// values = {}
152155
// positionals = ['b']

index.js

+62-36
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
const {
44
ArrayPrototypeConcat,
5-
ArrayPrototypeIncludes,
5+
ArrayPrototypeFind,
6+
ArrayPrototypeForEach,
67
ArrayPrototypeSlice,
78
ArrayPrototypeSplice,
89
ArrayPrototypePush,
910
ObjectHasOwn,
11+
ObjectEntries,
1012
StringPrototypeCharAt,
1113
StringPrototypeIncludes,
1214
StringPrototypeIndexOf,
@@ -16,7 +18,10 @@ const {
1618

1719
const {
1820
validateArray,
19-
validateObject
21+
validateObject,
22+
validateString,
23+
validateUnion,
24+
validateBoolean,
2025
} = require('./validators');
2126

2227
function getMainArgs() {
@@ -53,41 +58,57 @@ function getMainArgs() {
5358
return ArrayPrototypeSlice(process.argv, 2);
5459
}
5560

56-
function storeOptionValue(parseOptions, option, value, result) {
57-
const multiple = parseOptions.multiples &&
58-
ArrayPrototypeIncludes(parseOptions.multiples, option);
61+
function storeOptionValue(options, longOption, value, result) {
62+
const optionConfig = options[longOption] || {};
5963

6064
// Flags
61-
result.flags[option] = true;
65+
result.flags[longOption] = true;
6266

6367
// Values
64-
if (multiple) {
68+
if (optionConfig.multiple) {
6569
// Always store value in array, including for flags.
66-
// result.values[option] starts out not present,
70+
// result.values[longOption] starts out not present,
6771
// first value is added as new array [newValue],
6872
// subsequent values are pushed to existing array.
6973
const usedAsFlag = value === undefined;
7074
const newValue = usedAsFlag ? true : value;
71-
if (result.values[option] !== undefined)
72-
ArrayPrototypePush(result.values[option], newValue);
75+
if (result.values[longOption] !== undefined)
76+
ArrayPrototypePush(result.values[longOption], newValue);
7377
else
74-
result.values[option] = [newValue];
78+
result.values[longOption] = [newValue];
7579
} else {
76-
result.values[option] = value;
80+
result.values[longOption] = value;
7781
}
7882
}
7983

80-
const parseArgs = (
81-
argv = getMainArgs(),
84+
const parseArgs = ({
85+
args = getMainArgs(),
8286
options = {}
83-
) => {
84-
validateArray(argv, 'argv');
87+
} = {}) => {
88+
validateArray(args, 'args');
8589
validateObject(options, 'options');
86-
for (const key of ['withValue', 'multiples']) {
87-
if (ObjectHasOwn(options, key)) {
88-
validateArray(options[key], `options.${key}`);
90+
ArrayPrototypeForEach(
91+
ObjectEntries(options),
92+
([longOption, optionConfig]) => {
93+
validateObject(optionConfig, `options.${longOption}`);
94+
95+
if (ObjectHasOwn(optionConfig, 'type')) {
96+
validateUnion(optionConfig.type, `options.${longOption}.type`, ['string', 'boolean']);
97+
}
98+
99+
if (ObjectHasOwn(optionConfig, 'short')) {
100+
const shortOption = optionConfig.short;
101+
validateString(shortOption, `options.${longOption}.short`);
102+
if (shortOption.length !== 1) {
103+
throw new Error(`options.${longOption}.short must be a single character, got '${shortOption}'`);
104+
}
105+
}
106+
107+
if (ObjectHasOwn(optionConfig, 'multiple')) {
108+
validateBoolean(optionConfig.multiple, `options.${longOption}.multiple`);
109+
}
89110
}
90-
}
111+
);
91112

92113
const result = {
93114
flags: {},
@@ -96,8 +117,8 @@ const parseArgs = (
96117
};
97118

98119
let pos = 0;
99-
while (pos < argv.length) {
100-
let arg = argv[pos];
120+
while (pos < args.length) {
121+
let arg = args[pos];
101122

102123
if (StringPrototypeStartsWith(arg, '-')) {
103124
if (arg === '-') {
@@ -110,30 +131,36 @@ const parseArgs = (
110131
// and is returned verbatim
111132
result.positionals = ArrayPrototypeConcat(
112133
result.positionals,
113-
ArrayPrototypeSlice(argv, ++pos)
134+
ArrayPrototypeSlice(args, ++pos)
114135
);
115136
return result;
116137
} else if (StringPrototypeCharAt(arg, 1) !== '-') {
117138
// Look for shortcodes: -fXzy and expand them to -f -X -z -y:
118139
if (arg.length > 2) {
119140
for (let i = 2; i < arg.length; i++) {
120-
const short = StringPrototypeCharAt(arg, i);
141+
const shortOption = StringPrototypeCharAt(arg, i);
121142
// Add 'i' to 'pos' such that short options are parsed in order
122143
// of definition:
123-
ArrayPrototypeSplice(argv, pos + (i - 1), 0, `-${short}`);
144+
ArrayPrototypeSplice(args, pos + (i - 1), 0, `-${shortOption}`);
124145
}
125146
}
126147

127148
arg = StringPrototypeCharAt(arg, 1); // short
128-
if (options.short && options.short[arg])
129-
arg = options.short[arg]; // now long!
149+
150+
const [longOption] = ArrayPrototypeFind(
151+
ObjectEntries(options),
152+
([, optionConfig]) => optionConfig.short === arg
153+
) || [];
154+
155+
arg = longOption ?? arg;
156+
130157
// ToDo: later code tests for `=` in arg and wrong for shorts
131158
} else {
132159
arg = StringPrototypeSlice(arg, 2); // remove leading --
133160
}
134161

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

152-
// If withValue option is specified, take next position argument as
153-
// value and then increment pos so that we don't re-evaluate that
179+
// If `type: "string"` option is specified, take next position argument
180+
// as value and then increment pos so that we don't re-evaluate that
154181
// arg, else set value as undefined ie. --foo b --bar c, after setting
155182
// b as the value for foo, evaluate --bar next and skip 'b'
156-
const val = options.withValue &&
157-
ArrayPrototypeIncludes(options.withValue, arg) ? argv[++pos] :
183+
const val = options[arg] && options[arg].type === 'string' ?
184+
args[++pos] :
158185
undefined;
159186
storeOptionValue(options, arg, val, result);
160187
} else {
@@ -163,7 +190,6 @@ const parseArgs = (
163190
// save value as undefined
164191
storeOptionValue(options, arg, undefined, result);
165192
}
166-
167193
} else {
168194
// Arguments without a dash prefix are considered "positional"
169195
ArrayPrototypePush(result.positionals, arg);

0 commit comments

Comments
 (0)