modules/cmd/options.js

/**
 * Parse a command line options & environment variables and create a configuration object.
 *
         var
            cmdLineOpt = require('cmd/options'),
            argv = require('cmd/argv');

         var opts = cmdLineOpt.describe('cmd/generateDDL', 'Check database structure for application domain. Generate DDL (both create and alter) if need and optionally run it')
            .add(argv.establishConnectionFromCmdLineAttributes._cmdLineParams)
            .add({short: 'm',  long: 'models', param: 'modelsList', defaultValue: '*',  help: 'Comma separated model names for DDL generation. If -e specified this options is ignored'})
            .add({short: 'e',  long: 'entities', param: 'entitiesList', defaultValue: '*',  help: 'Comma separated entity names list for DDL generation'})
            .add({short: 'out',  long: 'out', param: 'outputPath', defaultValue: process.cwd(),  help: 'Folder to output generated DDLs (one file per connection)'})
            .add({short: 'autorun',  long: 'autorun', defaultValue: false,  help: 'execute DDL statement after generation. BE CAREFUL! DO NOT USE ON PRODUCTION'});
         options = opts.parseVerbose({}, true);

 *
 * @author pavel.mash
 * @module cmd/options
 */

//[{short: 'u', long: 'user', param: 'userName', defaultValue: true, searchInEnv: true, help: 'A user name for server connection'}]
/**
 * @class
 * @param commandName
 * @param commandDescription
 * @constructor
 */
function Options(commandName, commandDescription){
  this.commandName = commandName || '';
  this.commandDescription = commandDescription || '';
  this.options = [];
}

/**
 * @typedef {Object} Option
 * @property {String} short - a short option name
 * @property {String} long - a long option name. This name are used in `parse` result
 * @property {String} [param] - if parameter has a value - help string for a parameter name `-short param`
 * @property {*} [defaultValue] - a default value for a property. For a string properties what allow empty value set it to `*`
 * @property {Boolean} [searchInEnv=false] - if property do not passed as a cmd line switch then
 *    perform search of `UB_`+long.toUpperCase() in environment variables
 * @property {String} help - a help string for a `usage()` call
 */
/**
 * Add a option(s) definition.
 * @param {Option|Array.<Option>} otherOptions
 * @return {Options}
 */
Options.prototype.add = function add(otherOptions){
    if (Array.isArray(otherOptions)){
        this.options = this.options.concat(otherOptions);
    } else {
        this.options.push(otherOptions);
    }
    return this;
};

/**
 *
 * Parse a command line & env variables for a options and create a configuration object.
 * Return `undefined` in case options is not valid or a object with keys - options.long & values
 * @param {Object} [defaults] Override for a command line attributes. If any - this one will be used
 * @param {Array} [errors] If passed will bw filled by a errors in passed parameters
 */
Options.prototype.parse = function parse(defaults, errors){
    var result = _.defaults({}, defaults);
    //[{short: 'u', long: 'user', param: 'userName', defaultValue: true, searchInEnv: true, help: 'A user name for server connection'}]
    var valid = true;
    this.options.forEach(function(option){
        var val, t;
        if (!result.hasOwnProperty(option.long)) { // not passed in defaults
            if (option.param) { // option with parameter `-http register`
                val = switchValue(option.short);
                if (typeof val === 'undefined') {
                    val = switchValue(option.long);
                }
                if ((typeof val === 'undefined') && option.searchInEnv) {
                    val = process.env['UB_' + option.long.toUpperCase()]
                }
            } else { // boolean option without parameter `-createDB`
                if (switchIndex(option.short) !== -1) {
                    val = true
                } else if (switchIndex(option.long) !== -1) {
                    val = true
                } else if (option.searchInEnv) {
                    t = process.env['UB_' + option.long.toUpperCase()];
                    if (typeof  t !== 'undefined') {
                        val = (t === 'true') || (t === 'TRUE');
                    }
                }
            }
            if ((typeof val === 'undefined')) {
                val = option.defaultValue;
            }
            if ((typeof val === 'undefined')) {
                valid = false;
                if (errors){
                    errors.push('expected parameter "' + option.long + '" not found');
                }
            } else {
                result[option.long] = (val === '*' ? '' : val);
            }
        }
    });
    return valid ? result : undefined;

};

/**
 * In case `-help` or '-?' command line switch found or passed options not match a options set
 * will output a usage help to console and return `undefined`, else - return a parsed options
 *
 * @param {Object} [defaults] - Override passed parameter values by this one
 * @param {Boolean} [outputParsed=false] output a parsed parameters to a log
 */
Options.prototype.parseVerbose = function parseVerbose(defaults, outputParsed){
    var result, errors = [];
    if (switchIndex('?') !==-1 || switchIndex('help') !==-1) {
        console.log(this.usage());
    } else {
        result = this.parse(defaults, errors);

        if (!result) console.log(this.usage());
        if (errors.length) {
            console.error('\nInvalid usage');
            console.error('\t' + errors.join('\r\t'));
        }
        if (outputParsed) {
            console.info('Run a command "%s" using %j', this.commandName, result);
        }
    }
    return result;
};

Options.prototype.howParamsAppearInCommandLine = function(){
    var res = [], elm;
    this.options.forEach(function(option){
        elm = '-' + option.short + (option.param ? ' ' + option.param : '');
        if (option.defaultValue) elm = '[' + elm + ']';
        res.push(elm);
    });
    return res.join('  ');
};

/**
 * Output a usage info to console
 */
Options.prototype.usage = function usage(){
    if (this.commandDescription){
        console.info('\n' + this.commandDescription);
    }
    console.info('\nUsage: ub %s %s', this.commandName, this.howParamsAppearInCommandLine());
    console.info('\nwhere');
    var envs = [];
    var res = [], elm;
    // create a parameters description
    this.options.forEach(function(option){
        elm = '-' + option.short;
        if (option.short !== option.long) elm += ' | ' + option.long;
        if (option.searchInEnv) elm += '*';
        elm += '\t' + option.help;
        if (typeof option.defaultValue !== 'undefined') elm += ', default: ' + option.defaultValue;
        res.push(elm);
        if (option.searchInEnv) {
            envs.push('UB_' + option.long.toUpperCase());
        }
    });
    res = '\t' + res.join('\n\t');
    if (envs.length){
        res += '\n\n* will lookup a environment variable in case switch omitted: ' + JSON.stringify(envs);
    }
    return res;
};

/**
 * Create a new options definition.
 * @example

 var cmdLineOpt = require('cmd/options');
 var opts = cmdLineOpt.describe('cmd/createStore',
        'Create internal store structure (folders) for specifies FileSystem store'
     )
     .add({short: 'cfg',  long: 'cfg', param: 'serverConfig', defaultValue: 'ubConfig.json',  help: 'Server config'})
     .add({short: 'store',  long: 'store', param: 'storesList', defaultValue: '*',  help: 'Comma separated blob stores list'});
 options = opts.parseVerbose({}, true);

 * @param {String} commandName Name of a command then executed from a command line
 * @param {String} commandDescription Command description for help (-help switch)
 * @return {Options}
 */
exports.describe = function describe(commandName, commandDescription){
    return new Options(commandName || '', commandDescription || '');
};

/**
 * Determines whether a switchName was passed as a command-line argument to the application
 * Switch may be specified in the following ways on the command line:
 *      -switchName
 *      or
 *      /switchName
 * @param switchName
 * @returns {Number} switch index if found or -1 otherwise
 */
exports.switchIndex = function switchIndex(switchName){
    var
        res = process.argv.indexOf('-' + switchName);
    return (res === -1) ? process.argv.indexOf('/' + switchName) : res;
};
var switchIndex = exports.switchIndex;
/**
 * Determines whether a switchName was passed as a command-line argument to the application and have VALUE
 * Switch values may be specified in the following ways on the command line:
 *      -switchName Value
 *      or
 *      /switchName Value
 * @param switchName
 * @returns {String} switch value or `undefined` in case switch not found or switch not have value
 */
exports.switchValue = function switchValue(switchName){
    var
        idx = switchIndex(switchName) + 1,
        val;
    return (idx && (val = process.argv[idx]) && val.charAt !== '-' && val.charAt !== '/') ? process.argv[idx]: undefined;
};
var switchValue = exports.switchValue;