| /* eslint-disable class-methods-use-this */ |
| 'use strict'; |
| |
| const |
| UTIL = require('util'), |
| PATH = require('path'), |
| EOL = require('os').EOL, |
| |
| Q = require('q'), |
| chalk = require('chalk'), |
| |
| CoaObject = require('./coaobject'), |
| Opt = require('./opt'), |
| Arg = require('./arg'), |
| completion = require('./completion'); |
| |
| /** |
| * Command |
| * |
| * Top level entity. Commands may have options and arguments. |
| * |
| * @namespace |
| * @class Cmd |
| * @extends CoaObject |
| */ |
| class Cmd extends CoaObject { |
| /** |
| * @constructs |
| * @param {COA.Cmd} [cmd] parent command |
| */ |
| constructor(cmd) { |
| super(cmd); |
| |
| this._parent(cmd); |
| this._cmds = []; |
| this._cmdsByName = {}; |
| this._opts = []; |
| this._optsByKey = {}; |
| this._args = []; |
| this._api = null; |
| this._ext = false; |
| } |
| |
| static create(cmd) { |
| return new Cmd(cmd); |
| } |
| |
| /** |
| * Returns object containing all its subcommands as methods |
| * to use from other programs. |
| * |
| * @returns {Object} |
| */ |
| get api() { |
| // Need _this here because of passed arguments into _api |
| const _this = this; |
| this._api || (this._api = function () { |
| return _this.invoke.apply(_this, arguments); |
| }); |
| |
| const cmds = this._cmdsByName; |
| Object.keys(cmds).forEach(cmd => { this._api[cmd] = cmds[cmd].api; }); |
| |
| return this._api; |
| } |
| |
| _parent(cmd) { |
| this._cmd = cmd || this; |
| |
| this.isRootCmd || |
| cmd._cmds.push(this) && |
| this._name && |
| (this._cmd._cmdsByName[this._name] = this); |
| |
| return this; |
| } |
| |
| get isRootCmd() { |
| return this._cmd === this; |
| } |
| |
| /** |
| * Set a canonical command identifier to be used anywhere in the API. |
| * |
| * @param {String} name - command name |
| * @returns {COA.Cmd} - this instance (for chainability) |
| */ |
| name(name) { |
| super.name(name); |
| |
| this.isRootCmd || |
| (this._cmd._cmdsByName[name] = this); |
| |
| return this; |
| } |
| |
| /** |
| * Create new or add existing subcommand for current command. |
| * |
| * @param {COA.Cmd} [cmd] existing command instance |
| * @returns {COA.Cmd} new subcommand instance |
| */ |
| cmd(cmd) { |
| return cmd? |
| cmd._parent(this) |
| : new Cmd(this); |
| } |
| |
| /** |
| * Create option for current command. |
| * |
| * @returns {COA.Opt} new option instance |
| */ |
| opt() { |
| return new Opt(this); |
| } |
| |
| /** |
| * Create argument for current command. |
| * |
| * @returns {COA.Opt} new argument instance |
| */ |
| arg() { |
| return new Arg(this); |
| } |
| |
| /** |
| * Add (or set) action for current command. |
| * |
| * @param {Function} act - action function, |
| * invoked in the context of command instance |
| * and has the parameters: |
| * - {Object} opts - parsed options |
| * - {String[]} args - parsed arguments |
| * - {Object} res - actions result accumulator |
| * It can return rejected promise by Cmd.reject (in case of error) |
| * or any other value treated as result. |
| * @param {Boolean} [force=false] flag for set action instead add to existings |
| * @returns {COA.Cmd} - this instance (for chainability) |
| */ |
| act(act, force) { |
| if(!act) return this; |
| |
| (!this._act || force) && (this._act = []); |
| this._act.push(act); |
| |
| return this; |
| } |
| |
| /** |
| * Make command "helpful", i.e. add -h --help flags for print usage. |
| * |
| * @returns {COA.Cmd} - this instance (for chainability) |
| */ |
| helpful() { |
| return this.opt() |
| .name('help') |
| .title('Help') |
| .short('h') |
| .long('help') |
| .flag() |
| .only() |
| .act(function() { |
| return this.usage(); |
| }) |
| .end(); |
| } |
| |
| /** |
| * Adds shell completion to command, adds "completion" subcommand, |
| * that makes all the magic. |
| * Must be called only on root command. |
| * |
| * @returns {COA.Cmd} - this instance (for chainability) |
| */ |
| completable() { |
| return this.cmd() |
| .name('completion') |
| .apply(completion) |
| .end(); |
| } |
| |
| /** |
| * Allow command to be extendable by external node.js modules. |
| * |
| * @param {String} [pattern] Pattern of node.js module to find subcommands at. |
| * @returns {COA.Cmd} - this instance (for chainability) |
| */ |
| extendable(pattern) { |
| this._ext = pattern || true; |
| return this; |
| } |
| |
| _exit(msg, code) { |
| return process.once('exit', function(exitCode) { |
| msg && console[code === 0 ? 'log' : 'error'](msg); |
| process.exit(code || exitCode || 0); |
| }); |
| } |
| |
| /** |
| * Build full usage text for current command instance. |
| * |
| * @returns {String} usage text |
| */ |
| usage() { |
| const res = []; |
| |
| this._title && res.push(this._fullTitle()); |
| |
| res.push('', 'Usage:'); |
| |
| this._cmds.length |
| && res.push([ |
| '', '', chalk.redBright(this._fullName()), chalk.blueBright('COMMAND'), |
| chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]') |
| ].join(' ')); |
| |
| (this._opts.length + this._args.length) |
| && res.push([ |
| '', '', chalk.redBright(this._fullName()), |
| chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]') |
| ].join(' ')); |
| |
| res.push( |
| this._usages(this._cmds, 'Commands'), |
| this._usages(this._opts, 'Options'), |
| this._usages(this._args, 'Arguments') |
| ); |
| |
| return res.join(EOL); |
| } |
| |
| _usage() { |
| return chalk.blueBright(this._name) + ' : ' + this._title; |
| } |
| |
| _usages(os, title) { |
| if(!os.length) return; |
| |
| return ['', title + ':'] |
| .concat(os.map(o => ` ${o._usage()}`)) |
| .join(EOL); |
| } |
| |
| _fullTitle() { |
| return `${this.isRootCmd? '' : this._cmd._fullTitle() + EOL}${this._title}`; |
| } |
| |
| _fullName() { |
| return `${this.isRootCmd? '' : this._cmd._fullName() + ' '}${PATH.basename(this._name)}`; |
| } |
| |
| _ejectOpt(opts, opt) { |
| const pos = opts.indexOf(opt); |
| if(pos === -1) return; |
| |
| return opts[pos]._arr? |
| opts[pos] : |
| opts.splice(pos, 1)[0]; |
| } |
| |
| _checkRequired(opts, args) { |
| if(this._opts.some(opt => opt._only && opts.hasOwnProperty(opt._name))) return; |
| |
| const all = this._opts.concat(this._args); |
| let i; |
| while(i = all.shift()) |
| if(i._req && i._checkParsed(opts, args)) |
| return this.reject(i._requiredText()); |
| } |
| |
| _parseCmd(argv, unparsed) { |
| unparsed || (unparsed = []); |
| |
| let i, |
| optSeen = false; |
| while(i = argv.shift()) { |
| i.indexOf('-') || (optSeen = true); |
| |
| if(optSeen || !/^\w[\w-_]*$/.test(i)) { |
| unparsed.push(i); |
| continue; |
| } |
| |
| let pkg, cmd = this._cmdsByName[i]; |
| if(!cmd && this._ext) { |
| if(this._ext === true) { |
| pkg = i; |
| let c = this; |
| while(true) { // eslint-disable-line |
| pkg = c._name + '-' + pkg; |
| if(c.isRootCmd) break; |
| c = c._cmd; |
| } |
| } else if(typeof this._ext === 'string') |
| pkg = ~this._ext.indexOf('%s')? |
| UTIL.format(this._ext, i) : |
| this._ext + i; |
| |
| let cmdDesc; |
| try { |
| cmdDesc = require(pkg); |
| } catch(e) { |
| // Dummy |
| } |
| |
| if(cmdDesc) { |
| if(typeof cmdDesc === 'function') { |
| this.cmd().name(i).apply(cmdDesc).end(); |
| } else if(typeof cmdDesc === 'object') { |
| this.cmd(cmdDesc); |
| cmdDesc.name(i); |
| } else throw new Error('Error: Unsupported command declaration type, ' |
| + 'should be a function or COA.Cmd() object'); |
| |
| cmd = this._cmdsByName[i]; |
| } |
| } |
| |
| if(cmd) return cmd._parseCmd(argv, unparsed); |
| |
| unparsed.push(i); |
| } |
| |
| return { cmd : this, argv : unparsed }; |
| } |
| |
| _parseOptsAndArgs(argv) { |
| const opts = {}, |
| args = {}, |
| nonParsedOpts = this._opts.concat(), |
| nonParsedArgs = this._args.concat(); |
| |
| let res, i; |
| while(i = argv.shift()) { |
| if(i !== '--' && i[0] === '-') { |
| const m = i.match(/^(--\w[\w-_]*)=(.*)$/); |
| if(m) { |
| i = m[1]; |
| this._optsByKey[i]._flag || argv.unshift(m[2]); |
| } |
| |
| const opt = this._ejectOpt(nonParsedOpts, this._optsByKey[i]); |
| if(!opt) return this.reject(`Unknown option: ${i}`); |
| |
| if(Q.isRejected(res = opt._parse(argv, opts))) return res; |
| |
| continue; |
| } |
| |
| i === '--' && (i = argv.splice(0)); |
| Array.isArray(i) || (i = [i]); |
| |
| let a; |
| while(a = i.shift()) { |
| let arg = nonParsedArgs.shift(); |
| if(!arg) return this.reject(`Unknown argument: ${a}`); |
| |
| arg._arr && nonParsedArgs.unshift(arg); |
| if(Q.isRejected(res = arg._parse(a, args))) return res; |
| } |
| } |
| |
| return { |
| opts : this._setDefaults(opts, nonParsedOpts), |
| args : this._setDefaults(args, nonParsedArgs) |
| }; |
| } |
| |
| _setDefaults(params, desc) { |
| for(const item of desc) |
| item._def !== undefined && |
| !params.hasOwnProperty(item._name) && |
| item._saveVal(params, item._def); |
| |
| return params; |
| } |
| |
| _processParams(params, desc) { |
| const notExists = []; |
| |
| for(const item of desc) { |
| const n = item._name; |
| |
| if(!params.hasOwnProperty(n)) { |
| notExists.push(item); |
| continue; |
| } |
| |
| const vals = Array.isArray(params[n])? params[n] : [params[n]]; |
| delete params[n]; |
| |
| let res; |
| for(const v of vals) |
| if(Q.isRejected(res = item._saveVal(params, v))) |
| return res; |
| } |
| |
| return this._setDefaults(params, notExists); |
| } |
| |
| _parseArr(argv) { |
| return Q.when(this._parseCmd(argv), p => |
| Q.when(p.cmd._parseOptsAndArgs(p.argv), r => ({ |
| cmd : p.cmd, |
| opts : r.opts, |
| args : r.args |
| }))); |
| } |
| |
| _do(inputPromise) { |
| return Q.when(inputPromise, input => { |
| return [this._checkRequired] |
| .concat(input.cmd._act || []) |
| .reduce((res, act) => |
| Q.when(res, prev => act.call(input.cmd, input.opts, input.args, prev)), |
| undefined); |
| }); |
| } |
| |
| /** |
| * Parse arguments from simple format like NodeJS process.argv |
| * and run ahead current program, i.e. call process.exit when all actions done. |
| * |
| * @param {String[]} argv - arguments |
| * @returns {COA.Cmd} - this instance (for chainability) |
| */ |
| run(argv) { |
| argv || (argv = process.argv.slice(2)); |
| |
| const cb = code => |
| res => res? |
| this._exit(res.stack || res.toString(), (res.hasOwnProperty('exitCode')? res.exitCode : code) || 0) : |
| this._exit(); |
| |
| Q.when(this.do(argv), cb(0), cb(1)).done(); |
| |
| return this; |
| } |
| |
| /** |
| * Invoke specified (or current) command using provided |
| * options and arguments. |
| * |
| * @param {String|String[]} [cmds] - subcommand to invoke (optional) |
| * @param {Object} [opts] - command options (optional) |
| * @param {Object} [args] - command arguments (optional) |
| * @returns {Q.Promise} |
| */ |
| invoke(cmds, opts, args) { |
| cmds || (cmds = []); |
| opts || (opts = {}); |
| args || (args = {}); |
| typeof cmds === 'string' && (cmds = cmds.split(' ')); |
| |
| if(arguments.length < 3 && !Array.isArray(cmds)) { |
| args = opts; |
| opts = cmds; |
| cmds = []; |
| } |
| |
| return Q.when(this._parseCmd(cmds), p => { |
| if(p.argv.length) |
| return this.reject(`Unknown command: ${cmds.join(' ')}`); |
| |
| return Q.all([ |
| this._processParams(opts, this._opts), |
| this._processParams(args, this._args) |
| ]).spread((_opts, _args) => |
| this._do({ |
| cmd : p.cmd, |
| opts : _opts, |
| args : _args |
| }) |
| .fail(res => (res && res.exitCode === 0)? |
| res.toString() : |
| this.reject(res))); |
| }); |
| } |
| } |
| |
| /** |
| * Convenient function to run command from tests. |
| * |
| * @param {String[]} argv - arguments |
| * @returns {Q.Promise} |
| */ |
| Cmd.prototype.do = function(argv) { |
| return this._do(this._parseArr(argv || [])); |
| }; |
| |
| module.exports = Cmd; |