Skip to content

Commit

Permalink
refactor(args): splitup parse() into smaller fns
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Jan 12, 2021
1 parent 909cd88 commit 64be608
Showing 1 changed file with 120 additions and 77 deletions.
197 changes: 120 additions & 77 deletions packages/args/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,140 @@ import { camel } from "@thi.ng/strings";
import type { Args, ArgSpecExt, ParseOpts, ParseResult } from "./api";
import { usage } from "./usage";

const HELP = "--help";

export const parse = <T extends IObjectOf<any>>(
specs: Args<T>,
argv: string[],
opts?: Partial<ParseOpts>
): ParseResult<T> | undefined => {
opts = { start: 2, showUsage: true, ...opts };
try {
return parseOpts(specs, argv, opts);
} catch (e) {
if (opts.showUsage) {
console.log(e.message + "\n\n" + usage(specs, opts.usageOpts));
}
throw e;
}
};

const parseOpts = <T extends IObjectOf<any>>(
specs: Args<T>,
argv: string[],
opts: Partial<ParseOpts>
): ParseResult<T> | undefined => {
const aliases = aliasIndex<T>(specs);
const acc: any = {};
const aliases = Object.entries(specs).reduce(
(acc, [k, v]) => (v.alias ? ((acc[v.alias] = k), acc) : acc),
<IObjectOf<string>>{}
);
let id: Nullable<string>;
let spec: Nullable<ArgSpecExt>;
let i = opts.start!;
try {
for (; i < argv.length; ) {
const a = argv[i];
if (!id) {
if (a[0] === "-") {
if (a[1] === "-") {
if (a === "--") {
i++;
break;
}
id = camel(a.substr(2));
} else {
id = aliases[a[1]];
!id && illegalArgs(`unknown alias: ${a}`);
}
if (id === "help") {
console.log(usage(specs, opts.usageOpts));
return;
}
spec = specs[id];
!spec && illegalArgs(id);
i++;
if (spec.flag) {
acc[id] = true;
id = null;
if (spec.fn && !spec.fn("true")) break;
}
} else break;
} else {
/^-[a-z]/i.test(a) && illegalArgs(`missing value for: --${id}`);
if (spec!.multi) {
isArray(acc[id]) ? acc[id].push(a) : (acc[id] = [a]);
} else {
acc[id] = a;
}
id = null;
i++;
if (spec!.fn && !spec!.fn(a)) break;
for (; i < argv.length; ) {
const a = argv[i];
if (!id) {
if (a === HELP) {
console.log(usage(specs, opts.usageOpts));
return;
}
const state = parseKey(specs, aliases, acc, a);
id = state.id;
spec = state.spec;
i = i + ~~(state.state < 2);
if (state.state) break;
} else {
if (parseValue(spec!, acc, id, a)) break;
id = null;
i++;
}
}
id && illegalArgs(`missing value for: --${id}`);
return {
result: processResults(specs, acc),
index: i,
rest: argv.slice(i),
done: i >= argv.length,
};
};

const aliasIndex = <T extends IObjectOf<any>>(specs: Args<T>) =>
Object.entries(specs).reduce(
(acc, [k, v]) => (v.alias ? ((acc[v.alias] = k), acc) : acc),
<IObjectOf<string>>{}
);

interface ParseKeyResult {
state: number;
id?: string;
spec?: ArgSpecExt;
}

const parseKey = <T extends IObjectOf<any>>(
specs: Args<T>,
aliases: IObjectOf<string>,
acc: any,
a: string
): ParseKeyResult => {
if (a[0] === "-") {
let id: string | undefined;
if (a[1] === "-") {
// terminator arg, stop parsing
if (a === "--") return { state: 1 };
id = camel(a.substr(2));
} else {
id = aliases[a[1]];
!id && illegalArgs(`unknown option: ${a}`);
}
const spec: ArgSpecExt = specs[id];
!spec && illegalArgs(id);
if (spec.flag) {
acc[id] = true;
id = undefined;
// stop parsing if fn returns false
if (spec.fn && !spec.fn("true")) return { state: 1, spec };
}
id && illegalArgs(`missing value for: --${id}`);
for (id in specs) {
spec = specs[id];
if (acc[id] === undefined) {
if (spec.default !== undefined) {
acc[id] = spec.default;
} else if (spec.optional === false) {
illegalArgs(`missing arg: --${id}`);
}
} else {
if (spec.coerce) {
try {
if (spec.multi && spec.delim) {
acc[id] = (<string[]>acc[id]).reduce(
(acc, x) => (
acc.push(...x.split(spec!.delim!)), acc
),
<string[]>[]
);
}
acc[id] = spec.coerce(acc[id]);
} catch (e) {
throw new Error(`arg --${id}: ${e.message}`);
}
}
return { state: 0, id, spec };
}
// no option arg, stop parsing
return { state: 2 };
};

const parseValue = (spec: ArgSpecExt, acc: any, id: string, a: string) => {
/^-[a-z]/i.test(a) && illegalArgs(`missing value for: --${id}`);
if (spec!.multi) {
isArray(acc[id!]) ? acc[id!].push(a) : (acc[id!] = [a]);
} else {
acc[id!] = a;
}
return spec!.fn && !spec!.fn(a);
};

const processResults = <T extends IObjectOf<any>>(specs: Args<T>, acc: any) => {
let spec: Nullable<ArgSpecExt>;
for (let id in specs) {
spec = specs[id];
if (acc[id] === undefined) {
if (spec.default !== undefined) {
acc[id] = spec.default;
} else if (spec.optional === false) {
illegalArgs(`missing arg: --${id}`);
}
} else if (spec.coerce) {
coerceValue(spec, acc, id);
}
return {
result: acc,
index: i,
rest: argv.slice(i),
done: i >= argv.length,
};
} catch (e) {
if (opts.showUsage) {
console.log(e.message + "\n\n" + usage(specs, opts.usageOpts));
}
return acc;
};

const coerceValue = (spec: ArgSpecExt, acc: any, id: string) => {
try {
if (spec.multi && spec.delim) {
acc[id] = (<string[]>acc[id]).reduce(
(acc, x) => (acc.push(...x.split(spec!.delim!)), acc),
<string[]>[]
);
}
throw e;
acc[id] = spec.coerce!(acc[id]);
} catch (e) {
throw new Error(`arg --${id}: ${e.message}`);
}
};

0 comments on commit 64be608

Please sign in to comment.