@eeue56/baner
Version:
Flag parsing library in Typescript
675 lines (578 loc) • 16 kB
text/typescript
import { Err, Ok, Result } from "@eeue56/ts-core/build/main/lib/result";
/**
* A result from a flag: contains the flag name, if it was present in the arguments,
* and whether the arguments were parsed as described.
*/
type FlagResult = {
name: string;
isPresent: boolean;
arguments: Result<string, KnownTypes>;
};
export type StringArgument = {
kind: "StringArgument";
};
function StringArgument(): StringArgument {
return {
kind: "StringArgument",
};
}
/**
* An argument parser that treats an argument as a string
*/
export function string(): FlagArgument {
return StringArgument();
}
export type NumberArgument = {
kind: "NumberArgument";
};
function NumberArgument(): NumberArgument {
return {
kind: "NumberArgument",
};
}
/**
* An argument parser that treats an argument as a number
*/
export function number(): FlagArgument {
return NumberArgument();
}
export type BooleanArgument = {
kind: "BooleanArgument";
};
function BooleanArgument(): BooleanArgument {
return {
kind: "BooleanArgument",
};
}
/**
* An argument parser that treats an argument as a boolean
*/
export function boolean(): FlagArgument {
return BooleanArgument();
}
export type EmptyArgument = {
kind: "EmptyArgument";
};
function EmptyArgument(): EmptyArgument {
return {
kind: "EmptyArgument",
};
}
/**
* An argument parser that always passes
*/
export function empty(): FlagArgument {
return EmptyArgument();
}
export type ListArgument = {
kind: "ListArgument";
items: FlagArgument[];
};
function ListArgument(items: FlagArgument[]): ListArgument {
return {
kind: "ListArgument",
items,
};
}
/**
* An argument parser that treats an argument as a list
*/
export function list(flagArgumentParsers: FlagArgument[]): FlagArgument {
return ListArgument(flagArgumentParsers);
}
export type VariableListArgument = {
kind: "VariableListArgument";
item: FlagArgument;
};
function VariableListArgument(item: FlagArgument): VariableListArgument {
return {
kind: "VariableListArgument",
item,
};
}
/**
* An argument parser that treats an argument as an enum
*/
export function oneOf(items: string[]): FlagArgument {
return OneOfArgument(items);
}
export type OneOfArgument = {
kind: "OneOfArgument";
items: string[];
};
function OneOfArgument(items: string[]): OneOfArgument {
return {
kind: "OneOfArgument",
items,
};
}
/**
* An argument parser that treats an argument as a list
*/
export function variableList(flagArgumentParser: FlagArgument): FlagArgument {
return VariableListArgument(flagArgumentParser);
}
type KnownTypes = string | number | boolean | null | KnownTypes[];
export type FlagArgument =
| StringArgument
| NumberArgument
| BooleanArgument
| EmptyArgument
| ListArgument
| VariableListArgument
| OneOfArgument;
export type Short = {
kind: "Short";
name: string;
help: string;
parser: FlagArgument;
};
function Short(name: string, help: string, parser: FlagArgument): Short {
return {
kind: "Short",
name,
help,
parser,
};
}
export type Long = {
kind: "Long";
name: string;
help: string;
parser: FlagArgument;
};
function Long(name: string, help: string, parser: FlagArgument): Long {
return {
kind: "Long",
name,
help,
parser,
};
}
export type Both = {
kind: "Both";
shortName: string;
longName: string;
help: string;
parser: FlagArgument;
};
function Both(
shortName: string,
longName: string,
help: string,
parser: FlagArgument
): Both {
return {
kind: "Both",
shortName,
longName,
help,
parser,
};
}
export type Flag = Short | Long | Both;
/**
* A short flag, like -y
*/
export function shortFlag(name: string, help: string, parser: FlagArgument) {
return Short(name, help, parser);
}
/**
* A long flag, like --yes
*/
export function longFlag(name: string, help: string, parser: FlagArgument) {
return Long(name, help, parser);
}
/**
* A short or long flag, like -y or --yes
*/
export function bothFlag(
shortName: string,
longName: string,
help: string,
parser: FlagArgument
) {
return Both(shortName, longName, help, parser);
}
/**
* A program parser is composed of an array of flags
*/
export type ProgramParser = {
flags: Flag[];
};
/**
* A parser is composed of an array of flags
*/
export function parser(flags: Flag[]): ProgramParser {
return {
flags,
};
}
/**
* A Program contains all arguments given to it, and an record of all the flags
*/
export type Program = {
args: string[];
flags: Record<
string,
{
isPresent: boolean;
arguments: Result<string, KnownTypes>;
}
>;
};
function isFlag(string: string) {
const isNumber = isNaN(parseFloat(string));
if (isNumber) {
return string.startsWith("-");
}
return false;
}
function runEmpty(parseable: string[]): Result<string, null> {
return Ok(null);
}
function runString(parseable: string[]): Result<string, string> {
if (parseable.length === 0 || isFlag(parseable[0]))
return Err("Not enough arguments. Expected a string.");
return Ok(parseable[0]);
}
function runNumber(parseable: string[]): Result<string, 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: string[]): Result<string, boolean> {
if (parseable.length === 0 || isFlag(parseable[0]))
return Err("Not enough arguments. Expected a boolean.");
const parsed = parseable[0] === "true" || parseable[0] === "false";
if (!parsed) return Err("Not a boolean argument");
return Ok(parseable[0] === "true");
}
function runList(
flagArguments: FlagArgument[],
parseable: string[]
): Result<string, KnownTypes[]> {
const results = [ ];
for (var i = 0; i < flagArguments.length; i++) {
const argument = flagArguments[i];
const res = runArgument(argument, parseable.slice(i));
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: string[],
parseable: string[]
): Result<string, KnownTypes> {
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(
flagArgument: FlagArgument,
parseable: string[]
): Result<string, KnownTypes[]> {
const results = [ ];
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);
}
return Ok(results);
}
function runArgument(
argument: FlagArgument,
parseable: string[]
): Result<string, KnownTypes> {
switch (argument.kind) {
case "StringArgument": {
return runString(parseable);
}
case "NumberArgument": {
return runNumber(parseable);
}
case "BooleanArgument": {
return runBoolean(parseable);
}
case "EmptyArgument": {
return runEmpty(parseable);
}
case "ListArgument": {
return runList(argument.items, parseable);
}
case "VariableListArgument": {
return runVariableList(argument.item, parseable);
}
case "OneOfArgument": {
return runOneOf(argument.items, parseable);
}
}
}
function runShortFlag(
flagName: string,
innerParser: FlagArgument,
parseable: string[]
): FlagResult {
if (parseable.length === 0) {
return {
name: flagName,
isPresent: false,
arguments: Err(`Short flag -${flagName} not found`),
};
}
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 {
name: flagName,
isPresent: true,
arguments: res,
};
}
}
return {
name: flagName,
isPresent: false,
arguments: Err(`Short flag -${flagName} not found`),
};
}
function runLongFlag<a>(
flagName: string,
innerParser: FlagArgument,
parseable: string[]
): FlagResult {
if (parseable.length === 0) {
return {
name: flagName,
isPresent: false,
arguments: Err(`Long flag --${flagName} not found`),
};
}
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 {
name: flagName,
isPresent: true,
arguments: res,
};
}
}
return {
name: flagName,
isPresent: false,
arguments: Err(`Long flag --${flagName} not found`),
};
}
function runBothFlag(
shortFlagName: string,
longFlagName: string,
innerParser: FlagArgument,
parseable: string[]
): FlagResult {
const combinedFlagName = shortFlagName + "/" + longFlagName;
if (parseable.length === 0) {
return {
name: combinedFlagName,
isPresent: false,
arguments: Err(
`Mixed flag -${shortFlagName}/--${longFlagName} not found`
),
};
}
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 -${shortFlag}/--${longFlagName} due to: ${res.error}`
);
}
return {
name: combinedFlagName,
isPresent: true,
arguments: res,
};
}
}
return {
name: combinedFlagName,
isPresent: false,
arguments: Err(
`Mixed flag -${shortFlagName}/--${longFlagName} not found`
),
};
}
function runParser(programParser: ProgramParser, parseable: string[]): Program {
const emptyRecord: Record<
string,
{
isPresent: boolean;
arguments: Result<string, KnownTypes>;
}
> = {};
for (var i = 0; i < programParser.flags.length; i++) {
const flag = programParser.flags[i];
let res;
switch (flag.kind) {
case "Short": {
res = runShortFlag(flag.name, flag.parser, parseable);
break;
}
case "Long": {
res = runLongFlag(flag.name, flag.parser, parseable);
break;
}
case "Both": {
const bothFlag = flag as Both;
res = runBothFlag(
bothFlag.shortName,
bothFlag.longName,
bothFlag.parser,
parseable
);
}
}
let name;
switch (flag.kind) {
case "Short": {
name = flag.name;
break;
}
case "Long": {
name = flag.name;
break;
}
case "Both": {
const bothFlag = flag as Both;
name = bothFlag.shortName + "/" + bothFlag.longName;
}
}
emptyRecord[name] = {
isPresent: res.isPresent,
arguments: res.arguments,
};
}
return {
args: parseable,
flags: emptyRecord,
};
}
function helpFlagArgumentParser(parser: FlagArgument): string {
switch (parser.kind) {
case "BooleanArgument":
return "boolean";
case "NumberArgument":
return "number";
case "StringArgument":
return "string";
case "EmptyArgument":
return "";
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(flagParser: ProgramParser): string {
return flagParser.flags
.map((flag: Flag) => {
switch (flag.kind) {
case "Short": {
return ` -${flag.name} ${helpFlagArgumentParser(
flag.parser
)}:\t\t${flag.help}`;
}
case "Long": {
return ` --${flag.name} ${helpFlagArgumentParser(
flag.parser
)}:\t\t${flag.help}`;
}
case "Both": {
return ` -${flag.shortName}, --${
flag.longName
} ${helpFlagArgumentParser(flag.parser)}:\t\t${flag.help}`;
}
}
})
.join("\n");
}
/**
* Reports all errors in a program, ignoring missing flags.
*/
export function allErrors(program: Program): string[] {
const errors: string[] = [ ];
Object.keys(program.flags).map((key) => {
if (!program.flags[key].isPresent) return;
const argument = program.flags[key].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(program: Program, ignore: string[]): string[] {
const errors: string[] = [ ];
Object.keys(program.flags).map((key) => {
if (ignore.indexOf(key) > -1) return;
if (!program.flags[key].isPresent) errors.push(key);
});
return errors;
}
/**
* Runs a flag parser on the args
*/
export function parse(flagParser: ProgramParser, args: string[]): Program {
const parseable: string[] = [ ];
args.forEach((arg) => {
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;
}