@thi.ng/args
Version:
Declarative, functional CLI argument/options parser, app framework, arg value coercions, multi/sub-commands, usage generation, error handling etc.
120 lines (119 loc) • 3.17 kB
JavaScript
import { isArray } from "@thi.ng/checks/is-array";
import { defError } from "@thi.ng/errors/deferror";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { camel, kebab } from "@thi.ng/strings/case";
import { usage } from "./usage.js";
import { __ansi, __colorTheme } from "./utils.js";
const ParseError = defError(() => "parse error");
const parse = (specs, argv, opts) => {
opts = { start: 2, showUsage: true, help: ["--help", "-h"], ...opts };
try {
return __parseOpts(specs, argv, opts);
} catch (e) {
if (opts.showUsage) {
console.log(
__ansi(
e.message,
__colorTheme(opts.usageOpts?.color).error
) + "\n\n" + usage(specs, opts.usageOpts)
);
}
throw new ParseError(e.message);
}
};
const __parseOpts = (specs, argv, opts) => {
const aliases = __aliasIndex(specs);
const acc = {};
let id;
let spec;
let i = opts.start;
for (; i < argv.length; ) {
const a = argv[i];
if (!id) {
if (opts.help.includes(a)) {
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: --${kebab(id)}`);
return {
result: __processResults(specs, acc),
index: i,
rest: argv.slice(i),
done: i >= argv.length
};
};
const __aliasIndex = (specs) => Object.entries(specs).reduce(
(acc, [k, v]) => v.alias ? (acc[v.alias] = k, acc) : acc,
{}
);
const __parseKey = (specs, aliases, acc, a) => {
if (a[0] !== "-") return { state: 2 };
let id;
if (a[1] === "-") {
if (a.length === 2) return { state: 1 };
id = camel(a.substring(2));
} else {
id = aliases[a.substring(1)];
!id && illegalArgs(`unknown option: ${a}`);
}
const spec = specs[id];
!spec && illegalArgs(id);
if (spec.type === "flag") {
acc[id] = true;
id = void 0;
if (spec.fn && !spec.fn("true")) return { state: 1, spec };
}
return { state: 0, id, spec };
};
const __parseValue = (spec, acc, id, a) => {
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 = (specs, acc) => {
let spec;
for (let id in specs) {
spec = specs[id];
if (acc[id] === void 0) {
if (spec.default !== void 0) {
acc[id] = spec.default;
} else if (spec.required) {
illegalArgs(`missing arg: --${kebab(id)}`);
}
} else if (spec.coerce) {
__coerceValue(spec, acc, id);
}
}
return acc;
};
const __coerceValue = (spec, acc, id) => {
try {
if (spec.multi && spec.delim && spec.split !== false) {
acc[id] = acc[id].reduce(
(acc2, x) => (acc2.push(...x.split(spec.delim)), acc2),
[]
);
}
acc[id] = spec.coerce(acc[id]);
} catch (e) {
throw new Error(`arg --${kebab(id)}: ${e.message}`);
}
};
export {
ParseError,
parse
};