UNPKG

@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
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 };