UNPKG

yaro

Version:

Fast, minimalist, pluggable and functional command line parsing. Create CLIs easily. A wrapper package exporting everything from several Yaro packages.

344 lines (269 loc) 9.46 kB
// SPDX-License-Identifier: MPL-2.0 import proc from 'node:process'; /* eslint-disable unicorn/consistent-function-scoping */ /* eslint-disable unicorn/no-anonymous-default-export */ let unnamedCommandsCount = 0; export default (yaroPlugins) => { const { pipeline, parser, defaults, aliases, coerce, required } = yaroPlugins; // import { // pipeline, // parser, // defaults, // aliases, // coerce, // required, // } from 'yaro-plugins'; const UNNAMED_COMMAND_PREFIX = '___UNNAMED_COMMAND-'; // exports return { UNNAMED_COMMAND_PREFIX, command }; function command(usage, settings, action) { if (typeof usage === 'function') { action = usage; usage = ''; } if (typeof usage !== 'string') { throw new TypeError('cli requires `usage` to be passed and be a string'); } if (typeof settings === 'function') { action = settings; settings = {}; } if (typeof settings === 'string') { settings = { desc: settings }; } // validate args (required cannot have default values; optional ones can) findAllBrackets(usage); const cfg = { ...settings }; const meta = { defaults: {}, aliases: {}, coerces: {}, required: {}, flags: {}, ...cfg.meta, }; const cli = { settings: cfg, usage, meta, aliases: [], }; cli.alias = (...names) => { // @ts-ignore cli.aliases.push(...names.flat()); return cli; }; cli.option = createOptionMethod(cli); cli.action = createActionMethod(cli, cfg); if (typeof action === 'function') { return cli.action(action); } return cli; } function createOptionMethod(cli /* , settings */) { return (rawName, desc, config) => { const flag = createOption(rawName, desc, config); cli.meta.flags[flag.name] = flag; cli.meta.aliases = flag.aliases.reduce((accumulator, k) => { accumulator[k] = flag.aliases.length === 1 ? flag.aliases : flag.aliases.filter((x) => x !== k); return accumulator; }, cli.meta.aliases); cli.meta.defaults = flag.aliases.reduce((accumulator, k) => { accumulator[k] = flag.default; return accumulator; }, cli.meta.defaults); cli.meta.coerces = flag.aliases.reduce((accumulator, k) => { accumulator[k] = flag.type; return accumulator; }, cli.meta.coerces); cli.meta.required = flag.aliases.reduce((accumulator, k) => { accumulator[k] = flag.required; return accumulator; }, cli.meta.required); return cli; }; } function createOption(rawName, desc, config) { let cfg = config; if (desc && typeof desc === 'object') { cfg = desc; desc = cfg.desc || cfg.description || ''; } if (cfg !== null && typeof cfg !== 'object') { cfg = { default: cfg }; } cfg = { ...cfg }; const flag = { rawName, desc: desc || '', default: cfg.default, type: cfg.type, }; const names = removeBrackets(rawName) .split(',') .reduce((accumulator, v) => { let name = v.trim(); let index; // eslint-disable-next-line no-plusplus for (index = 0; index < name.length; index++) { if (name.codePointAt(index) !== 45) break; // "-" } if (index === 1) { name = name.slice(1); return accumulator.concat(name, camelcase(name)); } if (name.slice(index, index + 3) === 'no-') { flag.negated = true; name = name.slice(index + 3); } else { name = name.slice(index); } return accumulator.concat(name, camelcase(name)); }, []) .sort((a, b) => (a.length > b.length ? 1 : -1)); // Sort names // Use the longest name (last one) as actual option name flag.name = names.at(-1); flag.name = flag.name.includes('-') ? names.at(-2) : flag.name; flag.aliases = [...new Set(names)]; if (flag.negated) { flag.default = true; } if (rawName.includes('<')) { flag.required = true; } else if (rawName.includes('[')) { flag.required = false; } flag.required = cfg.required ?? flag.required; return flag; } function removeBrackets(v) { return v.replace(/[<[].+/, '').trim(); } function camelcase(input) { return input.replaceAll(/([a-z])-([a-z])/g, (_, p1, p2) => p1 + p2.toUpperCase()); } function findAllBrackets(v) { const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g; const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)]/g; const res = []; const parseMatch = (match) => { let variadic = false; let value = match[1]; if (value.startsWith('...')) { value = value.slice(3); variadic = true; } const isRequired = match[0].startsWith('<'); const hasDefault = value.includes('='); if (hasDefault && isRequired) { const error = new Error(`required arguments cannot have defaults: ${v}`); // @ts-ignore error.cmdUsage = v; throw error; } let defValue = null; if (hasDefault) { let val = value.split('=')[1]; // eslint-disable-next-line prefer-template val = val.startsWith('[') ? val + ']' : val; val = val.startsWith('[') || val.startsWith('{') ? val : `"${val}"`; defValue = JSON.parse(val); } return { optional: !isRequired, required: isRequired, name: hasDefault ? value.split('=')[0] : value, variadic, ...(hasDefault ? { default: defValue } : {}), }; }; let angledMatch; // eslint-disable-next-line no-cond-assign while ((angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v))) { res.push(parseMatch(angledMatch)); } let squareMatch; // eslint-disable-next-line no-cond-assign while ((squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v))) { res.push(parseMatch(squareMatch)); } return res; } function createActionMethod(cmd, _cfg) { return (handler) => { if (!cmd.fn && typeof handler !== 'function') { throw new TypeError('cmd do not have action handler function'); } const commandHandler = cmd.fn || handler; cmd.name = removeBrackets(cmd.usage); cmd.args = findAllBrackets(cmd.usage); cmd.parts = cmd.name.split(' '); cmd.usage = cmd.usage.slice(cmd.name.length).trim(); if (cmd.name.length === 0) { unnamedCommandsCount += 1; cmd.name = String(UNNAMED_COMMAND_PREFIX + unnamedCommandsCount); } commandAction.isYaroCommand = true; commandAction.cli = cmd; commandHandler.isYaroCommand = true; commandHandler.cli = cmd; return commandAction; async function commandAction(parsedFlags, args) { const argv = pipeline( // run parser only if when we are given process.argv array Array.isArray(parsedFlags) && parser(), aliases(cmd.meta.aliases), // aliases always first defaults(cmd.meta.defaults, cmd.meta.aliases), // todo: buggy, better use `required`? seems fixed. coerce(cmd.meta.coerces), // casting should be before `required` required(cmd.meta.required, cmd.meta.aliases), )(parsedFlags); const { _: positionals, __GLOBAL_OPTIONS__, ...flags } = argv; cmd.argv = { ...flags, _: positionals }; // !NOTE: if global options is defined, then the current cmd is used as subcommand to a rootCommand cmd.globalOptions = __GLOBAL_OPTIONS__; cmd.usedAsSubcommand = Boolean(cmd.globalOptions); const cmdUsage = `${cmd.name === '_' ? '' : cmd.name} ${cmd.usage}`.trim(); if (args && typeof args !== 'object') { const error = new Error( `Command "${cmdUsage}" expect second argument to be object when given`, ); // @ts-ignore error.cmdUsage = cmdUsage; throw error; } const cmdArgs = cmd.args.map((x, idx) => ({ ...x, value: x.variadic ? positionals.slice(idx) : positionals[idx], })); args = args || {}; const manualCallArgs = Object.entries(args); const hasManualInput = manualCallArgs.length > 0; for (const arg of cmdArgs) { const isArr = Array.isArray(arg.value) && arg.value.length === 0; const isObj = typeof arg.value === 'object' && Object.keys(arg.value || {}).length === 0; const val = // eslint-disable-next-line no-nested-ternary 'default' in arg && (!arg.value || isArr || isObj) ? arg.default : hasManualInput ? args[arg.name] : arg.value; const isNotVal = hasManualInput ? !(arg.name in args) : !val; if (arg.required && isNotVal) { const error = new Error( `missing required argument "${arg.name}" from "${cmdUsage}" command`, ); // @ts-ignore error.cmdUsage = cmdUsage; throw error; } args[arg.name] = val; } cmd.env = cmd.globalOptions?.populateEnv || cmd.argv?.populateEnv ? { ...proc.env } : {}; return commandHandler.call(null, flags, args, cmd); } }; } };