Commander.js

/* globals Wsh: false */
/* globals process: false */

(function () {
  if (Wsh && Wsh.Commander) return;

  /**
   * The Command-Prompt Interfaces for WSH (Windows Script Host). Similar to {@link https://github.com/tj/commander.js|Commander.js}.
   *
   * @namespace Commander
   * @memberof Wsh
   * @requires {@link https://github.com/tuckn/WshModeJs|tuckn/WshModeJs}
   */
  Wsh.Commander = {};

  // Shorthands
  var CD = Wsh.Constants;
  var util = Wsh.Util;
  var path = Wsh.Path;

  var insp = util.inspect;
  var isArray = util.isArray;
  var isFunction = util.isFunction;
  var isString = util.isString;
  var isPlainObject = util.isPlainObject;
  var isSolidArray = util.isSolidArray;
  var isSolidString = util.isSolidString;
  var hasContent = util.hasContent;
  // var isSameStr = util.isSameMeaning;
  var obtain = util.obtainPropVal;
  var includes = util.includes;
  var startsWith = util.startsWith;
  var endsWith = util.endsWith;

  var cmd = Wsh.Commander;

  /** @constant {string} */
  var MODULE_TITLE = 'WshCore/Commander.js';

  var throwErrNonArray = function (functionName, typeErrVal) {
    util.throwTypeError('array', MODULE_TITLE, functionName, typeErrVal);
  };

  var throwErrNonObject = function (functionName, typeErrVal) {
    util.throwTypeError('object', MODULE_TITLE, functionName, typeErrVal);
  };

  var throwErrNonStr = function (functionName, typeErrVal) {
    util.throwTypeError('string', MODULE_TITLE, functionName, typeErrVal);
  };

  /** @constant {number} */
  var I_FLAG = 0;
  /** @constant {number} */
  var I_DESCRIPTION = 1;
  /** @constant {number} */
  var I_FUNC_OR_DEFVAL = 2;
  /** @constant {number} */
  var I_DEFVAL = 3;
  /** @constant {string} */
  var TYPE_SWITCH_NC = 'SWITCH_NC'; // true|false
  /** @constant {string} */
  var TYPE_SWITCH_NO = 'SWITCH_NO'; // false|true
  /** @constant {string} */
  var TYPE_VAL = 'VALUE'; // undefined|true|String

  var flagChar = '[0-9_.,!?+*$a-zA-Z]'; // @TODO Review
  var reFlagStr = new RegExp('(-' + flagChar + ')?[,\\s]\\s?(--\\S+)\\s*(\\S*)', 'i');

  var reShortFlag = new RegExp('^-(' + flagChar + ')$', 'i');
  function isShortFlag (flag) { // Ex. "-s"
    return reShortFlag.test(flag);
  }

  var reJoinedShortFlags = new RegExp('^-(' + flagChar + '{2,})$', 'i');
  function isJoinedShortFlags (flag) { // Ex. "-Cfs"
    return reJoinedShortFlags.test(flag);
  }

  var reLongFlag = new RegExp('^--(' + flagChar + '+(-' + flagChar + '+)*)$', 'i'); // @TODO Review
  function isLongFlag (flag) { // Ex. "--file", "--save-file"
    return reLongFlag.test(flag);
  }

  // _createCmdObj {{{

  /**
   * @typedef {object} typeCommandObj
   * @property {string} name
   * @property {boolean} isRequired
   * @property {boolean} isArray - The following val type
   * @property {(string|string[])} val - If isArray is true, Array. not String
   * @property {string} schema - The source schema
   */

  /**
   * @typedef {object} typeCommandObject
   * @property {string} name
   * @property {string} description
   * @property {object} version
   * @property {object} help
   * @property {Array} options
   * @property {typeCommandObj[]} args
   */

  /**
   * @private
   * @param {string} cmdSchema - Ex. "play" Ex. "play <consoleName> [gameTitle]"
   * @returns {typeCommandObject}
   */
  function _createCmdObj (cmdSchema) {
    var functionName = '_createCmdObj';
    if (!isString(cmdSchema)) throwErrNonStr(functionName, cmdSchema);

    var cmdObj = {
      name: '',
      description: '',
      args: [],
      version: {},
      help: {},
      options: []
    };

    if (!isSolidString(cmdSchema)) return cmdObj;

    var matches = cmdSchema.match(/(\S+)\s*(.*)/i);

    // 1. Main Command
    var userCmdSchema = matches[1];
    if (!isSolidString(userCmdSchema)) {
      throw new Error('\nError: [Invalid CommandSchema(main)]: ' + cmdSchema + '\n'
        + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
    }

    cmdObj.name = userCmdSchema;

    // 2. Subcommands (= Arguments)
    var argsSchema = matches[2];
    if (!isSolidString(argsSchema)) return cmdObj;

    var argSchemas = argsSchema.split(/\s+/);
    argSchemas.forEach(function (argSchema) {
      var argMatches = argSchema.match(/^([<[])(\S+)([>\]])$/i);
      if (!isSolidArray(argMatches)) {
        throw new Error('\nError: [Invalid CommandSchema(arg)]: ' + cmdSchema + '\n'
          + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
      }

      var argOpen = argMatches[1];
      var argName = argMatches[2];
      var argClose = argMatches[3];

      if (!isSolidString(argOpen) || !isSolidString(argName) || !isSolidString(argClose)) {
        throw new Error('\nError: [Invalid CommandSchema(argVal)]: ' + cmdSchema + '\n'
          + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
      }

      var argObj = {
        name: argName,
        isSpecified: false,
        isRequired: false,
        schema: argSchema,
        val: undefined
      };

      if (argOpen === '<' && argClose === '>') {
        argObj.isRequired = true;
      } else if (argOpen === '[' && argClose === ']') {
        argObj.isRequired = false;
      } else {
        throw new Error('\nError: [Invalid CommandSchema](argType): ' + cmdSchema + '\n'
          + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
      }

      if (endsWith(argName, '...')) {
        argObj.isArray = true;
        argObj.val = [];
      }

      cmdObj.args.push(argObj);
    });

    return cmdObj;
  } // }}}

  // _camelcase {{{
  /**
   * Camel-case the given `flag`
   *
   * @private
   * @param {string} flag
   * @returns {string}
   */
  function _camelcase (flag) {
    if (!isSolidString(flag)) return '';
    return flag.split('-').reduce(function (str, word) {
      return str + word.charAt(0).toUpperCase() + word.slice(1);
    });
  } // }}}

  // _getLongFlagVarName {{{
  /**
   * Returns option name.
   *
   * @private
   * @param {string} longFlag
   * @returns {string}
   */
  function _getLongFlagVarName (longFlag) {
    if (!isSolidString(longFlag)) return '';
    return _camelcase(longFlag.replace(/^--/, '').replace(/^no-/i, ''));
  } // }}}

  // _showCmdVersionAndExit {{{
  /**
   * @private
   * @param {object} cmdObj
   * @param {string} arg
   * @returns {void}
   */
  function _showCmdVersionAndExit (cmdObj, arg) {
    if (!hasContent(cmdObj.version)) return;

    var sFlag = cmdObj.version.shortFlag; // Shorthand
    var lFlag = cmdObj.version.longFlag;

    var shows;

    // A short flag (e.g. "-V")
    if (isShortFlag(arg)) {
      shows = (arg === sFlag);
    // Joined short flags (e.g. "-sVc")
    } else if (isJoinedShortFlags(arg)) {
      shows = includes(arg, sFlag.substring(1));
    // A long flags (e.g. "--version")
    } else if (isLongFlag(arg)) {
      shows = (arg === lFlag);
    }

    if (shows) {
      console.log(cmdObj.version.val);
      process.exit(CD.runs.ok);
    }
  } // }}}

  // _creatVersionOptionObj {{{

  /**
   * @typedef {array} typeVersionSchema
   * @example
   * ['0.0.1', '-v, --vers', 'Output the current version']
   * @property {string} version
   * @property {string} flagSchema
   * @property {string} [description]
   */

  /**
   * @typedef {object} typeVersionObject
   * @example
   * { name: 'vers',
   *   shortFlag: '-v',
   *   longFlag: '--ver',
   *   description: 'Output the current version',
   *   val: '1.0.0',
   *   schema: ['0.0.1', '-v, --vers', 'Output the current version'] }
   * @property {string} name
   * @property {string} shortFlag
   * @property {string} longFlag
   * @property {string} description
   * @property {(string|typeVersionSchema)} schema
   */

  /**
   * @private
   * @example
   * // Ex. 1
   * _creatVersionOptionObj('0.0.1');
   *
   * // Ex. 2
   * _creatVersionOptionObj([
   *   '0.0.1', '-v, --vers', 'Output the current version']);
   * @param {(string|typeVersionSchema)} [versionSchema={}]
   * @returns {typeVersionObject}
   */
  function _creatVersionOptionObj (versionSchema) {
    var functionName = '_creatVersionOptionObj';

    if (!hasContent(versionSchema)) return {};

    var verOptObj = {
      name: 'version',
      shortFlag: '-V',
      longFlag: '--version',
      description: 'Output the version number',
      schema: versionSchema
    };

    if (isString(versionSchema)) {
      verOptObj.val = versionSchema;
      return verOptObj;
    }

    if (!isArray(versionSchema)) throwErrNonArray(functionName, versionSchema);

    verOptObj.val = versionSchema[0];

    var flagMatches = versionSchema[1].match(reFlagStr);
    if (!hasContent(flagMatches)) {
      throw new Error('\nError: [Invalid VersionSchema]: ' + insp(versionSchema) + '\n'
        + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
    }

    verOptObj.shortFlag = flagMatches[1];
    verOptObj.longFlag = flagMatches[2];

    if (!hasContent(versionSchema[3])) return verOptObj;
    verOptObj.description = versionSchema[3];

    return verOptObj;
  } // }}}

  // _createOptionObjs {{{

  /**
   * @typedef {array} typeOptionSchema
   * @example
   * // Ex.1
   * ['-O, --switch-no', 'Normally opened switch']
   *
   * // Ex. 2
   * ['-I, --increment <Number>', 'Example of processing', function (num, pre) {
   *   return pre + num;
   * }, 3]
   * @property {string} flagSchema - Ex. '-O, --switch-no'
   * @property {string} [description]
   * @property {Function} [process]
   * @property {any} [default] - The default/starting value
   */

  /**
   * @typedef {object} typeOptionObject
   * @example
   * { name: "gameTitle",
   *   shortFlag: "-G",
   *   longFlag: "--game-title",
   *   valType: "VALUE",
   *   description: "A game name",
   *   isRequired: false,
   *   isPairWithVal: false,
   *   isSpecified: false,
   *   isArray: false,
   *   val: undefined,
   *   initFunc: undefined,
   *   schema: ['-G, --game-title [title]', 'A game name']
   *   valSchema: '[title]' }
   * @property {string} name - A option name
   * @property {string} shortFlag - A Short Flag. Ex. "-G"
   * @property {string} longFlag - A Long Flag. Ex. "--game-title"
   * @property {string} valType - "VALUE",
   * @property {string} description
   * @property {boolean} isRequired
   * @property {boolean} isPairWithVal
   * @property {boolean} isSpecified
   * @property {boolean} isArray
   * @property {string|undefined} val
   * @property {function|undefined} initFunc
   * @property {typeOptionSchema} schema - The source schema. Ex. ['-G, --game-title [title]', 'A game name']
   * @property {string} valSchema: '[title]'
   */

  /**
   * Creates a option object from the option schema. Similar to {@link https://www.npmjs.com/package/commander#options|Commander.js Options}
   *
   * @example
   * _createOptionObjs([
   *   ["-O, --switch-no", "Normally opened switch"],
   *   ["-C, --no-switch-nc", "Normally closed switch"],
   *   ['-o, --opt-word [Bar]', '[Bar] is arbitrary'],
   *   ['-a, --array-vals <val...>', 'Store as Array']
   * ]);
   * @private
   * @param {typeOptionSchema[]} optsSchemas
   * @param {boolean} [isRequired=false]
   * @returns {typeOptionObject[]} - See {@link typeOptionObject}
   */
  function _createOptionObjs (optsSchemas, isRequired) {
    var functionName = '_createOptionObjs';
    if (!isArray(optsSchemas)) throwErrNonArray(functionName, optsSchemas);

    var options = [];
    if (!isSolidArray(optsSchemas)) return options;

    optsSchemas.forEach(function (schema) {
      var flagMatches = schema[I_FLAG].match(reFlagStr);
      if (!isSolidArray(flagMatches)) {
        throw new Error('\nError: [Invalid OptionsSchema(flag)]: ' + insp(schema) + '\n'
          + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
      }

      var shortFlag = flagMatches[1]; // Ex. "-f"
      var longFlag = flagMatches[2]; // Ex. "--file"

      var optObj = {
        name: _getLongFlagVarName(longFlag),
        shortFlag: shortFlag,
        longFlag: longFlag,
        description: schema[I_DESCRIPTION] || '',
        isRequired: Boolean(isRequired),
        isSpecified: false,
        isPairWithVal: false,
        schema: schema,
        val: undefined
      };

      var valSchema = flagMatches[3].trim();
      if ((/^--no-/i).test(longFlag)) {
        optObj.valType = TYPE_SWITCH_NC;
        optObj.val = true;
      } else if (!hasContent(valSchema)) {
        optObj.valType = TYPE_SWITCH_NO;
        optObj.val = false;
      } else if ((/^<.+>$/i).test(valSchema)) { // Ex. "<Hoo>"
        optObj.isPairWithVal = true;
        optObj.valSchema = valSchema;
        optObj.valType = TYPE_VAL;

        if (endsWith(valSchema, '...>')) { // Ex. "<Bar...>"
          optObj.isArray = true;
          optObj.val = [];
        } else {
          optObj.isArray = false;
          optObj.val = undefined;
        }
      } else if ((/^\[.+\]$/i).test(valSchema)) { // Ex. "[Hoge]"
        optObj.valSchema = valSchema;
        optObj.valType = TYPE_VAL;

        if (endsWith(valSchema, '...]')) { // Ex. "[Piyo...]"
          optObj.isArray = true;
          optObj.val = [];
        } else {
          optObj.isArray = false;
          optObj.val = undefined;
        }
      } else {
        throw new Error('\nError: [Invalid OptionsSchema(val)]: ' + insp(schema) + '\n'
          + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
      }

      /**
       * Set pre-function and default-value
       * @note If default-value is specified, isPairWithVal to false.
       */
      if (schema.length > I_FUNC_OR_DEFVAL) {
        if (isFunction(schema[I_FUNC_OR_DEFVAL])) {
          optObj.initFunc = schema[I_FUNC_OR_DEFVAL];

          if (schema.length > I_DEFVAL) {
            if (optObj.isArray) optObj.val = [schema[I_DEFVAL]];
            else optObj.val = schema[I_DEFVAL];
            optObj.isPairWithVal = false;
          }
        } else {
          if (optObj.isArray) optObj.val = [schema[I_FUNC_OR_DEFVAL]];
          else optObj.val = schema[I_FUNC_OR_DEFVAL];
          optObj.isPairWithVal = false;
        }
      }


      options.push(optObj);
    });

    return options;
  } // }}}

  // _createCmdHelpMsg {{{
  /**
   * @private
   * @param {object} cmdObj
   * @returns {string}
   */
  function _createCmdHelpMsg (cmdObj) {
    // var functionName = '_createCmdHelpMsg';

    var helpMsg = 'Usage: ';

    if (cmdObj.name === '') {
      helpMsg += path.basename(process.argv[1]);
    } else {
      helpMsg += cmdObj.name;

      helpMsg += cmdObj.args.reduce(function (acc, arg) {
        return acc + ' ' + arg.schema;
      }, '');
    }

    // Create a temporary array for the help message
    var optObjs = cmdObj.options.concat([{
      // Add the help object
      shortFlag: cmdObj.help.shortFlag,
      longFlag: cmdObj.help.longFlag,
      description: cmdObj.help.description,
      valSchema: undefined,
      val: undefined
    }]);

    if (hasContent(cmdObj.version)) { // Add the version object
      optObjs.unshift({
        shortFlag: cmdObj.version.shortFlag,
        longFlag: cmdObj.version.longFlag,
        description: cmdObj.version.description,
        valSchema: undefined,
        val: undefined
      });
    }

    helpMsg += ' [options]\n\n';

    if (hasContent(cmdObj.description)) helpMsg += cmdObj.description + '\n\n';

    if (optObjs.length > 0) {
      helpMsg += 'Options:\n';

      // Get the max length of the flag character to indent
      var maxWidth = 0;
      optObjs.forEach(function (opt) {
        var width = String(opt.longFlag).length;
        /*
         * @note "--long-flag" or "--long-flag <val>"
         * attention the space between Flag and Val
         */
        width += opt.valSchema ? (String(opt.valSchema).length + 1) : 0;
        if (width > maxWidth) maxWidth = width;
      });

      helpMsg += optObjs.reduce(function (acc, opt) {
        if (!hasContent(opt.shortFlag)) {
          acc += '    ,';
        } else {
          acc += '  ' + opt.shortFlag + ',';
        }

        acc += ' ' + opt.longFlag;
        acc += opt.valSchema ? (' ' + opt.valSchema) : '';

        // indent
        var baseLen = opt.longFlag.length
          + (opt.valSchema ? (String(opt.valSchema).length + 1) : 0);

        for (var i = baseLen; i < maxWidth; i++) acc += ' ';

        if (hasContent(opt.description)) acc += ' ' + opt.description;

        // In case of `--no-` flag, The default value (true) is not displayed.
        if (hasContent(opt.val) && opt.valType !== TYPE_SWITCH_NC) {
          acc += ' (default: ' + insp(opt.val) + ')';
        }
        acc += '\n';

        return acc;
      }, '');
    }

    return helpMsg;
  } // }}}

  // _createHelpOptionObj {{{

  /**
   * @typedef {array} typeHelpSchema
   * @example
   * ['-S, --show-help', 'Show the help']
   * @property {string} version
   * @property {string} flagSchema
   * @property {string} [description]
   */

  /**
   * @typedef {object} typeHelpObject
   * @example
   * { name: 'showHelp',
   *   shortFlag: '-S',
   *   longFlag: '--show-help',
   *   description: 'Show the help',
   *   val: '', // Auto create with _createCmdHelpMsg(cmdObj)
   *   schema: ['-S, --show-help', 'Show the help'] }
   * @property {string} name - The option name.
   * @property {string} shortFlag - The short flag.
   * @property {string} longFlag - The long flag.
   * @property {string} description
   * @property {string} val - A help message when shows in CLI.
   * @property {(string|typeHelpSchema)} schema - The source schema.
   */

  /**
   * @private
   * @param {typeHelpSchema} helpOptSchema
   * @returns {typeHelpObject}
   */
  function _createHelpOptionObj (helpOptSchema) {
    var functionName = '_createHelpOptionObj';
    if (!isArray(helpOptSchema)) throwErrNonArray(functionName, helpOptSchema);

    var helpOptObj = {
      name: 'help',
      shortFlag: '-h',
      longFlag: '--help',
      description: 'Output usage information',
      val: undefined,
      schema: helpOptSchema
    };

    if (!isSolidArray(helpOptSchema)) return helpOptObj;
    if (!isSolidString(helpOptSchema[I_FLAG])) {
      throwErrNonStr(functionName, helpOptSchema);
    }

    var flagMatches = helpOptSchema[I_FLAG].match(reFlagStr);
    if (!hasContent(flagMatches)) {
      throw new Error('\nError: [Invalid HelpOptionSchema(flag)]: ' + insp(helpOptSchema) + '\n'
        + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
    }

    helpOptObj.shortFlag = flagMatches[1];
    helpOptObj.longFlag = flagMatches[2];

    if (!isSolidString(helpOptSchema[I_DESCRIPTION])) return helpOptObj;
    helpOptObj.description = helpOptSchema[I_DESCRIPTION];

    return helpOptObj;
  } // }}}

  // _showCmdHelpAndExit {{{
  /**
   * @private
   * @param {object} cmdObj
   * @param {string} arg
   * @returns {void}
   */
  function _showCmdHelpAndExit (cmdObj, arg) {
    var sFlag = cmdObj.help.shortFlag; // Shorthand
    var lFlag = cmdObj.help.longFlag;

    var shows;

    // A short flag (e.g. "-h")
    if (isShortFlag(arg)) {
      shows = (arg === sFlag);
    // Joined short flags (e.g. "-shc")
    } else if (isJoinedShortFlags(arg)) {
      shows = includes(arg, sFlag.substring(1));
    // A long flags (e.g. "--help")
    } else if (isLongFlag(arg)) {
      shows = (arg === lFlag);
    }

    if (shows) {
      console.log(cmdObj.help.val);
      process.exit(CD.runs.ok);
    }
  } // }}}

  // The inner object {{{
  /**
   * @typedef {object} typeProgramObj
   * @example
   * { name: 'playgame',
   *   args: [
   *     {
   *       name: 'consoleName',
   *       isRequired: false,
   *       isArray: false,
   *       isSpecified: false,
   *       val: undefined,
   *       schema: '[consoleName]'
   *     },
   *     {}...
   *   ],
   *   description: 'Playing Game Command for CLI',
   *   version: {
   *     name: 'vers',
   *     shortFlag: '-v',
   *     longFlag: '--ver',
   *     description: 'Output the current version',
   *     val: '1.0.0',
   *     schema: ['0.0.1', '-v, --vers', 'Output the current version']
   *   },
   *   help: {
   *     name: 'showHelp',
   *     shortFlag: '-S',
   *     longFlag: '--show-help',
   *     description: 'Show the help',
   *     val: 'Usage: ... [options]\n\n ...',
   *     schema: ['-S, --show-help', 'Show help']
   *   },
   *   options: [
   *     { name: 'gameTitle',
   *       shortFlag: '-G',
   *       longFlag: '--game-title',
   *       valType: 'VALUE',
   *       description: 'A game name',
   *       isRequired: false,
   *       isPairWithVal: false,
   *       isSpecified: false,
   *       isArray: false,
   *       val: undefined,
   *       initFunc: undefined,
   *       schema: ['-G, --game-title [title]', 'A game name']
   *       valSchema: '[title]' },
   *     { ... },
   *     ...
   *   ],
   *   action: function (console, ..., options) { ... } }
   * @property {string} name - The program name.
   * @property {typeCommandObj[]} args - The program arguments.
   * @property {string} description - The program description.
   * @property {typeVersionObject} version - The object of program version.
   * @property {typeHelpObject} help - The object of program help.
   * @property {typeOptionObject[]} options - The program options.
   * @property {Function} action - The function to execute.
   */

  /**
   * The inner objects defined with Wsh.Commander.addProgram.
   *
   * @name __commands
   * @type {typeProgramObj[]}
   */
  var __commands = []; // }}}

  // cmd._getCommandObjects {{{
  /**
   * Gets the inner objects defined with Wsh.Commander.addProgram.
   *
   * @example
   * var cmd = Wsh.Commander;
   *
   * cmd.addProgram({
   *  // The program schema
   * });
   *
   * if (debug) console.dir(cmd._getCommandObjects());
   *
   * cmd.parse(process.argv);
   * @function _getCommandObjects
   * @memberof Wsh.Commander
   * @returns {typeProgramObj[]} - The inner objects defined with .addProgram().
   */
  cmd._getCommandObjects = function () {
    return __commands;
  }; // }}}

  // cmd.addProgram {{{
  /**
   * @typedef {object} typeCommanderSchema
   * @example
   * {
   *   command: 'connect <resourceName>',
   *   description: 'The command to connect my PC to a resource',
   *   version: '0.5.1',
   *   requiredOptions: [
   *     ['-p, --password', 'The password to connect']
   *   ],
   *   options: [
   *     ['-d, --domain-name <name>', 'A domain name of the resource'],
   *     ['-n, --user-name [name]', 'A user name to log in', 'Tuckn']
   *   ],
   *   action: function (resourceName, options) {
   *     if (options.domainName) {
   *       connRsrc(processName, options.password,
   *           options.domainName + '\\' + options.userName);
   *     } else {
   *       connRsrc(processName, options.password, options.userName);
   *     }
   *   }
   * }
   * @property {string} [command=''] - The command and arguments. e.g1: 'play' e.g2: 'play \<consoleName\> [gameTitle]'
   * @property {string} [description] - The command description.
   * @property {string|typeVersionSchema} [version] - The schema of command version.
   * @property {typeOptionSchema[]} [options] - An Array of command option schemas.
   * @property {typeOptionSchema[]} [requiredOptions] - An Array of required option schemas.
   * @property {Function} [action] - The function to execute.
   * @property {typeHelpSchema} [helpOption] - The schema of command help.
   */

  /**
   * Defines the program. Similar to {@link https://www.npmjs.com/package/commander#options|Commander.js Options}
   *
   * @example
   * var cmd = Wsh.Commander;
   *
   * // Ex. Command and Action
   * cmd.addProgram({
   *   command: 'play <consoleName> [gameTitle]',
   *   action: function (consoleName, gameTitle) {
   *     if (typeof gameTitle === 'string') {
   *       console.log('play ' + gameTitle + ' on ' + consoleName);
   *     } else {
   *       console.log('play ' + consoleName);
   *     }
   *   }
   * });
   * cmd.parse(process.argv);
   *
   * // D:\>cscript Run.wsf
   * // ERROR: Set a command.
   * // where <command> is one of:
   * //     play
   *
   * // D:\>cscript Run.wsf play
   * // error: missing required argument "consoleName"
   * // Usage: play <consoleName> [gameTitle]
   * //
   * // Options:
   * //   -h, --help Output usage information
   *
   * // D:\>cscript Run.wsf play "PC-Engine"
   * // play PC-Engine
   * @example
   * // Ex. Any-arguments
   * cmd.addProgram({
   *   command: 'createZip <srcDir> <destDir> [excludes...]',
   *   action: function (srcDir, destDir, excludes) {
   *     if (excludes) {
   *       return 'srcDir: "' + srcDir + '", destDir: "' + destDir + '", '
   *         + 'excludes: ' + excludes.join(', ');
   *     }
   *     return 'srcDir: "' + srcDir + '", destDir: "' + destDir + '"';
   *   }
   * });
   * var str = cmd.parse(process.argv);
   * console.log(str);
   *
   * // D:\>cscript Run.wsf createZip C:\\Users D:\\BackUp tmp temp cache
   * // srcDir: "C:\\Users", destDir: "D:\\BackUp", excludes: tmp,temp,cache
   * @example
   * // Ex. Commands
   * cmd.addProgram({
   *   command: 'play',
   *   action: function () { console.log('Your command is `play`'); }
   * });
   *
   * cmd.addProgram({
   *   command: 'study',
   *   action: function () { console.log('Your command is `study`'); }
   * });
   * cmd.parse(process.argv);
   *
   * // D:\>cscript Run.wsf study
   * // Your command is `study`
   * @example
   * // Ex. Option Switch
   * cmd.addProgram({
   *   options: [
   *     ['-C, --close1', 'Normally opened switch'], // default: false
   *     ['-O, --no-close2', 'Normally closed switch'] // default: true
   *   ]
   * });
   * cmd.parse(process.argv);
   *
   * // `D:\>cscript Run.wsf`
   * cmd.opt.close1; // false
   * cmd.opt.close2; // true
   *
   * // `D:\>cscript Run.wsf -O -C`
   * //  or `D:\>cscript Run.wsf --close1 --no-close2`
   * cmd.opt.close1; // true
   * cmd.opt.close2; // false
   * @example
   * // Ex. Option Flag value
   * cmd.addProgram({
   *   options: [
   *     ['-f, --flag [name]', 'Flag name'], // default: undefined
   *     ['-d, --flag-def [name]', 'Flag name(default: "My Name")', 'My Name'],
   *   ]
   * });
   * cmd.parse(process.argv);
   *
   * // `D:\>cscript Run.wsf`
   * cmd.opt.flag; // undefined
   * cmd.opt.flagDef; // 'My Name'
   *
   * // `D:\>cscript Run.wsf -f -d`
   * //  or `D:\>cscript Run.wsf --flag --flag-def`
   * cmd.opt.flag; // true
   * cmd.opt.flagDef; // 'My Name'
   *
   * // `D:\>cscript Run.wsf -f "Flag A" -d "Flag B"`
   * //  or `D:\>cscript Run.wsf --flag "Flag A" --flag-def "Flag B"`
   * cmd.opt.flag; // 'Flag A'
   * cmd.opt.flagDef; // 'Flag B'
   * @example
   * // Ex. Option Pair with a value
   * cmd.addProgram({
   *   options: [
   *     ['-r, --required <Foo>', 'Empty <Foo> is not allowed'],
   *     ['-R, --def-word <Bar>', 'default value is "Def Name"', 'Def Name']
   *   ]
   * });
   * cmd.parse(process.argv); // No Error
   *
   * // `D:\>cscript Run.wsf`
   * cmd.opt.required; // undefined
   * cmd.opt.defWord; // 'Def Name'
   *
   * // `D:\>cscript Run.wsf -r`
   * //  or `D:\>cscript Run.wsf --required`
   * // Outputs the help
   *
   * // `D:\>cscript Run.wsf -r "Val A"`
   * //  or `D:\>cscript Run.wsf --required "Val A"`
   * cmd.opt.required; // 'Val A'
   * @example
   * // Ex. Option Array values
   * cmd.addProgram({
   *   options: [
   *     ['-f, --flags [name...]', 'Flag name'],
   *     ['-v, --values <val...>', 'Specify your value', 'Val 0']
   *   ]
   * });
   * cmd.parse(process.argv);
   *
   * // `D:\>cscript Run.wsf`
   * cmd.opt.flags; // []
   * cmd.opt.values; // ['Val 0']
   *
   * // `D:\>cscript Run.wsf -f "Name 0" -v "Val 1"`
   * //  or `D:\>cscript Run.wsf --flags "Name 0" --values "Val 1"`
   * cmd.opt.flags; // ['Name 0']
   * cmd.opt.values; // ['Val 0', 'Val 1']
   *
   * // `D:\>cscript Run.wsf -v "Val 1" "Val 2" "Val 3"`
   * cmd.opt.values; // ['Val 0', 'Val 1', 'Val 2', 'Val 3']
   * @function addProgram
   * @memberof Wsh.Commander
   * @param {typeCommanderSchema} schemaObj - A schema of the program.
   * @returns {void}
   */
  cmd.addProgram = function (schemaObj) {
    var functionName = 'cmd.addProgram';
    if (!isPlainObject(schemaObj)) throwErrNonObject(functionName, schemaObj);

    var commandSchema = obtain(schemaObj, 'command', '');
    var optionsSchema = obtain(schemaObj, 'options', []);
    var requiredOptions = obtain(schemaObj, 'requiredOptions', []);

    if (!isString(commandSchema) && !hasContent(optionsSchema) && !hasContent(requiredOptions)) {
      throw new Error('\nError: [Invalid Schema]: Empty of command and options\n'
        + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
    }

    // Set a command
    var cmdObj = _createCmdObj(commandSchema);

    if (__commands.some(function (o) { return o.name === cmdObj.name; })) {
      throw new Error('\nError: [Invalid Command Name] "' + cmdObj.name + '" is already existing\n'
        + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
    }

    // Set a description
    var cmdDescription = obtain(schemaObj, 'description', '');
    cmdObj.description = cmdDescription;

    // Set a version
    var versionOptSchema = obtain(schemaObj, 'version', '');
    cmdObj.version = _creatVersionOptionObj(versionOptSchema);

    // Set options
    cmdObj.options = [].concat(_createOptionObjs(requiredOptions, true));
    cmdObj.options = cmdObj.options.concat(_createOptionObjs(optionsSchema, false));

    // Set a help option
    var helpOptSchema = obtain(schemaObj, 'helpOption', []);
    cmdObj.help = _createHelpOptionObj(helpOptSchema);
    cmdObj.help.val = _createCmdHelpMsg(cmdObj);

    // Set a action
    var actionSchema = obtain(schemaObj, 'action', null);
    if (isFunction(actionSchema)) {
      cmdObj.action = actionSchema;
    } else {
      cmdObj.action = null;
    }

    __commands.push(cmdObj);
  }; // }}}

  // cmd.addPrograms {{{
  /**
   * Defines the programs.
   *
   * @function addPrograms
   * @memberof Wsh.Commander
   * @param {typeCommanderSchema[]} schemaObjs - Schemas of the programs.
   * @returns {void}
   */
  cmd.addPrograms = function (schemaObjs) {
    var functionName = 'cmd.addPrograms';
    if (!isSolidArray(schemaObjs)) throwErrNonArray(functionName, schemaObjs);

    schemaObjs.forEach(function (schemaObj) {
      cmd.addProgram(schemaObj);
    });
  }; // }}}

  cmd.opt = {};

  // cmd.parse {{{
  /**
   * Parses the arguments of a WSH script with the program schemas.
   *
   * @example
   * var cmd = Wsh.Commander;
   *
   * cmd.addProgram({
   *   options: [
   *     ['-c, --console-name <name>', 'The game console name'],
   *     ['-G, --game-title [title]', 'The game name']
   *   ]
   * });
   *
   * cmd.parse(process.argv);
   *
   * // `cscript .\Run.wsf -c "SEGA Saturn" -G "StreetFighter ZERO"`
   * console.log(cmd.opt.consoleName); // 'SEGA Saturn'
   * console.log(cmd.opt.gameTitle); // 'StreetFighter ZERO'
   * @function parse
   * @memberof Wsh.Commander
   * @param {string[]} processArgv - The arguments of WSH (wscript/cscript).
   * @returns {void}
   */
  cmd.parse = function (processArgv) {
    var functionName = 'cmd.parse';
    if (!isArray(processArgv)) throwErrNonArray(functionName, processArgv);

    var args = Array.from(processArgv);

    var exePath = args.shift();
    if (exePath === undefined) {
      throw new Error('\nError: [Invalid Arguments(exePath)] ' + insp(processArgv) + '\n'
        + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
    }

    var wshPath = args.shift();
    if (wshPath === undefined) {
      throw new Error('\nError: [Invalid Arguments(wshPath)] ' + insp(processArgv) + '\n'
        + '  at ' + functionName + ' (' + MODULE_TITLE + ')');
    }

    var matchedCmdObj = null;

    // Set the command name {{{
    // Set the default command object
    var defCmdIdx = __commands.findIndex(function (cmd) {
      return cmd.name === '';
    });
    if (defCmdIdx !== -1) matchedCmdObj = __commands[defCmdIdx];

    // Read the 1st arg as a command name
    if (args.length > 0) {
      // Search the arg as a command name
      var matchedCmdIdx = __commands.findIndex(function (cmd) {
        return (cmd.name === args[0]);
      });

      // Set the matched command object
      if (matchedCmdIdx !== -1) {
        matchedCmdObj = __commands[matchedCmdIdx];
        args.shift();
      }
    }

    if (matchedCmdObj === null) {
      var cmdlist = __commands.reduce(function (acc, cmdObj, i) {
        if (i === 0) return cmdObj.name;
        return acc + ', ' + cmdObj.name;
      }, '');

      throw new Error('\n'
        + 'ERROR: Set a command.\n'
        + 'where <command> is one of:\n'
        + '    ' + cmdlist + '\n'
        + '\n'
        + 'process.argv: [' + process.argv.join(', ') + ']');
    } // }}}

    var matchedArgObjIdx;
    var matchedOptIdxs;
    var reserveOptIdx = -1;

    var arg, opt, argObj;

    while (args.length > 0) {
      arg = args.shift();
      if (arg === undefined) break;

      // Set into the reserved option {{{
      if (reserveOptIdx !== -1) {
        opt = matchedCmdObj.options[reserveOptIdx];

        if (startsWith(arg, '-') && !opt.isPairWithVal) {
          reserveOptIdx = -1; // Clear the reserved option
        } else if (opt.isArray) {
          if (startsWith(arg, '-')) {
            reserveOptIdx = -1; // Clear the reserved option
          } else {
            if (isFunction(opt.initFunc)) {
              opt.val.push(opt.initFunc(arg, opt.val));
            } else {
              opt.val.push(arg);
            }
          }
        } else {
          // Do the custom option processing
          if (isFunction(opt.initFunc)) {
            opt.val = opt.initFunc(arg, opt.val);
          } else {
            opt.val = arg;
          }

          reserveOptIdx = -1; // Clear the reserved option
          continue;
        }
      } // }}}

      // Option
      if (startsWith(arg, '-')) {
        _showCmdHelpAndExit(matchedCmdObj, arg);
        _showCmdVersionAndExit(matchedCmdObj, arg);

        matchedOptIdxs = [];

        // Get the matching options indexes {{{
        if (isShortFlag(arg)) { // A short flag (e.g. "-s")
          matchedOptIdxs.push(
            matchedCmdObj.options.findIndex(function (opt) {
              return opt.shortFlag === arg;
            })
          );
        } else if (isJoinedShortFlags(arg)) { // Joined short flags (e.g. "-sVc")
          matchedCmdObj.options.forEach(function (opt, idx) {
            if (!includes(opt.shortFlag, arg.substring(1))) return;
            matchedOptIdxs.push(idx);
          });
        } else if (isLongFlag(arg)) { // A long flags (e.g. "--file")
          matchedOptIdxs.push(
            matchedCmdObj.options.findIndex(function (opt) {
              return (opt.longFlag === arg);
            })
          );
        } // }}}

        // Set a value into .val from the flag {{{
        matchedOptIdxs.forEach(function (idx) {
          if (idx === -1) {
            throw new Error('\nError: [Invalid Option] "' + arg + '"\n'
              + '  at ' + functionName + ' (' + MODULE_TITLE + ')\n'
              + matchedCmdObj.help.val);
          }

          opt = matchedCmdObj.options[idx];
          opt.isSpecified = true;

          if (opt.valType === TYPE_SWITCH_NC) {
            if (isFunction(opt.initFunc)) { // the custom option processing
              opt.val = opt.initFunc(undefined, opt.val);
            } else {
              opt.val = false;
            }
          } else if (opt.valType === TYPE_SWITCH_NO) {
            if (isFunction(opt.initFunc)) { // the custom option processing
              opt.val = opt.initFunc(undefined, opt.val);
            } else {
              opt.val = true;
            }
          } else if (opt.valType === TYPE_VAL) {
            if (!opt.isArray && !opt.isPairWithVal) {
              if (!opt.val) opt.val = true;
            }

            reserveOptIdx = idx; // Reserve the next arg
          } else {
            throw new Error('\nError: Unknown option valType "' + opt.valType + '"\n'
              + '  at ' + functionName + ' (' + MODULE_TITLE + ')\n'
              + matchedCmdObj.help.val);
          }
        }); // }}}
      // Argument
      } else {
        // Find a empty element in the defined order
        matchedArgObjIdx = matchedCmdObj.args.findIndex(function (argObj) {
          if (argObj.isArray) return true;
          return (argObj.val === undefined);
        });

        if (matchedArgObjIdx === -1) continue;

        argObj = matchedCmdObj.args[matchedArgObjIdx];
        argObj.isSpecified = true;

        if (argObj.isArray) {
          argObj.val.push(arg);
        } else {
          argObj.val = arg;
        }
      }
    }

    // Check if the required value is not empty {{{
    matchedCmdObj.args.some(function (arg) {
      if (!arg.isRequired) return false;
      if (!hasContent(arg.val)) {
        throw new Error('\nerror: missing required argument "' + arg.name + '"\n'
          + matchedCmdObj.help.val);
      }
    });

    matchedCmdObj.options.some(function (opt) {
      if (opt.isRequired && !hasContent(opt.val)) {
        throw new Error('\nerror: option "' + opt.schema[0] + '" argument missing\n'
          + matchedCmdObj.help.val);
      }

      if (opt.isSpecified && opt.isPairWithVal && opt.val === undefined) {
        throw new Error('\nerror: option "' + opt.schema[0] + '" argument missing\n'
          + matchedCmdObj.help.val);
      }
    }); // }}}

    // Store the option name as the cmd property {{{
    matchedCmdObj.options.forEach(function (opt) {
      cmd.opt[opt.name] = opt.val;
    }); // }}}

    // Do the action {{{
    if (isFunction(matchedCmdObj.action)) {
      var cmdArgs = [];

      matchedCmdObj.args.forEach(function (argObj) {
        if (argObj.isSpecified) cmdArgs.push(argObj.val);
        else cmdArgs.push(undefined);
      });

      if (hasContent(matchedCmdObj.options)) {
        var options = {};
        matchedCmdObj.options.forEach(function (opt) {
          options[opt.name] = opt.val;
        });

        return matchedCmdObj.action.apply(null, cmdArgs.concat([options]));
      }

      return matchedCmdObj.action.apply(null, cmdArgs);
    } // }}}
  }; // }}}

  // cmd.help {{{
  /**
   * Shows the help of programs and exit with returning 1.
   *
   * @example
   * var cmd = Wsh.Commander;
   *
   * cmd.addProgram({
   *   options: [
   *     ['-c, --console-name <name>', 'The game console name'],
   *     ['-G, --game-title [title]', 'The game name']
   *   ]
   * });
   *
   * cmd.parse(process.argv);
   *
   * if (cmd.opt.consoleName === 'DUO') cmd.help();
   * @function help
   * @memberof Wsh.Commander
   * @param {Function} [callback] - A Function to run before showing help.
   * @returns {void}
   */
  cmd.help = function (callback) {
    if (isFunction(callback)) callback();
    console.log(__commands.help.val);
    process.exit(CD.runs.err);
  }; // }}}

  // cmd.clearPrograms {{{
  /**
   * Clears the programs.
   *
   * @example
   * var cmd = Wsh.Commander;
   *
   * cmd.addProgram({
   *  // The program schema
   * });
   *
   * if (debug) {
   *   cmd.clearPrograms();
   *   cmd.addProgram({
   *     // The program schema to debug
   *   });
   * }
   *
   * cmd.parse(process.argv);
   * @function clearPrograms
   * @memberof Wsh.Commander
   * @returns {void}
   */
  cmd.clearPrograms = function () {
    cmd.opt = {};
    __commands = [];
  }; // }}}
})();

// vim:set foldmethod=marker commentstring=//%s :