From 8c022c4f6488accaca1f0ccea93d982ba0c503dc Mon Sep 17 00:00:00 2001 From: Tim Reichen Date: Mon, 12 Feb 2024 00:15:51 +0100 Subject: [PATCH] refactor(cli): cleanup `parse_args.ts` (#4189) * initial commit * fix typo * rename negated capturing group * update nested functions * update * simplify setNested * update * add parseBooleanValue * update * remove unknown value type * rename fns * update * Update parse_args.ts Co-authored-by: Asher Gomez * Update cli/parse_args.ts Co-authored-by: Asher Gomez * update * revert to letters.entries() --------- Co-authored-by: Asher Gomez --- cli/parse_args.ts | 465 +++++++++++++++++++++------------------------- 1 file changed, 211 insertions(+), 254 deletions(-) diff --git a/cli/parse_args.ts b/cli/parse_args.ts index 094678906389..f34daef64da8 100644 --- a/cli/parse_args.ts +++ b/cli/parse_args.ts @@ -16,7 +16,7 @@ * * @module */ -import { assertExists } from "../assert/assert_exists.ts"; +import { assert } from "../assert/assert.ts"; /** Combines recursively all intersection types and returns a new single type. */ type Id = TRecord extends Record @@ -329,52 +329,75 @@ export interface ParseOptions< unknown?: (arg: string, key?: string, value?: unknown) => unknown; } -interface Flags { - bools: Record; - strings: Record; - collect: Record; - negatable: Record; - unknownFn: (arg: string, key?: string, value?: unknown) => unknown; - allBools: boolean; -} - interface NestedMapping { [key: string]: NestedMapping | unknown; } -const { hasOwn } = Object; - -function get( - obj: Record, - key: string, -): TValue | undefined { - if (hasOwn(obj, key)) { - return obj[key]; - } -} - -function getForce(obj: Record, key: string): TValue { - const v = get(obj, key); - assertExists(v); - return v; -} - function isNumber(x: unknown): boolean { if (typeof x === "number") return true; if (/^0x[0-9a-f]+$/i.test(String(x))) return true; return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(String(x)); } -function hasKey(obj: NestedMapping, keys: string[]): boolean { - let o = obj; +function setNested( + object: NestedMapping, + keys: string[], + value: unknown, + collect = false, +) { keys.slice(0, -1).forEach((key) => { - o = (get(o, key) ?? {}) as NestedMapping; + object[key] ??= {}; + object = object[key] as NestedMapping; }); - const key = keys.at(-1); - return key !== undefined && hasOwn(o, key); + const key = keys[keys.length - 1]; + + if (collect) { + const v = object[key]; + if (Array.isArray(v)) { + v.push(value); + return; + } + + value = v ? [v, value] : [value]; + } + + object[key] = value; +} + +function hasNested(object: NestedMapping, keys: string[]): boolean { + keys = [...keys]; + const lastKey = keys.pop(); + if (!lastKey) return false; + for (const key of keys) { + if (!object[key]) return false; + object = object[key] as NestedMapping; + } + return Object.hasOwn(object, lastKey); +} + +function aliasIsBoolean( + aliasMap: Map>, + booleanSet: Set, + key: string, +): boolean { + const set = aliasMap.get(key); + if (set === undefined) return false; + for (const alias of set) if (booleanSet.has(alias)) return true; + return false; +} + +function isBooleanString(value: string) { + return value === "true" || value === "false"; +} + +function parseBooleanString(value: unknown) { + return value !== "false"; } +const FLAG_REGEXP = + /^(?:-(?:(?-)(?no-)?)?)(?.+?)(?:=(?.+?))?$/s; + /** * Take a set of command line arguments, optionally with a set of options, and * return an object representing the flags found in the passed arguments. @@ -439,7 +462,7 @@ export function parseArgs< string = [], collect = [], negatable = [], - unknown = (i: string): unknown => i, + unknown: unknownFn = (i: string): unknown => i, }: ParseOptions< TBooleans, TStrings, @@ -450,219 +473,153 @@ export function parseArgs< TDoubleDash > = {}, ): Args { - const aliases: Record = {}; - const flags: Flags = { - bools: {}, - strings: {}, - unknownFn: unknown, - allBools: false, - collect: {}, - negatable: {}, - }; - - if (alias !== undefined) { + const aliasMap: Map> = new Map(); + const booleanSet = new Set(); + const stringSet = new Set(); + const collectSet = new Set(); + const negatableSet = new Set(); + + let allBools = false; + + if (alias) { for (const key in alias) { - const val = getForce(alias, key); - if (typeof val === "string") { - aliases[key] = [val]; - } else { - aliases[key] = val as Array; - } - const aliasesForKey = getForce(aliases, key); - for (const alias of aliasesForKey) { - aliases[alias] = [key].concat(aliasesForKey.filter((y) => alias !== y)); - } + const val = (alias as Record)[key]; + assert(val !== undefined); + const aliases = Array.isArray(val) ? val : [val]; + aliasMap.set(key, new Set(aliases)); + const set = new Set([key, ...aliases]); + aliases.forEach((alias) => aliasMap.set(alias, set)); } } - if (boolean !== undefined) { + if (boolean) { if (typeof boolean === "boolean") { - flags.allBools = !!boolean; + allBools = boolean; } else { - const booleanArgs: ReadonlyArray = typeof boolean === "string" - ? [boolean] - : boolean; - + const booleanArgs = Array.isArray(boolean) ? boolean : [boolean]; for (const key of booleanArgs.filter(Boolean)) { - flags.bools[key] = true; - const alias = get(aliases, key); - if (alias) { - for (const al of alias) { - flags.bools[al] = true; - } - } + booleanSet.add(key); + aliasMap.get(key)?.forEach((al) => { + booleanSet.add(al); + }); } } } - if (string !== undefined) { - const stringArgs: ReadonlyArray = typeof string === "string" - ? [string] - : string; - + if (string) { + const stringArgs = Array.isArray(string) ? string : [string]; for (const key of stringArgs.filter(Boolean)) { - flags.strings[key] = true; - const alias = get(aliases, key); - if (alias) { - for (const al of alias) { - flags.strings[al] = true; - } - } + stringSet.add(key); + aliasMap.get(key)?.forEach((al) => stringSet.add(al)); } } - if (collect !== undefined) { - const collectArgs: ReadonlyArray = typeof collect === "string" - ? [collect] - : collect; - + if (collect) { + const collectArgs = Array.isArray(collect) ? collect : [collect]; for (const key of collectArgs.filter(Boolean)) { - flags.collect[key] = true; - const alias = get(aliases, key); - if (alias) { - for (const al of alias) { - flags.collect[al] = true; - } - } + collectSet.add(key); + aliasMap.get(key)?.forEach((al) => collectSet.add(al)); } } - if (negatable !== undefined) { - const negatableArgs: ReadonlyArray = typeof negatable === "string" - ? [negatable] - : negatable; - + if (negatable) { + const negatableArgs = Array.isArray(negatable) ? negatable : [negatable]; for (const key of negatableArgs.filter(Boolean)) { - flags.negatable[key] = true; - const alias = get(aliases, key); - if (alias) { - for (const al of alias) { - flags.negatable[al] = true; - } - } + negatableSet.add(key); + aliasMap.get(key)?.forEach((alias) => negatableSet.add(alias)); } } const argv: Args = { _: [] }; - function argDefined(key: string, arg: string): boolean { - return ( - (flags.allBools && /^--[^=]+$/.test(arg)) || - get(flags.bools, key) || - !!get(flags.strings, key) || - !!get(aliases, key) - ); - } - - function setKey( - obj: NestedMapping, - name: string, - value: unknown, - collect = true, - ) { - let o = obj; - const keys = name.split("."); - keys.slice(0, -1).forEach(function (key) { - if (get(o, key) === undefined) { - o[key] = {}; - } - o = get(o, key) as NestedMapping; - }); - - const key = keys.at(-1)!; - const collectable = collect && !!get(flags.collect, name); - - if (!collectable) { - o[key] = value; - } else if (get(o, key) === undefined) { - o[key] = [value]; - } else if (Array.isArray(get(o, key))) { - (o[key] as unknown[]).push(value); - } else { - o[key] = [get(o, key), value]; - } - } - - function setArg( + function setArgument( key: string, - val: unknown, - arg: string | undefined = undefined, - collect?: boolean, + value: string | number | boolean, + arg: string, + collect: boolean, ) { - if (arg && flags.unknownFn && !argDefined(key, arg)) { - if (flags.unknownFn(arg, key, val) === false) return; + if ( + !booleanSet.has(key) && + !stringSet.has(key) && + !aliasMap.has(key) && + !(allBools && /^--[^=]+$/.test(arg)) && + unknownFn?.(arg, key, value) === false + ) { + return; } - - const value = !get(flags.strings, key) && isNumber(val) ? Number(val) : val; - setKey(argv, key, value, collect); - - const alias = get(aliases, key); - if (alias) { - for (const x of alias) { - setKey(argv, x, value, collect); - } + if (typeof value === "string" && !stringSet.has(key)) { + value = isNumber(value) ? Number(value) : value; } - } - function aliasIsBoolean(key: string): boolean { - return getForce(aliases, key).some( - (x) => typeof get(flags.bools, x) === "boolean", + const collectable = collect && collectSet.has(key); + setNested(argv, key.split("."), value, collectable); + aliasMap.get(key)?.forEach((key) => + setNested(argv, key.split("."), value, collectable) ); } let notFlags: string[] = []; // all args after "--" are not parsed - if (args.includes("--")) { - notFlags = args.slice(args.indexOf("--") + 1); - args = args.slice(0, args.indexOf("--")); + const index = args.indexOf("--"); + if (index !== -1) { + notFlags = args.slice(index + 1); + args = args.slice(0, index); } for (let i = 0; i < args.length; i++) { const arg = args[i]; - assertExists(arg); - - if (/^--.+=/.test(arg)) { - const m = arg.match(/^--([^=]+)=(.*)$/s); - assertExists(m); - const [, key, value] = m; - assertExists(key); - - if (flags.bools[key]) { - const booleanValue = value !== "false"; - setArg(key, booleanValue, arg); - } else { - setArg(key, value, arg); - } - } else if ( - /^--no-.+/.test(arg) && get(flags.negatable, arg.replace(/^--no-/, "")) - ) { - const m = arg.match(/^--no-(.+)/); - assertExists(m); - assertExists(m[1]); - setArg(m[1], false, arg, false); - } else if (/^--.+/.test(arg)) { - const m = arg.match(/^--(.+)/); - assertExists(m); - assertExists(m[1]); - const [, key] = m; - const next = args[i + 1]; - if ( - next !== undefined && - !/^-/.test(next) && - !get(flags.bools, key) && - !flags.allBools && - (get(aliases, key) ? !aliasIsBoolean(key) : true) - ) { - setArg(key, next, arg); - i++; - } else if (next !== undefined && (next === "true" || next === "false")) { - setArg(key, next === "true", arg); - i++; - } else { - setArg(key, get(flags.strings, key) ? "" : true, arg); + + const groups = arg.match(FLAG_REGEXP)?.groups; + + if (groups) { + const { doubleDash, negated } = groups; + let key = groups.key; + let value: string | number | boolean = groups.value; + + if (doubleDash) { + if (value) { + if (booleanSet.has(key)) value = parseBooleanString(value); + setArgument(key, value, arg, true); + continue; + } + + if (negated) { + if (negatableSet.has(key)) { + setArgument(key, false, arg, false); + continue; + } + key = `no-${key}`; + } + + const next = args[i + 1]; + + if ( + !booleanSet.has(key) && + !allBools && + next && + !/^-/.test(next) && + (aliasMap.get(key) + ? !aliasIsBoolean(aliasMap, booleanSet, key) + : true) + ) { + value = next; + i++; + setArgument(key, value, arg, true); + continue; + } + + if (isBooleanString(next)) { + value = parseBooleanString(next); + i++; + setArgument(key, value, arg, true); + continue; + } + + value = stringSet.has(key) ? "" : true; + setArgument(key, value, arg, true); + continue; } - } else if (/^-[^-]+/.test(arg)) { const letters = arg.slice(1, -1).split(""); let broken = false; @@ -670,12 +627,12 @@ export function parseArgs< const next = arg.slice(j + 2); if (next === "-") { - setArg(letter, next, arg); + setArgument(letter, next, arg, true); continue; } - if (/[A-Za-z]/.test(letter) && next.includes("=")) { - setArg(letter, next.split(/=(.+)/)[1], arg); + if (/[A-Za-z]/.test(letter) && /=/.test(next)) { + setArgument(letter, next.split(/=(.+)/)[1], arg, true); broken = true; break; } @@ -684,82 +641,82 @@ export function parseArgs< /[A-Za-z]/.test(letter) && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next) ) { - setArg(letter, next, arg); + setArgument(letter, next, arg, true); broken = true; break; } - if (letters[j + 1]?.match(/\W/)) { - setArg(letter, arg.slice(j + 2), arg); + if (letters[j + 1] && letters[j + 1].match(/\W/)) { + setArgument(letter, arg.slice(j + 2), arg, true); broken = true; break; - } else { - setArg(letter, get(flags.strings, letter) ? "" : true, arg); } + setArgument( + letter, + stringSet.has(letter) ? "" : true, + arg, + true, + ); } - const key = arg.at(-1)!; + key = arg.slice(-1); if (!broken && key !== "-") { const nextArg = args[i + 1]; if ( nextArg && !/^(-|--)[^-]/.test(nextArg) && - !get(flags.bools, key) && - (get(aliases, key) ? !aliasIsBoolean(key) : true) + !booleanSet.has(key) && + (aliasMap.get(key) + ? !aliasIsBoolean(aliasMap, booleanSet, key) + : true) ) { - setArg(key, nextArg, arg); + setArgument(key, nextArg, arg, true); i++; - } else if (nextArg && (nextArg === "true" || nextArg === "false")) { - setArg(key, nextArg === "true", arg); + } else if (nextArg && isBooleanString(nextArg)) { + const value = parseBooleanString(nextArg); + setArgument(key, value, arg, true); i++; } else { - setArg(key, get(flags.strings, key) ? "" : true, arg); + setArgument(key, stringSet.has(key) ? "" : true, arg, true); } } - } else { - if (!flags.unknownFn || flags.unknownFn(arg) !== false) { - argv._.push(flags.strings["_"] ?? !isNumber(arg) ? arg : Number(arg)); - } - if (stopEarly) { - argv._.push(...args.slice(i + 1)); - break; - } + continue; } - } - for (const [key, value] of Object.entries(defaults)) { - if (!hasKey(argv, key.split("."))) { - setKey(argv, key, value, false); + if (unknownFn?.(arg) !== false) { + argv._.push( + stringSet.has("_") || !isNumber(arg) ? arg : Number(arg), + ); + } - const alias = aliases[key]; - if (alias !== undefined) { - for (const x of alias) { - setKey(argv, x, value, false); - } - } + if (stopEarly) { + argv._.push(...args.slice(i + 1)); + break; } } - for (const key of Object.keys(flags.bools)) { - if (!hasKey(argv, key.split("."))) { - const value = get(flags.collect, key) ? [] : false; - setKey( - argv, - key, - value, - false, + for (const [key, value] of Object.entries(defaults)) { + const keys = key.split("."); + if (!hasNested(argv, keys)) { + setNested(argv, keys, value); + aliasMap.get(key)?.forEach((key) => + setNested(argv, key.split("."), value) ); } } - for (const key of Object.keys(flags.strings)) { - if (!hasKey(argv, key.split(".")) && get(flags.collect, key)) { - setKey( - argv, - key, - [], - false, - ); + for (const key of booleanSet.keys()) { + const keys = key.split("."); + if (!hasNested(argv, keys)) { + const value = collectSet.has(key) ? [] : false; + setNested(argv, keys, value); + } + } + + for (const key of stringSet.keys()) { + const keys = key.split("."); + if (!hasNested(argv, keys) && collectSet.has(key)) { + setNested(argv, keys, []); } }