Skip to content

Commit 3163abb

Browse files
authored
feat: Add options merge algorithm for use by asconfig (#1343)
1 parent f81250a commit 3163abb

File tree

4 files changed

+171
-19
lines changed

4 files changed

+171
-19
lines changed

Diff for: cli/asc.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@
188188
" tail-calls Tail call operations.",
189189
" multi-value Multi value types."
190190
],
191-
"type": "S"
191+
"type": "S",
192+
"mutuallyExclusive": "disable"
192193
},
193194
"disable": {
194195
"category": "Features",
@@ -198,7 +199,8 @@
198199
" mutable-globals Mutable global imports and exports.",
199200
""
200201
],
201-
"type": "S"
202+
"type": "S",
203+
"mutuallyExclusive": "enable"
202204
},
203205
"use": {
204206
"category": "Features",

Diff for: cli/util/options.d.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
* @license Apache-2.0
44
*/
55

6+
/** A set of options. */
7+
export interface OptionSet {
8+
[key: string]: number | string
9+
}
10+
611
/** Command line option description. */
712
export interface OptionDescription {
813
/** Textual description. */
914
description?: string | string[],
1015
/** Data type. One of (b)oolean [default], (i)nteger, (f)loat or (s)tring. Uppercase means multiple values. */
1116
type?: "b" | "i" | "f" | "s" | "I" | "F" | "S",
1217
/** Substituted options, if any. */
13-
value?: { [key: string]: number | string },
18+
value?: OptionSet,
1419
/** Short alias, if any. */
1520
alias?: string
1621
/** The default value, if any. */
@@ -27,19 +32,17 @@ interface Config {
2732
/** Parsing result. */
2833
interface Result {
2934
/** Parsed options. */
30-
options: { [key: string]: number | string },
35+
options: OptionSet,
3136
/** Unknown options. */
3237
unknown: string[],
3338
/** Normal arguments. */
3439
arguments: string[],
3540
/** Trailing arguments. */
36-
trailing: string[],
37-
/** Provided arguments from the cli. */
38-
provided: Set<string>
41+
trailing: string[]
3942
}
4043

4144
/** Parses the specified command line arguments according to the given configuration. */
42-
export function parse(argv: string[], config: Config): Result;
45+
export function parse(argv: string[], config: Config, propagateDefaults?: boolean): Result;
4346

4447
/** Help formatting options. */
4548
interface HelpOptions {
@@ -53,3 +56,9 @@ interface HelpOptions {
5356

5457
/** Generates the help text for the specified configuration. */
5558
export function help(config: Config, options?: HelpOptions): string;
59+
60+
/** Merges two sets of options into one, preferring the current over the parent set. */
61+
export function merge(config: Config, currentOptions: OptionSet, parentOptions: OptionSet): OptionSet;
62+
63+
/** Populates default values on a parsed options result. */
64+
export function addDefaults(config: Config, options: OptionSet): OptionSet;

Diff for: cli/util/options.js

+103-11
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ const colorsUtil = require("./colors");
1616
// S | string array
1717

1818
/** Parses the specified command line arguments according to the given configuration. */
19-
function parse(argv, config) {
19+
function parse(argv, config, propagateDefaults = true) {
2020
var options = {};
2121
var unknown = [];
2222
var args = [];
2323
var trailing = [];
24-
var provided = new Set();
2524

2625
// make an alias map and initialize defaults
2726
var aliases = {};
@@ -54,13 +53,15 @@ function parse(argv, config) {
5453
else { args.push(arg); continue; } // argument
5554
}
5655
if (option) {
57-
if (option.type == null || option.type === "b") {
58-
options[key] = true; // flag
59-
provided.add(key);
56+
if (option.value) {
57+
// alias setting fixed values
58+
Object.keys(option.value).forEach(k => options[k] = option.value[k]);
59+
} else if (option.type == null || option.type === "b") {
60+
// boolean flag not taking a value
61+
options[key] = true;
6062
} else {
61-
// the argument was provided
62-
if (i + 1 < argv.length && argv[i + 1].charCodeAt(0) != 45) { // present
63-
provided.add(key);
63+
if (i + 1 < argv.length && argv[i + 1].charCodeAt(0) != 45) {
64+
// non-boolean with given value
6465
switch (option.type) {
6566
case "i": options[key] = parseInt(argv[++i], 10); break;
6667
case "I": options[key] = (options[key] || []).concat(parseInt(argv[++i], 10)); break;
@@ -70,7 +71,8 @@ function parse(argv, config) {
7071
case "S": options[key] = (options[key] || []).concat(argv[++i].split(",")); break;
7172
default: unknown.push(arg); --i;
7273
}
73-
} else { // omitted
74+
} else {
75+
// non-boolean with omitted value
7476
switch (option.type) {
7577
case "i":
7678
case "f": options[key] = option.default || 0; break;
@@ -82,12 +84,12 @@ function parse(argv, config) {
8284
}
8385
}
8486
}
85-
if (option.value) Object.keys(option.value).forEach(k => options[k] = option.value[k]);
8687
} else unknown.push(arg);
8788
}
8889
while (i < k) trailing.push(argv[i++]); // trailing
90+
if (propagateDefaults) addDefaults(config, options);
8991

90-
return { options, unknown, arguments: args, trailing, provided };
92+
return { options, unknown, arguments: args, trailing };
9193
}
9294

9395
exports.parse = parse;
@@ -138,3 +140,93 @@ function help(config, options) {
138140
}
139141

140142
exports.help = help;
143+
144+
/** Sanitizes an option value to be a valid value of the option's type. */
145+
function sanitizeValue(value, type) {
146+
if (value != null) {
147+
switch (type) {
148+
case undefined:
149+
case "b": return Boolean(value);
150+
case "i": return Math.trunc(value) || 0;
151+
case "f": return Number(value) || 0;
152+
case "s": return String(value);
153+
case "I": {
154+
if (!Array.isArray(value)) value = [ value ];
155+
return value.map(v => Math.trunc(v) || 0);
156+
}
157+
case "F": {
158+
if (!Array.isArray(value)) value = [ value ];
159+
return value.map(v => Number(v) || 0);
160+
}
161+
case "S": {
162+
if (!Array.isArray(value)) value = [ value ];
163+
return value.map(String);
164+
}
165+
}
166+
}
167+
return undefined;
168+
}
169+
170+
/** Merges two sets of options into one, preferring the current over the parent set. */
171+
function merge(config, currentOptions, parentOptions) {
172+
const mergedOptions = {};
173+
for (const [key, { type, mutuallyExclusive }] of Object.entries(config)) {
174+
let currentValue = sanitizeValue(currentOptions[key], type);
175+
let parentValue = sanitizeValue(parentOptions[key], type);
176+
if (currentValue == null) {
177+
if (parentValue != null) {
178+
// only parent value present
179+
if (Array.isArray(parentValue)) {
180+
let exclude;
181+
if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) {
182+
mergedOptions[key] = parentValue.filter(value => !exclude.includes(value));
183+
} else {
184+
mergedOptions[key] = parentValue.slice();
185+
}
186+
} else {
187+
mergedOptions[key] = parentValue;
188+
}
189+
}
190+
} else if (parentValue == null) {
191+
// only current value present
192+
if (Array.isArray(currentValue)) {
193+
mergedOptions[key] = currentValue.slice();
194+
} else {
195+
mergedOptions[key] = currentValue;
196+
}
197+
} else {
198+
// both current and parent values present
199+
if (Array.isArray(currentValue)) {
200+
let exclude;
201+
if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) {
202+
mergedOptions[key] = [
203+
...currentValue,
204+
...parentValue.filter(value => !currentValue.includes(value) && !exclude.includes(value))
205+
];
206+
} else {
207+
mergedOptions[key] = [
208+
...currentValue,
209+
...parentValue.filter(value => !currentValue.includes(value)) // dedup
210+
];
211+
}
212+
} else {
213+
mergedOptions[key] = currentValue;
214+
}
215+
}
216+
}
217+
return mergedOptions;
218+
}
219+
220+
exports.merge = merge;
221+
222+
/** Populates default values on a parsed options result. */
223+
function addDefaults(config, options) {
224+
for (const [key, { default: defaultValue }] of Object.entries(config)) {
225+
if (options[key] == null && defaultValue != null) {
226+
options[key] = defaultValue;
227+
}
228+
}
229+
return options;
230+
}
231+
232+
exports.addDefaults = addDefaults;

Diff for: tests/cli/options.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const assert = require("assert");
2+
const optionsUtil = require("../../cli/util/options");
3+
4+
const config = {
5+
"enable": {
6+
"type": "S",
7+
"mutuallyExclusive": "disable"
8+
},
9+
"disable": {
10+
"type": "S",
11+
"mutuallyExclusive": "enable"
12+
},
13+
"other": {
14+
"type": "S",
15+
"default": ["x"]
16+
}
17+
};
18+
19+
// Present in both should concat
20+
var merged = optionsUtil.merge(config, { enable: ["a"] }, { enable: ["b"] });
21+
assert.deepEqual(merged.enable, ["a", "b"]);
22+
23+
merged = optionsUtil.merge(config, { enable: ["a"] }, { enable: ["a", "b"] });
24+
assert.deepEqual(merged.enable, ["a", "b"]);
25+
26+
// Mutually exclusive should exclude
27+
merged = optionsUtil.merge(config, { enable: ["a", "b"] }, { disable: ["a", "c"] });
28+
assert.deepEqual(merged.enable, ["a", "b"]);
29+
assert.deepEqual(merged.disable, ["c"]);
30+
31+
merged = optionsUtil.merge(config, { disable: ["a", "b"] }, { enable: ["a", "c"] });
32+
assert.deepEqual(merged.enable, ["c"]);
33+
assert.deepEqual(merged.disable, ["a", "b"]);
34+
35+
// Populating defaults should work after the fact
36+
merged = optionsUtil.addDefaults(config, {});
37+
assert.deepEqual(merged.other, ["x"]);
38+
39+
merged = optionsUtil.addDefaults(config, { other: ["y"] });
40+
assert.deepEqual(merged.other, ["y"]);
41+
42+
// Complete usage test
43+
var result = optionsUtil.parse(["--enable", "a", "--disable", "b"], config, false);
44+
merged = optionsUtil.merge(config, result.options, { enable: ["b", "c"] });
45+
merged = optionsUtil.merge(config, merged, { disable: ["a", "d"] });
46+
optionsUtil.addDefaults(config, merged);
47+
assert.deepEqual(merged.enable, ["a", "c"]);
48+
assert.deepEqual(merged.disable, ["b", "d"]);
49+
assert.deepEqual(merged.other, ["x"]);

0 commit comments

Comments
 (0)