UNPKG

synopt

Version:

Command line options package

160 lines (159 loc) 6.12 kB
const Ok = (options) => ({ ok: true, options }); const Err = (error) => ({ ok: false, error, }); /** * Parse an option declaration list into an argument list parser * * The object representing the option interface also parses an argument vector * into an concrete options object. */ const parseDeclaration = (declaration) => { const decl = declaration.slice().reverse(); const option = {}; const reLong = /^(--([-\w]+?))( (<?\w+?>?)?)?$/; const reShort = /^-[^-]$/; while (decl.length > 0) { const elem = decl.pop(); if (decl.length === 0 && typeof elem === "object") { if (elem.boolean) { option.boolean = true; } if (elem.repeat) { option.repeat = true; } } else { const str = elem; if (reShort.test(str)) { option.short = str; } else if (reLong.test(str)) { const [, long, name, , argname] = str.match(reLong); option.long = long; option.argname = argname || name.toUpperCase(); option.name = name; } else if (option.name && !option.description) { option.description = str; } else { throw new Error(`Declaration error: ${declaration}`); } } } if (!option.name || !option.long) { throw new Error(`Option long-form option is required in declaration and used to derive a name: ${declaration}`); } return option; }; const ensureNamesUnique = (declaration, declarations) => { const { long, short } = declaration; if (declarations.find((d) => d.long === long)) throw new Error(`Duplicate option (${long})`); if (short && declarations.find((d) => d.short === short)) throw new Error(`Duplicate short option (${short})`); }; const createCommand = (name) => { const isOption = (string) => /^-/.test(string); const state = { name, optionDeclarations: [], }; const command = { name: (text) => { state.name = text; return command; }, summary: (text) => { state.summary = text; return command; }, description: (text) => { state.description = text; return command; }, option: (...args) => { const declaration = parseDeclaration(args); ensureNamesUnique(declaration, state.optionDeclarations); state.optionDeclarations.push(declaration); return command; }, parse: (args) => { const options = {}; try { for (let i = 0; i < args.length; i++) { const element = args[i]; const nextElement = args[i + 1]; const decl = state.optionDeclarations.find((d) => { return (element === d.long || element.startsWith(d.long + "=") || element.startsWith(d.short)); }); if (!decl) { throw new Error(`Unknown option (${element})`); } else if (decl.boolean) { options[decl.name] = true; } else if (element.startsWith(decl.long + "=")) { const value = element.substring(decl.long.length + 1); options[decl.name] = value; } else if (element.startsWith(decl.short) && element.length > decl.short.length) { const value = element.substring(2); options[decl.name] = value; } else if (nextElement && !isOption(nextElement)) { if (decl.repeat) { options[decl.name] ||= []; options[decl.name].push(...nextElement.split(",")); } else { options[decl.name] = nextElement; } i++; } else { throw new Error(`Option '${element}' requires value, because it's not boolean flag`); } } } catch (error) { return Err(error.message); } return Ok(options); }, declarations: () => state.optionDeclarations, usage: () => { const shorts = state.optionDeclarations.map((val) => val.short); const longs = state.optionDeclarations.map((val) => val.long); const argnames = state.optionDeclarations.map((val) => val.argname); const descriptions = state.optionDeclarations.map((val) => val.description); const longMax = Math.max(...longs.map((l, i) => { let len = l.length; if (!state.optionDeclarations[i].boolean) { len += argnames[i].length + 1; } return len; })); const anyShorts = shorts.filter((x) => x).length > 0; const optionHelpLines = longs.map((long, i) => { return ` ${anyShorts ? (shorts[i] ? shorts[i] + "," : " ") : ""} ${(long + (state.optionDeclarations[i].boolean ? "" : ` ${argnames[i]}`)).padEnd(longMax, " ")} ${descriptions[i]}`; }); return [ `Usage: ${state.name || "SCRIPT_NAME"} [options]`, ...(state.summary ? ["", state.summary] : []), ...(state.description ? ["", state.description] : []), "", ...optionHelpLines, ].join("\n"); }, }; return command; }; const synopt = createCommand(); export default synopt; export { createCommand };