From e5ca4ee0c37c673c51b1f01b8e7171ec14149662 Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 05:30:37 +0200 Subject: [PATCH 1/8] Add options merge algorithm for use by asconfig --- cli/asc.json | 6 ++- cli/util/options.d.ts | 12 ++++-- cli/util/options.js | 97 ++++++++++++++++++++++++++++++++++++++----- tests/cli/options.js | 49 ++++++++++++++++++++++ 4 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 tests/cli/options.js diff --git a/cli/asc.json b/cli/asc.json index c675b64b58..fbbe218b70 100644 --- a/cli/asc.json +++ b/cli/asc.json @@ -188,7 +188,8 @@ " tail-calls Tail call operations.", " multi-value Multi value types." ], - "type": "S" + "type": "S", + "mutuallyExclusive": "disable" }, "disable": { "category": "Features", @@ -198,7 +199,8 @@ " mutable-globals Mutable global imports and exports.", "" ], - "type": "S" + "type": "S", + "mutuallyExclusive": "enable" }, "use": { "category": "Features", diff --git a/cli/util/options.d.ts b/cli/util/options.d.ts index 630c496030..f15804c0c6 100644 --- a/cli/util/options.d.ts +++ b/cli/util/options.d.ts @@ -33,13 +33,11 @@ interface Result { /** Normal arguments. */ arguments: string[], /** Trailing arguments. */ - trailing: string[], - /** Provided arguments from the cli. */ - provided: Set + 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, populateDefaults?: boolean): Result; /** Help formatting options. */ interface HelpOptions { @@ -53,3 +51,9 @@ interface HelpOptions { /** Generates the help text for the specified configuration. */ export function help(config: Config, options?: HelpOptions): string; + +/** Populates default values on a parsed options result. */ +export function addDefaults(config: Config, options: { [key: string]: number | string }); + +/** Merges two sets of options into one, preferring the newer set. */ +export function merge(config: Config, newerOptions: { [key: string]: number | string }, olderOptions: { [key: string]: number | string }); diff --git a/cli/util/options.js b/cli/util/options.js index ac20ba68e7..6de748364a 100644 --- a/cli/util/options.js +++ b/cli/util/options.js @@ -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, populateDefaults = true) { var options = {}; var unknown = []; var args = []; var trailing = []; - var provided = new Set(); // make an alias map and initialize defaults var aliases = {}; @@ -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; @@ -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; @@ -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 (populateDefaults) addDefaults(config, options); - return { options, unknown, arguments: args, trailing, provided }; + return { options, unknown, arguments: args, trailing }; } exports.parse = parse; @@ -138,3 +140,76 @@ 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])) { + if (mutuallyExclusive) { + const exclude = currentOptions[mutuallyExclusive]; + if (exclude) { + mergedOptions[key] = parentOptions[key].filter(value => !exclude.includes(value)); + } else { + mergedOptions[key] = parentOptions[key].slice(); + } + } 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])) { + if (mutuallyExclusive) { + const exclude = currentOptions[mutuallyExclusive]; + if (exclude) { + 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], + ...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; diff --git a/tests/cli/options.js b/tests/cli/options.js new file mode 100644 index 0000000000..e24575b876 --- /dev/null +++ b/tests/cli/options.js @@ -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"]); From 3446200a4cd2967b115d5a7002b6e564587f2713 Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 05:44:22 +0200 Subject: [PATCH 2/8] sync parameter names --- cli/util/options.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/util/options.d.ts b/cli/util/options.d.ts index f15804c0c6..d5a5e6d041 100644 --- a/cli/util/options.d.ts +++ b/cli/util/options.d.ts @@ -56,4 +56,4 @@ export function help(config: Config, options?: HelpOptions): string; export function addDefaults(config: Config, options: { [key: string]: number | string }); /** Merges two sets of options into one, preferring the newer set. */ -export function merge(config: Config, newerOptions: { [key: string]: number | string }, olderOptions: { [key: string]: number | string }); +export function merge(config: Config, currentOptions: { [key: string]: number | string }, parentOptions: { [key: string]: number | string }); From 87e3190567c51605a64b348f9f7abc0bfcf705c3 Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 06:04:48 +0200 Subject: [PATCH 3/8] clarify definitions --- cli/util/options.d.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cli/util/options.d.ts b/cli/util/options.d.ts index d5a5e6d041..f0aeedb63e 100644 --- a/cli/util/options.d.ts +++ b/cli/util/options.d.ts @@ -3,6 +3,11 @@ * @license Apache-2.0 */ +/** A set of options. */ +export interface OptionSet { + [key: string]: number | string +} + /** Command line option description. */ export interface OptionDescription { /** Textual description. */ @@ -10,7 +15,7 @@ export interface OptionDescription { /** 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. */ @@ -27,7 +32,7 @@ interface Config { /** Parsing result. */ interface Result { /** Parsed options. */ - options: { [key: string]: number | string }, + options: OptionSet, /** Unknown options. */ unknown: string[], /** Normal arguments. */ @@ -53,7 +58,7 @@ interface HelpOptions { export function help(config: Config, options?: HelpOptions): string; /** Populates default values on a parsed options result. */ -export function addDefaults(config: Config, options: { [key: string]: number | string }); +export function addDefaults(config: Config, options: OptionSet): OptionSet; /** Merges two sets of options into one, preferring the newer set. */ -export function merge(config: Config, currentOptions: { [key: string]: number | string }, parentOptions: { [key: string]: number | string }); +export function merge(config: Config, currentOptions: OptionSet, parentOptions: OptionSet): OptionSet; From 1bad9c168af5a15724cb68dcfdca00c5945c79b3 Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 09:58:40 +0200 Subject: [PATCH 4/8] address review comments --- cli/util/options.d.ts | 8 ++++---- cli/util/options.js | 33 +++++++++++---------------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/cli/util/options.d.ts b/cli/util/options.d.ts index f0aeedb63e..e12013fef3 100644 --- a/cli/util/options.d.ts +++ b/cli/util/options.d.ts @@ -42,7 +42,7 @@ interface Result { } /** Parses the specified command line arguments according to the given configuration. */ -export function parse(argv: string[], config: Config, populateDefaults?: boolean): Result; +export function parse(argv: string[], config: Config, propagateDefaults?: boolean): Result; /** Help formatting options. */ interface HelpOptions { @@ -57,8 +57,8 @@ 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; - -/** Merges two sets of options into one, preferring the newer set. */ -export function merge(config: Config, currentOptions: OptionSet, parentOptions: OptionSet): OptionSet; diff --git a/cli/util/options.js b/cli/util/options.js index 6de748364a..59b2a23b9a 100644 --- a/cli/util/options.js +++ b/cli/util/options.js @@ -16,7 +16,7 @@ const colorsUtil = require("./colors"); // S | string array /** Parses the specified command line arguments according to the given configuration. */ -function parse(argv, config, populateDefaults = true) { +function parse(argv, config, propagateDefaults = true) { var options = {}; var unknown = []; var args = []; @@ -87,7 +87,7 @@ function parse(argv, config, populateDefaults = true) { } else unknown.push(arg); } while (i < k) trailing.push(argv[i++]); // trailing - if (populateDefaults) addDefaults(config, options); + if (propagateDefaults) addDefaults(config, options); return { options, unknown, arguments: args, trailing }; } @@ -149,13 +149,9 @@ function merge(config, currentOptions, parentOptions) { if (parentOptions[key] != null) { // only parent value present if (Array.isArray(parentOptions[key])) { - if (mutuallyExclusive) { - const exclude = currentOptions[mutuallyExclusive]; - if (exclude) { - mergedOptions[key] = parentOptions[key].filter(value => !exclude.includes(value)); - } else { - mergedOptions[key] = parentOptions[key].slice(); - } + let exclude; + if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) { + mergedOptions[key] = parentOptions[key].filter(value => !exclude.includes(value)); } else { mergedOptions[key] = parentOptions[key].slice(); } @@ -173,19 +169,12 @@ function merge(config, currentOptions, parentOptions) { } else { // both current and parent values present if (Array.isArray(currentOptions[key])) { - if (mutuallyExclusive) { - const exclude = currentOptions[mutuallyExclusive]; - if (exclude) { - 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 - ]; - } + 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], From a70fc46e5855c7f4cc65aebae2bddaa704acc4b6 Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 10:59:32 +0200 Subject: [PATCH 5/8] sanitize values --- cli/util/options.js | 62 ++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/cli/util/options.js b/cli/util/options.js index 59b2a23b9a..1f7adfac14 100644 --- a/cli/util/options.js +++ b/cli/util/options.js @@ -141,48 +141,76 @@ function help(config, options) { exports.help = help; +/** Sanitizes an option value to be a valid value of the option's type. */ +function sanitizeValue(value, type) { + if (value != null) { + switch (type) { + case undefined: + case "b": return Boolean(value); + case "i": return +(+value).toFixed(0) || 0; + case "f": return +value || 0; + case "s": return String(value); + case "I": { + if (!Array.isArray(value)) value = [ value ]; + return value.map(v => +(+v).toFixed(0) || 0); + } + case "F": { + if (!Array.isArray(value)) value = [ value ]; + return value.map(v => +v || 0); + } + case "S": { + if (!Array.isArray(value)) value = [ value ]; + return value.map(v => String(v)); + } + } + } + return undefined; +} + /** 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) { + for (const [key, { type, mutuallyExclusive }] of Object.entries(config)) { + let currentValue = sanitizeValue(currentOptions[key], type); + let parentValue = sanitizeValue(parentOptions[key], type); + if (currentValue == null) { + if (parentValue != null) { // only parent value present - if (Array.isArray(parentOptions[key])) { + if (Array.isArray(parentValue)) { let exclude; if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) { - mergedOptions[key] = parentOptions[key].filter(value => !exclude.includes(value)); + mergedOptions[key] = parentValue.filter(value => !exclude.includes(value)); } else { - mergedOptions[key] = parentOptions[key].slice(); + mergedOptions[key] = parentValue.slice(); } } else { - mergedOptions[key] = parentOptions[key]; + mergedOptions[key] = parentValue; } } - } else if (parentOptions[key] == null) { + } else if (parentValue == null) { // only current value present - if (Array.isArray(currentOptions[key])) { - mergedOptions[key] = currentOptions[key].slice(); + if (Array.isArray(currentValue)) { + mergedOptions[key] = currentValue.slice(); } else { - mergedOptions[key] = currentOptions[key]; + mergedOptions[key] = currentValue; } } else { // both current and parent values present - if (Array.isArray(currentOptions[key])) { + if (Array.isArray(currentValue)) { let exclude; if (mutuallyExclusive != null && (exclude = currentOptions[mutuallyExclusive])) { mergedOptions[key] = [ - ...currentOptions[key], - ...parentOptions[key].filter(value => !currentOptions[key].includes(value) && !exclude.includes(value)) + ...currentValue, + ...parentValue.filter(value => !currentValue.includes(value) && !exclude.includes(value)) ]; } else { mergedOptions[key] = [ - ...currentOptions[key], - ...parentOptions[key].filter(value => !currentOptions[key].includes(value)) // dedup + ...currentValue, + ...parentValue.filter(value => !currentValue.includes(value)) // dedup ]; } } else { - mergedOptions[key] = currentOptions[key]; + mergedOptions[key] = currentValue; } } } From 9ba84625626d7f4c9911785a438a54c58ba9e4e1 Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 11:04:14 +0200 Subject: [PATCH 6/8] be consistent fwiw --- cli/util/options.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/util/options.js b/cli/util/options.js index 1f7adfac14..9a2fccb969 100644 --- a/cli/util/options.js +++ b/cli/util/options.js @@ -147,16 +147,16 @@ function sanitizeValue(value, type) { switch (type) { case undefined: case "b": return Boolean(value); - case "i": return +(+value).toFixed(0) || 0; - case "f": return +value || 0; + case "i": return Number(Number(value).toFixed(0)) || 0; + case "f": return Number(value) || 0; case "s": return String(value); case "I": { if (!Array.isArray(value)) value = [ value ]; - return value.map(v => +(+v).toFixed(0) || 0); + return value.map(v => Number(Number(v).toFixed(0)) || 0); } case "F": { if (!Array.isArray(value)) value = [ value ]; - return value.map(v => +v || 0); + return value.map(v => Number(v) || 0); } case "S": { if (!Array.isArray(value)) value = [ value ]; From 8c97dcca04a5f2a86efc3826ce469d9e4d43583e Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 11:11:48 +0200 Subject: [PATCH 7/8] simplify string array case --- cli/util/options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/util/options.js b/cli/util/options.js index 9a2fccb969..6144e11b19 100644 --- a/cli/util/options.js +++ b/cli/util/options.js @@ -160,7 +160,7 @@ function sanitizeValue(value, type) { } case "S": { if (!Array.isArray(value)) value = [ value ]; - return value.map(v => String(v)); + return value.map(String); } } } From b2b7d5de563c73a71283b6eab3d62fcdf781c11a Mon Sep 17 00:00:00 2001 From: dcode Date: Tue, 16 Jun 2020 11:15:10 +0200 Subject: [PATCH 8/8] use Math.trunc --- cli/util/options.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/util/options.js b/cli/util/options.js index 6144e11b19..8d3b39ed61 100644 --- a/cli/util/options.js +++ b/cli/util/options.js @@ -147,12 +147,12 @@ function sanitizeValue(value, type) { switch (type) { case undefined: case "b": return Boolean(value); - case "i": return Number(Number(value).toFixed(0)) || 0; + case "i": return Math.trunc(value) || 0; case "f": return Number(value) || 0; case "s": return String(value); case "I": { if (!Array.isArray(value)) value = [ value ]; - return value.map(v => Number(Number(v).toFixed(0)) || 0); + return value.map(v => Math.trunc(v) || 0); } case "F": { if (!Array.isArray(value)) value = [ value ];