Skip to content

feat(asc): Add options merge algorithm for use by asconfig #1343

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

Merged
merged 8 commits into from
Jun 17, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
6 changes: 4 additions & 2 deletions cli/asc.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@
" tail-calls Tail call operations.",
" multi-value Multi value types."
],
"type": "S"
"type": "S",
"mutuallyExclusive": "disable"
},
"disable": {
"category": "Features",
Expand All @@ -198,7 +199,8 @@
" mutable-globals Mutable global imports and exports.",
""
],
"type": "S"
"type": "S",
"mutuallyExclusive": "enable"
},
"use": {
"category": "Features",
Expand Down
21 changes: 15 additions & 6 deletions cli/util/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
* @license Apache-2.0
*/

/** A set of options. */
export interface OptionSet {
[key: string]: number | string
}

/** Command line option description. */
export interface OptionDescription {
/** Textual description. */
description?: string | string[],
/** Data type. One of (b)oolean [default], (i)nteger, (f)loat or (s)tring. Uppercase means multiple values. */
type?: "b" | "i" | "f" | "s" | "I" | "F" | "S",
/** Substituted options, if any. */
value?: { [key: string]: number | string },
value?: OptionSet,
/** Short alias, if any. */
alias?: string
/** The default value, if any. */
Expand All @@ -27,19 +32,17 @@ interface Config {
/** Parsing result. */
interface Result {
/** Parsed options. */
options: { [key: string]: number | string },
options: OptionSet,
/** Unknown options. */
unknown: string[],
/** Normal arguments. */
arguments: string[],
/** Trailing arguments. */
trailing: string[],
/** Provided arguments from the cli. */
provided: Set<string>
trailing: string[]
}

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

/** Help formatting options. */
interface HelpOptions {
Expand All @@ -53,3 +56,9 @@ interface HelpOptions {

/** Generates the help text for the specified configuration. */
export function help(config: Config, options?: HelpOptions): string;

/** Merges two sets of options into one, preferring the current over the parent set. */
export function merge(config: Config, currentOptions: OptionSet, parentOptions: OptionSet): OptionSet;

/** Populates default values on a parsed options result. */
export function addDefaults(config: Config, options: OptionSet): OptionSet;
86 changes: 75 additions & 11 deletions cli/util/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ const colorsUtil = require("./colors");
// S | string array

/** Parses the specified command line arguments according to the given configuration. */
function parse(argv, config) {
function parse(argv, config, propagateDefaults = true) {
var options = {};
var unknown = [];
var args = [];
var trailing = [];
var provided = new Set();

// make an alias map and initialize defaults
var aliases = {};
Expand Down Expand Up @@ -54,13 +53,15 @@ function parse(argv, config) {
else { args.push(arg); continue; } // argument
}
if (option) {
if (option.type == null || option.type === "b") {
options[key] = true; // flag
provided.add(key);
if (option.value) {
// alias setting fixed values
Object.keys(option.value).forEach(k => options[k] = option.value[k]);
} else if (option.type == null || option.type === "b") {
// boolean flag not taking a value
options[key] = true;
} else {
// the argument was provided
if (i + 1 < argv.length && argv[i + 1].charCodeAt(0) != 45) { // present
provided.add(key);
if (i + 1 < argv.length && argv[i + 1].charCodeAt(0) != 45) {
// non-boolean with given value
switch (option.type) {
case "i": options[key] = parseInt(argv[++i], 10); break;
case "I": options[key] = (options[key] || []).concat(parseInt(argv[++i], 10)); break;
Expand All @@ -70,7 +71,8 @@ function parse(argv, config) {
case "S": options[key] = (options[key] || []).concat(argv[++i].split(",")); break;
default: unknown.push(arg); --i;
}
} else { // omitted
} else {
// non-boolean with omitted value
switch (option.type) {
case "i":
case "f": options[key] = option.default || 0; break;
Expand All @@ -82,12 +84,12 @@ function parse(argv, config) {
}
}
}
if (option.value) Object.keys(option.value).forEach(k => options[k] = option.value[k]);
} else unknown.push(arg);
}
while (i < k) trailing.push(argv[i++]); // trailing
if (propagateDefaults) addDefaults(config, options);

return { options, unknown, arguments: args, trailing, provided };
return { options, unknown, arguments: args, trailing };
}

exports.parse = parse;
Expand Down Expand Up @@ -138,3 +140,65 @@ function help(config, options) {
}

exports.help = help;

/** Merges two sets of options into one, preferring the current over the parent set. */
function merge(config, currentOptions, parentOptions) {
const mergedOptions = {};
for (const [key, { mutuallyExclusive }] of Object.entries(config)) {
if (currentOptions[key] == null) {
if (parentOptions[key] != null) {
// only parent value present
if (Array.isArray(parentOptions[key])) {
let exclude;
if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) {
mergedOptions[key] = parentOptions[key].filter(value => !exclude.includes(value));
} else {
mergedOptions[key] = parentOptions[key].slice();
}
} else {
mergedOptions[key] = parentOptions[key];
}
}
} else if (parentOptions[key] == null) {
// only current value present
if (Array.isArray(currentOptions[key])) {
mergedOptions[key] = currentOptions[key].slice();
} else {
mergedOptions[key] = currentOptions[key];
}
} else {
// both current and parent values present
if (Array.isArray(currentOptions[key])) {
let exclude;
if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) {
mergedOptions[key] = [
...currentOptions[key],
...parentOptions[key].filter(value => !currentOptions[key].includes(value) && !exclude.includes(value))
];
} else {
mergedOptions[key] = [
...currentOptions[key],
...parentOptions[key].filter(value => !currentOptions[key].includes(value)) // dedup
];
}
} else {
mergedOptions[key] = currentOptions[key];
}
}
}
return mergedOptions;
}

exports.merge = merge;

/** Populates default values on a parsed options result. */
function addDefaults(config, options) {
for (const [key, { default: defaultValue }] of Object.entries(config)) {
if (options[key] == null && defaultValue != null) {
options[key] = defaultValue;
}
}
return options;
}

exports.addDefaults = addDefaults;
49 changes: 49 additions & 0 deletions tests/cli/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const assert = require("assert");
const optionsUtil = require("../../cli/util/options");

const config = {
"enable": {
"type": "S",
"mutuallyExclusive": "disable"
},
"disable": {
"type": "S",
"mutuallyExclusive": "enable"
},
"other": {
"type": "S",
"default": ["x"]
}
};

// Present in both should concat
var merged = optionsUtil.merge(config, { enable: ["a"] }, { enable: ["b"] });
assert.deepEqual(merged.enable, ["a", "b"]);

merged = optionsUtil.merge(config, { enable: ["a"] }, { enable: ["a", "b"] });
assert.deepEqual(merged.enable, ["a", "b"]);

// Mutually exclusive should exclude
merged = optionsUtil.merge(config, { enable: ["a", "b"] }, { disable: ["a", "c"] });
assert.deepEqual(merged.enable, ["a", "b"]);
assert.deepEqual(merged.disable, ["c"]);

merged = optionsUtil.merge(config, { disable: ["a", "b"] }, { enable: ["a", "c"] });
assert.deepEqual(merged.enable, ["c"]);
assert.deepEqual(merged.disable, ["a", "b"]);

// Populating defaults should work after the fact
merged = optionsUtil.addDefaults(config, {});
assert.deepEqual(merged.other, ["x"]);

merged = optionsUtil.addDefaults(config, { other: ["y"] });
assert.deepEqual(merged.other, ["y"]);

// Complete usage test
var result = optionsUtil.parse(["--enable", "a", "--disable", "b"], config, false);
merged = optionsUtil.merge(config, result.options, { enable: ["b", "c"] });
merged = optionsUtil.merge(config, merged, { disable: ["a", "d"] });
optionsUtil.addDefaults(config, merged);
assert.deepEqual(merged.enable, ["a", "c"]);
assert.deepEqual(merged.disable, ["b", "d"]);
assert.deepEqual(merged.other, ["x"]);