| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 | /* 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;
 |