UNPKG

@eeue56/baner

Version:

Flag parsing library in Typescript

527 lines (457 loc) 15 kB
import type { BaseParseableTypes, BaseVariableListFlagArgument, BasicTypes, Both, Flag, FlagArgument, FlagResult, InferFlagArgumentType, InferFlagName, InferFlagType, KnownTypes, Long, Program, ProgramParser, ProgramValues, Result, Short, VariableListBasicTypes, } from "./types.ts"; import { Err, Ok } from "./types.ts"; /** * A parser is composed of an array of flags */ export function parser< const flags extends readonly Flag<FlagArgument<KnownTypes>, string>[], >(...flags: flags): ProgramParser<flags> { const collectedFlags: Partial<ProgramParser<flags>> = {}; for (const flag of flags) { switch (flag.kind) { case "Short": { (collectedFlags as any)[flag.name] = flag; break; } case "Long": { (collectedFlags as any)[flag.name] = flag; break; } case "Both": { (collectedFlags as any)[flag.longName] = flag; break; } } } return { flags: collectedFlags } as ProgramParser<flags>; } function isFlag(string: string): boolean { const isNumber = isNaN(parseFloat(string)); if (isNumber) { return string.startsWith("-"); } return false; } function runString(parseable: readonly string[]): Result<string> { if (parseable.length === 0 || isFlag(parseable[0])) return Err("Not enough arguments. Expected a string."); return Ok(parseable[0]); } function runNumber(parseable: readonly string[]): Result<number> { if (parseable.length === 0 || isFlag(parseable[0])) return Err("Not enough arguments. Expected a number."); const parsed = parseFloat(parseable[0]); if (isNaN(parsed)) return Err("Not a number argument"); return Ok(parsed); } function runBoolean(parseable: readonly string[]): Result<boolean> { if (parseable.length === 0 || isFlag(parseable[0])) { return Ok(true); } const parsed = parseable[0] === "true" || parseable[0] === "false"; if (!parsed) return Err("Not a boolean argument"); return Ok(parseable[0] === "true"); } function runList< const flagArguments extends readonly FlagArgument<BasicTypes>[], >( flagArguments: flagArguments, parseable: readonly string[], ): Result<readonly InferFlagArgumentType<flagArguments[number]>[]> { const results: InferFlagArgumentType<flagArguments[number]>[] = [] as InferFlagArgumentType<flagArguments[number]>[]; for (var i = 0; i < flagArguments.length; i++) { const argument = flagArguments[i]; const res = runArgument(argument, parseable.slice(i)) as Result< InferFlagArgumentType<flagArguments[number]> >; if (res.kind === "Err") { if (i >= parseable.length || isFlag(parseable[i])) { return Err(`${res.error} at index ${i}`); } return res; } else { results.push(res.value); } } return Ok(results); } function runOneOf( items: readonly string[], parseable: readonly string[], ): Result<BaseParseableTypes> { if (parseable.length === 0 || isFlag(parseable[0])) return Err( `Not enough arguments. Expected one of: ${items.join(" | ")}.`, ); for (var i = 0; i < items.length; i++) { const item = items[i]; if (item === parseable[0]) return Ok(item); } return Err(`Didn't match any of: ${items.join(" | ")}`); } function runVariableList<flagType extends VariableListBasicTypes>( flagArgument: BaseVariableListFlagArgument<flagType>, parseable: readonly string[], ): Result<readonly flagType[]> { const results: flagType[] = []; for (var i = 0; i < parseable.length; i++) { if (isFlag(parseable[i])) break; const res = runArgument(flagArgument, parseable.slice(i)); if (res.kind === "Err") return res; results.push(res.value as flagType); } return Ok(results); } function runArgument<const flagType extends FlagArgument<KnownTypes>>( argument: flagType, parseable: readonly string[], ): Result<InferFlagArgumentType<flagType>> { switch (argument.kind) { case "StringArgument": { return runString(parseable) as Result< InferFlagArgumentType<flagType> >; } case "NumberArgument": { return runNumber(parseable) as Result< InferFlagArgumentType<flagType> >; } case "BooleanArgument": { return runBoolean(parseable) as Result< InferFlagArgumentType<flagType> >; } case "ListArgument": { return runList(argument.items, parseable) as Result< InferFlagArgumentType<flagType> >; } case "VariableListArgument": { return runVariableList( argument.item as BaseVariableListFlagArgument<VariableListBasicTypes>, parseable, ) as Result<InferFlagArgumentType<flagType>>; } case "OneOfArgument": { return runOneOf(argument.items, parseable) as Result< InferFlagArgumentType<flagType> >; } } } function runShortFlag< const flagArgument extends FlagArgument<KnownTypes>, const name extends string, >( flag: Short<flagArgument, name>, parseable: readonly string[], ): FlagResult<flagArgument, name> { const flagName: name = flag.name; const innerParser: flagArgument = flag.parser; if (parseable.length === 0) { return { isPresent: false, arguments: Err(`Short flag -${flagName} not found`), flag: flag, }; } for (var i = 0; i < parseable.length; i++) { const value = parseable[i]; if (value === `-${flagName}`) { let res = runArgument(innerParser, parseable.slice(i + 1)); if (res.kind === "Err") { res = Err(`Error parsing -${flagName} due to: ${res.error}`); } return { isPresent: true, arguments: res as Result<InferFlagArgumentType<flagArgument>>, flag: flag, }; } } return { isPresent: false, arguments: Err(`Short flag -${flagName} not found`), flag: flag, }; } function runLongFlag< const flagArgument extends FlagArgument<KnownTypes>, const name extends string, >( flag: Long<flagArgument, name>, parseable: readonly string[], ): FlagResult<flagArgument, name> { const flagName: name = flag.name; const innerParser: flagArgument = flag.parser; if (parseable.length === 0) { return { isPresent: false, arguments: Err(`Long flag --${flagName} not found`), flag, }; } for (var i = 0; i < parseable.length; i++) { const value = parseable[i]; if (value === `--${flagName}`) { let res = runArgument(innerParser, parseable.slice(i + 1)); if (res.kind === "Err") { res = Err(`Error parsing --${flagName} due to: ${res.error}`); } return { isPresent: true, arguments: res as Result<InferFlagArgumentType<flagArgument>>, flag, }; } } return { isPresent: false, arguments: Err(`Long flag --${flagName} not found`), flag, }; } function runBothFlag< const flagArgument extends FlagArgument<KnownTypes>, const name extends string, >( flag: Both<flagArgument, name>, parseable: readonly string[], ): FlagResult<flagArgument, name> { const shortFlagName = flag.shortName; const longFlagName = flag.longName; const innerParser: flagArgument = flag.parser; const combinedFlagName = `-${shortFlagName}/--${longFlagName}`; if (parseable.length === 0) { return { isPresent: false, arguments: Err(`Mixed flag ${combinedFlagName} not found`), flag, }; } for (var i = 0; i < parseable.length; i++) { const value = parseable[i]; if (value === `-${shortFlagName}` || value === `--${longFlagName}`) { let res = runArgument(innerParser, parseable.slice(i + 1)); if (res.kind === "Err") { res = Err( `Error parsing ${combinedFlagName} due to: ${res.error}`, ); } return { isPresent: true, arguments: res as Result<InferFlagArgumentType<flagArgument>>, flag, }; } } return { isPresent: false, arguments: Err(`Mixed flag ${combinedFlagName} not found`), flag, }; } function runParser< const flags extends readonly Flag<FlagArgument<KnownTypes>, string>[], >( programParser: ProgramParser<flags>, parseable: readonly string[], ): Program<flags> { const emptyRecord: Program<flags> = { args: parseable, flags: {} as Program<flags>["flags"], }; for (const flag of Object.values(programParser.flags) as flags[number][]) { let res; switch (flag.kind) { case "Short": { res = runShortFlag(flag, parseable); break; } case "Long": { res = runLongFlag(flag, parseable); break; } case "Both": { res = runBothFlag(flag, parseable); } } let name: InferFlagName<flags[number]>; switch (flag.kind) { case "Short": { name = flag.name as InferFlagName<flags[number]>; break; } case "Long": { name = flag.name as InferFlagName<flags[number]>; break; } case "Both": { name = flag.longName as InferFlagName<flags[number]>; break; } } (emptyRecord.flags as any)[name] = { isPresent: res.isPresent, arguments: res.arguments as Result<InferFlagType<flags[number]>>, flag, }; } return emptyRecord; } function helpFlagArgumentParser< const flagArgument extends FlagArgument<KnownTypes>, >(parser: flagArgument): string { switch (parser.kind) { case "BooleanArgument": return "boolean"; case "NumberArgument": return "number"; case "StringArgument": return "string"; case "ListArgument": return ( "[" + parser.items.map(helpFlagArgumentParser).join(" ") + "]" ); case "VariableListArgument": return "[" + helpFlagArgumentParser(parser.item) + "...]"; case "OneOfArgument": return parser.items.join(" | "); } } /** * Creates a help text for a given program parser */ export function help< const flags extends readonly Flag<FlagArgument<KnownTypes>, string>[], >(flagParser: ProgramParser<flags>): string { const output: string[] = []; for (const flag of Object.values(flagParser.flags) as flags[number][]) { switch (flag.kind) { case "Short": { output.push( ` -${flag.name} ${helpFlagArgumentParser( flag.parser, )}:\t\t${flag.help}`, ); break; } case "Long": { output.push( ` --${flag.name} ${helpFlagArgumentParser( flag.parser, )}:\t\t${flag.help}`, ); break; } case "Both": { output.push( ` -${flag.shortName}, --${ flag.longName } ${helpFlagArgumentParser(flag.parser)}:\t\t${flag.help}`, ); break; } } } return output.join("\n"); } /** * Reports all errors in a program, ignoring missing flags. */ export function allErrors< const flags extends readonly Flag<FlagArgument<KnownTypes>, string>[], >(program: Program<flags>): string[] { const errors: string[] = []; for (const key of Object.keys(program.flags) as InferFlagName< flags[number] >[]) { const value = program.flags[key] as FlagResult< FlagArgument<KnownTypes>, InferFlagName<flags[number]> >; if (!value.isPresent) continue; const argument = value.arguments; if (argument.kind === "Err") { errors.push(argument.error); } } return errors; } /** * Reports missing flags, ignoring the ones you don't care about. */ export function allMissing< const flags extends readonly Flag<FlagArgument<KnownTypes>, string>[], >(program: Program<flags>, ignore: string[]): string[] { const errors: string[] = []; for (const key of Object.keys(program.flags)) { if (ignore.indexOf(key) > -1) continue; if (!(program.flags as any)[key].isPresent) errors.push(key); } return errors; } /** * Gets the Ok values out of the program as the raw type (i.e a `string` from `string()`) */ export function allValues< const flags extends readonly Flag<FlagArgument<KnownTypes>, string>[], >(program: Program<flags>): ProgramValues<flags> { const values: Partial<ProgramValues<flags>> = {}; for (const key of Object.keys(program.flags)) { if (!(program.flags as any)[key].isPresent) continue; const argument = (program.flags as any)[key].arguments; if (argument.kind === "Ok") { values[key as keyof ProgramValues<flags>] = argument.value; } else { values[key as keyof ProgramValues<flags>] = undefined; } } return values as unknown as ProgramValues<flags>; } /** * Runs a flag parser on the args */ export function parse< const flags extends readonly Flag<FlagArgument<KnownTypes>, string>[], >(flagParser: ProgramParser<flags>, args: string[]): Program<flags> { const parseable: string[] = []; for (const arg of args) { if (arg.indexOf("=") > -1) { parseable.push(arg.split("=")[0]); arg.split("=")[1] .split(",") .forEach((splitArg) => { parseable.push(splitArg); }); } else { arg.split(",").forEach((splitArg) => { parseable.push(splitArg); }); } } const res = runParser(flagParser, parseable); return res; } export * from "./types.ts";