Skip to content

Commit e5ca4ee

Browse files
committed
Add options merge algorithm for use by asconfig
1 parent 6dce0f2 commit e5ca4ee

File tree

4 files changed

+147
-17
lines changed

4 files changed

+147
-17
lines changed

cli/asc.json

Lines changed: 4 additions & 2 deletions
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",

cli/util/options.d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,11 @@ interface Result {
3333
/** Normal arguments. */
3434
arguments: string[],
3535
/** Trailing arguments. */
36-
trailing: string[],
37-
/** Provided arguments from the cli. */
38-
provided: Set<string>
36+
trailing: string[]
3937
}
4038

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

4442
/** Help formatting options. */
4543
interface HelpOptions {
@@ -53,3 +51,9 @@ interface HelpOptions {
5351

5452
/** Generates the help text for the specified configuration. */
5553
export function help(config: Config, options?: HelpOptions): string;
54+
55+
/** Populates default values on a parsed options result. */
56+
export function addDefaults(config: Config, options: { [key: string]: number | string });
57+
58+
/** Merges two sets of options into one, preferring the newer set. */
59+
export function merge(config: Config, newerOptions: { [key: string]: number | string }, olderOptions: { [key: string]: number | string });

cli/util/options.js

Lines changed: 86 additions & 11 deletions
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, populateDefaults = 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 (populateDefaults) 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,76 @@ function help(config, options) {
138140
}
139141

140142
exports.help = help;
143+
144+
/** Merges two sets of options into one, preferring the current over the parent set. */
145+
function merge(config, currentOptions, parentOptions) {
146+
const mergedOptions = {};
147+
for (const [key, { mutuallyExclusive }] of Object.entries(config)) {
148+
if (currentOptions[key] == null) {
149+
if (parentOptions[key] != null) {
150+
// only parent value present
151+
if (Array.isArray(parentOptions[key])) {
152+
if (mutuallyExclusive) {
153+
const exclude = currentOptions[mutuallyExclusive];
154+
if (exclude) {
155+
mergedOptions[key] = parentOptions[key].filter(value => !exclude.includes(value));
156+
} else {
157+
mergedOptions[key] = parentOptions[key].slice();
158+
}
159+
} else {
160+
mergedOptions[key] = parentOptions[key].slice();
161+
}
162+
} else {
163+
mergedOptions[key] = parentOptions[key];
164+
}
165+
}
166+
} else if (parentOptions[key] == null) {
167+
// only current value present
168+
if (Array.isArray(currentOptions[key])) {
169+
mergedOptions[key] = currentOptions[key].slice();
170+
} else {
171+
mergedOptions[key] = currentOptions[key];
172+
}
173+
} else {
174+
// both current and parent values present
175+
if (Array.isArray(currentOptions[key])) {
176+
if (mutuallyExclusive) {
177+
const exclude = currentOptions[mutuallyExclusive];
178+
if (exclude) {
179+
mergedOptions[key] = [
180+
...currentOptions[key],
181+
...parentOptions[key].filter(value => !currentOptions[key].includes(value) && !exclude.includes(value))
182+
];
183+
} else {
184+
mergedOptions[key] = [
185+
...currentOptions[key],
186+
...parentOptions[key].filter(value => !currentOptions[key].includes(value)) // dedup
187+
];
188+
}
189+
} else {
190+
mergedOptions[key] = [
191+
...currentOptions[key],
192+
...parentOptions[key].filter(value => !currentOptions[key].includes(value)) // dedup
193+
];
194+
}
195+
} else {
196+
mergedOptions[key] = currentOptions[key];
197+
}
198+
}
199+
}
200+
return mergedOptions;
201+
}
202+
203+
exports.merge = merge;
204+
205+
/** Populates default values on a parsed options result. */
206+
function addDefaults(config, options) {
207+
for (const [key, { default: defaultValue }] of Object.entries(config)) {
208+
if (options[key] == null && defaultValue != null) {
209+
options[key] = defaultValue;
210+
}
211+
}
212+
return options;
213+
}
214+
215+
exports.addDefaults = addDefaults;

tests/cli/options.js

Lines changed: 49 additions & 0 deletions
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)