UNPKG

nix-clap

Version:

Simple, lightweight, flexible, and comprehensive Un*x Command Line Argument Parsing for NodeJS

415 lines (348 loc) 10.8 kB
"use strict"; /* eslint-disable no-magic-numbers,no-process-exit,max-statements,prefer-template,complexity */ const assert = require("assert"); const Path = require("path"); const xtil = require("./xtil"); const symbols = require("./symbols"); const Options = require("./options"); const Commands = require("./commands"); const EventEmitter = require("events"); const Parser = require("./parser"); const objEach = xtil.objEach; const makeDefaults = xtil.makeDefaults; const applyDefaults = xtil.applyDefaults; const CMD = symbols.CMD; const HELP = Symbol("help"); class NixClap extends EventEmitter { constructor(config) { super(); config = config || {}; this._name = config.name; this._version = config.version || false; this._versionAlias = config.versionAlias; this._helpOpt = config.hasOwnProperty("help") ? config.help : { [HELP]: true, alias: config.helpAlias || ["?", "h"], type: "string", desc: () => { const cmdText = this._commands.count > 0 ? " Add a command to show its help" : ""; return `Show help.${cmdText}`; } }; this._usage = config.usage || "$0"; this._cmdUsage = config.cmdUsage || "$0 $1"; this.exit = config.exit || (n => this.emit("exit", n)); this.output = config.output || (s => process.stdout.write(s)); this._evtHandlers = { "pre-help": () => {}, help: parsed => this.showHelp(null, parsed.opts.help || parsed.optCmd.help), "post-help": () => {}, version: () => this.showVersion(), "parse-fail": parsed => this.showHelp(parsed.error), parsed: () => undefined, "unknown-option": name => { throw new Error(`Unknown option ${name}`); }, "unknown-options-v2": xtil.noop, "unknown-command": ctx => { throw new Error(`Unknown command ${ctx.name}`); }, "no-action": () => this.showHelp(new Error("No command given")), "new-command": xtil.noop, exit: code => process.exit(code) }; const handlers = config.handlers || {}; objEach(this._evtHandlers, (handler, name) => { handler = handlers.hasOwnProperty(name) ? handlers[name] : handler; if (typeof handler === "function") this.on(name, handler); }); this._skipExec = config.skipExec; this._skipExecDefault = config.skipExecDefault; this.Promise = config.Promise || Promise; } _getVersionOpt(verAlias) { return { alias: this._versionAlias || verAlias, desc: "Show version number" }; } removeDefaultHandlers(x) { const evts = x === "*" ? Object.keys(this._evtHandlers) : arguments; for (let i = 0; i < evts.length; i++) { const evtName = evts[i]; this.removeListener(evtName, this._evtHandlers[evtName]); } return this; } applyConfig(config, parsed, src) { const source = parsed.source; for (const x in config) { if (!source.hasOwnProperty(x) || source[x] !== "cli") { parsed.opts[x] = config[x]; source[x] = src || "user"; } } return this; } init(options, commands) { options = Object.assign({}, options); if (this._version) { let verAlias = ["V", "v"]; Object.keys(options).forEach(k => { const opt = options[k]; if (opt.alias) verAlias = verAlias.filter(x => opt.alias.indexOf(x) < 0); }); options.version = this._getVersionOpt(verAlias); } if (this._helpOpt) { options.help = this._helpOpt; } this._cliOptions = new Options(options); this._commands = new Commands(commands); this._verifyOptions(); this._defaults = makeDefaults(options); return this; } usage(msg) { this._usage = msg; return this; } cmdUsage(msg) { this._cmdUsage = msg; return this; } version(v) { this._version = v; return this; } help(custom) { this._helpOpt = custom; return this; } get commands() { return this._commands; } get cliOptions() { return this._cliOptions; } showVersion() { this.output(`${this._version}\n`); return this.exit(0); } makeHelp(cmdName) { let cmdCtx; let cmd; if (cmdName) { cmdCtx = this._commands.getContext(cmdName); if (cmdCtx.unknown) { return [`Unknown command: ${cmdName}`]; } cmd = cmdCtx[CMD]; } const usage = [""]; let usageMsg; if (cmd) { usageMsg = cmd.usage || this._cmdUsage; } if (!usageMsg) { usageMsg = this._usage; } if (usageMsg) { usageMsg = usageMsg.replace("$0", this._name || "").replace("$1", cmdName || ""); usage.push(`Usage: ${usageMsg}`.trim(), ""); } const options = this._cliOptions.makeHelp(); const optionHelp = options && options.length ? ["Options:"].concat(options) : []; let commandsHelp = []; if (!cmd) { const cmds = this._commands.makeHelp(this._name); commandsHelp = cmds && cmds.length ? ["Commands:"].concat(cmds, "") : []; } else if (cmd.desc) { usage.push(` ${cmd.desc}`, ""); } let cmdHelp = []; if (cmd) { cmdHelp.push(""); if (cmdCtx.name !== cmdCtx.long) { cmdHelp.push(`Command ${cmdName} is alias for ${cmdCtx.long}`); } const cmdOptions = cmd.options.makeHelp(); if (cmdOptions.length) { cmdHelp = cmdHelp.concat(`Command "${cmdCtx.long}" options:`, cmdOptions); } else { cmdHelp.push(`Command ${cmdCtx.long} has no options`); } } return usage.concat(commandsHelp, optionHelp, cmdHelp); } showHelp(err, cmdName) { this.emit("pre-help", { self: this }); this.output(`${this.makeHelp(cmdName).join("\n")}\n`); let code = 0; if (err) { this.output(`\nError: ${err.message}\n`); code = 1; } this.output("\n"); this.emit("post-help", { self: this }); return this.exit(code); } checkRequireOptions(parsed) { const missing = Object.keys(this._cliOptions._options) .filter(name => { const opt = this._cliOptions._options[name]; return opt.require && !parsed.opts.hasOwnProperty(name); }) .map(x => `'${x}'`); if (missing.length > 0) { parsed.error = Error(`Required option ${missing.join(", ")} missing`); } } skipExec() { this._skipExec = true; this._skipExecDefault = true; } parse(argv, start, parsed) { parsed = this._parse(argv, start, parsed); if (!this._skipExec && this.runExec(parsed, this._skipExecDefault) === 0) { if (this._commands.execCount > 0) { this.emit("no-action"); } } return parsed; } parseAsync(argv, start, parsed) { parsed = this._parse(argv, start, parsed); if (this._skipExec) return this.Promise.resolve(parsed); return this.runExecAsync(parsed, this._skipExecDefault).then(count => { if (count === 0 && this._commands.execCount > 0) { this.emit("no-action"); } return parsed; }); } _parse(argv, start, parsed) { if (argv === undefined) { argv = process.argv; if (this._name === undefined) { this._name = Path.basename(argv[1], ".js"); } start = 2; } const parser = new Parser(this); parsed = parser.parse(argv, start, parsed); Object.defineProperties(parsed, { _: { value: argv.slice(parsed.index + 1), enumerable: false }, argv: { value: argv, enumerable: false } }); if (!parsed.error) { this.checkRequireOptions(parsed); } if (parsed.error) { this.emit("parse-fail", parsed); return parsed; } if (this._version && parsed.opts.version) { this.emit("version", parsed); return parsed; } else if (this._helpOpt && this._helpOpt[HELP] && parsed.source.help === "cli") { this.emit("help", parsed); return parsed; } applyDefaults(this._defaults, parsed); parsed.commands.forEach(cmdCtx => { cmdCtx[CMD].applyDefaults(cmdCtx); }); this.emit("parsed", { nixClap: this, parsed }); return parsed; } runExec(parsed, skipDefault) { const count = this._execCmds(parsed); if (count > 0) return count; if (skipDefault === true) return 0; return this._runDefaultCmd(parsed); } runExecAsync(parsed, skipDefault) { return this._execCmdsAsync(parsed).then(count => { if (count > 0) return count; if (skipDefault === true) return 0; return this._runDefaultCmd(parsed); }); } _runDefaultCmd(parsed) { const defaultCmd = this._commands.defaultCmd; if (!defaultCmd) return 0; const defaultParsed = this._parse([defaultCmd], 0); const defaultCmdCtx = defaultParsed.commands[0]; return this._doExec(parsed, defaultCmdCtx); } _verifyOptions() { const top = this.cliOptions.list; const topAlias = this.cliOptions.alias; objEach(this.commands.list, (cmd, cmdName) => { objEach(cmd.options.list, (opt, optName) => { assert( !top.hasOwnProperty(optName), `Command ${cmdName} option ${optName} conflicts with top level option` ); assert( !topAlias.hasOwnProperty(optName), `Command ${cmdName} option ${optName} conflicts with top level alias` ); }); objEach(cmd.options.alias, (optName, aliasName) => { assert( !top.hasOwnProperty(aliasName), `Command ${cmdName} option ${optName} alias ${aliasName} conflicts with top level option` ); assert( !topAlias.hasOwnProperty(aliasName), `Command ${cmdName} option ${optName} alias ${aliasName} conflicts with top level alias` ); }); }); } _doExec(parsed, cmdCtx) { const cmd = cmdCtx[CMD]; if (cmd.exec) { const source = Object.assign({}, parsed.source, cmdCtx.source); const opts = Object.assign({}, parsed.opts, cmdCtx.opts); return ( cmd.exec( { name: cmdCtx.name, long: cmdCtx.long, source, opts, args: cmdCtx.args, argList: cmdCtx.argList }, parsed ) || true ); } return false; } _execCmds(parsed) { let count = 0; parsed.commands.forEach(cmdCtx => { count += this._doExec(parsed, cmdCtx) ? 1 : 0; }); return count; } _execCmdsAsync(parsed) { let count = 0; return parsed.commands .reduce((promise, cmdCtx) => { return promise.then(() => { const r = this._doExec(parsed, cmdCtx); if (r) count++; return r; }); }, this.Promise.resolve()) .then(() => count); } } module.exports = NixClap;