commander源码解读


使用:

命令行启动(源自http://www.ruanyifeng.com/blog/2015/05/command-line-with-node.html

编写package.json

{
  "name": "myServer",
  "bin": {
    "myServer": "myServer"// 文件名
  }
} 

  执行npm link,即可用myServer执行该脚本

案例:(源自http://blog.fens.me/nodejs-commander/

 

// 定义启动脚本
#!/usr/bin/env node

var program = require('commander');

function range(val) {
    return val.split('..').map(Number);
}

function list(val) {
    return val.split(',');
}

program
    .version('0.0.1')
    .usage('test')
    .option('-C, --chdir [value]', '设置服务器节点','/home/conan/server')
    .option('-c, --config [value]', '设置配置文件','./deploy.conf')
    .option('-m, --max ', '最大连接数', parseInt)
    .option('-s, --seed ', '出始种子', parseFloat)
    .option('-r,--range <a>..<b>', '阈值区间', range)
    .option('-l, --list ', 'IP列表', list)

program
    .command('deploy ')
    .description('部署一个服务节点')
    .action(function(name){
        console.log('Deploying "%s"', name);
    });

program.parse(process.argv); 

 

源码:

 

var EventEmitter = require('events').EventEmitter;
var spawn = require('child_process').spawn;
var readlink = require('graceful-readlink').readlinkSync;
var path = require('path');
var dirname = path.dirname;
var basename = path.basename;
var fs = require('fs');


/* 示例
new Option('-i, --integer <n>', 'An integer argument')
new Option('-f, --float <n>', 'A float argument')
new Option('-r, --range <a>..<b>', 'A range')
new Option('-l, --list <items>', 'A list')
new Option('-o, --optional [value]', 'An optional value')
new Option('-c, --collect [value]', 'A repeatable value')
new Option('-v, --verbose', 'A value that can be increased') */
// 以对象形式记录选项和描述,可选选项、必选选项打标记,提供获取关键字和判断关键字的方法
exports.Option = Option;
function Option(flags, description) {
  this.flags = flags;
  this.required = ~flags.indexOf('<');
  this.optional = ~flags.indexOf('[');
  this.bool = !~flags.indexOf('-no-');
  flags = flags.split(/[ ,|]+/);
  if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
  this.long = flags.shift();
  this.description = description || '';
};
Option.prototype.name = function() {
  return this.long
    .replace('--', '')
    .replace('no-', '');
};
Option.prototype.is = function(arg) {
  return arg == this.short || arg == this.long;
};


exports = module.exports = new Command();
exports.Command = Command;
function Command(name) {
  this.commands = [];
  this.options = [];
  this._execs = {};
  this._allowUnknownOption = false;
  this._args = [];
  this._name = name || '';
}

Command.prototype.__proto__=EventEmitter.prototype;

/**
 *      program
 *        .version('0.0.1')
 *        .option('-C, --chdir <path>', 'change the working directory')
 *        .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
 *        .option('-T, --no-tests', 'ignore test hook')
 *
 *      program
 *        .command('setup')
 *        .description('run remote setup commands')
 *        .action(function() {
 *          console.log('setup');
 *        });
 *
 *      program
 *        .command('exec <cmd>')
 *        .description('run the given remote command')
 *        .action(function(cmd) {
 *          console.log('exec "%s"', cmd);
 *        });
 *
 *      program
 *        .command('teardown <dir> [otherDirs...]')
 *        .description('run teardown commands')
 *        .action(function(dir, otherDirs) {
 *          console.log('dir "%s"', dir);
 *          if (otherDirs) {
 *            otherDirs.forEach(function (oDir) {
 *              console.log('dir "%s"', oDir);
 *            });
 *          }
 *        });
 *
 *      program
 *        .command('*')
 *        .description('deploy the given env')
 *        .action(function(env) {
 *          console.log('deploying "%s"', env);
 *        });
 *
 *      program.parse(process.argv);
 */
// 注册命令,加入this.commanders数组中,无desc返回Commander实例this,有返回当前命令对象Commander实例cmd
Command.prototype.command = function(name, desc, opts) {
  opts = opts || {};
  var args = name.split(/ +/);
  var cmd = new Command(args.shift());

  if (desc) {
    cmd.description(desc);
    this.executables = true;
    this._execs[cmd._name] = true;
    if (opts.isDefault) this.defaultExecutable = cmd._name;
  }

  cmd._noHelp = !!opts.noHelp;
  this.commands.push(cmd);
  cmd.parseExpectedArgs(args);// 解析[]或<>内命令行参数
  cmd.parent = this;

  if (desc) return this;
  return cmd;
};
// 调用parseExpectedArgs方法解析[]或<>内命令行参数
Command.prototype.arguments = function (desc) {
  return this.parseExpectedArgs(desc.split(/ +/));
};

Command.prototype.addImplicitHelpCommand = function() {
  this.command('help [cmd]', 'display help for [cmd]');
};
// 解析[]或<>内命令行参数,以对象形式存储在this._args中,required区分[]与<>
Command.prototype.parseExpectedArgs = function(args) {
  if (!args.length) return;
  var self = this;
  args.forEach(function(arg) {
    var argDetails = {
      required: false,
      name: '',
      variadic: false
    };

    switch (arg[0]) {
      case '<':
        argDetails.required = true;
        argDetails.name = arg.slice(1, -1);
        break;
      case '[':
        argDetails.name = arg.slice(1, -1);
        break;
    }

    if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
      argDetails.variadic = true;
      argDetails.name = argDetails.name.slice(0, -3);
    }
    if (argDetails.name) {
      self._args.push(argDetails);
    }
  });
  return this;
};

// 在执行js文件或二进制文件前,通过parseArgs方法触发回调函数fn执行
Command.prototype.action = function(fn) {
  var self = this;
  var listener = function(args, unknown) {// 参数由触发事件时带入绑定事件中,parseArgs方法触发事件
    args = args || [];
    unknown = unknown || [];

    var parsed = self.parseOptions(unknown);

    outputHelpIfNecessary(self, parsed.unknown);

    if (parsed.unknown.length > 0) {
      self.unknownOption(parsed.unknown[0]);
    }

    if (parsed.args.length) args = parsed.args.concat(args);

    self._args.forEach(function(arg, i) {
      if (arg.required && null == args[i]) {
        self.missingArgument(arg.name);
      } else if (arg.variadic) {
        if (i !== self._args.length - 1) {
          self.variadicArgNotLast(arg.name);
        }

        args[i] = args.splice(i);
      }
    });

    // Always append ourselves to the end of the arguments,
    // to make sure we match the number of arguments the user
    // expects
    if (self._args.length) {
      args[self._args.length] = self;
    } else {
      args.push(self);
    }

    fn.apply(self, args);
  };
  var parent = this.parent || this;
  var name = parent === this ? '*' : this._name;
  parent.on(name, listener);// this.parse中触发事件,action绑定事件
  if (this._alias) parent.on(this._alias, listener);
  return this;
};

// 长短名用","或"|"或" "划分,如"-p, --pepper"、"-p|--pepper"、"-p --pepper";
// .option('-c, --config [value]', '设置配置文件','./deploy.conf');
// 命令的配置参数,包括长短名,可选或必填选项,描述,过滤正则,默认值,no-参数为布尔值;
Command.prototype.option = function(flags, description, fn, defaultValue) {
  var self = this
    , option = new Option(flags, description)
    , oname = option.name()
    , name = camelcase(oname);

  if (typeof fn != 'function') {
    if (fn instanceof RegExp) {
      var regex = fn;
      fn = function(val, def) {
        var m = regex.exec(val);
        return m ? m[0] : def;
      }
    }
    else {
      defaultValue = fn;
      fn = null;
    }
  }

  if (false == option.bool || option.optional || option.required) {
    if (false == option.bool) defaultValue = true;
    if (undefined !== defaultValue) self[name] = defaultValue;
  }

  this.options.push(option);

  this.on(oname, function(val) {// parseOptions中触发,传入选项后键入的参数
    if (null !== val && fn) val = fn(val, undefined === self[name]
      ? defaultValue : self[name]);// 通过正则过滤键入值,不符合正则的取默认值

    if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
      if (null == val) {
        self[name] = option.bool ? defaultValue || true : false;// 无键入参数和默认值的时候返回true或false
      } else {
        self[name] = val;
      }
    } else if (null !== val) {
      self[name] = val;
    }
  });

  return this;
};

// 提供方法,使其满足命令行键入当前设置不识别的参数时,不报错
Command.prototype.allowUnknownOption = function(arg) {
    this._allowUnknownOption = arguments.length === 0 || arg;
    return this;
};

// program.parse(process.argv)解析命令行工具键入参数,argv[1]文件名,argv[2]其余参数
Command.prototype.parse = function(argv) {
  if (this.executables) this.addImplicitHelpCommand();

  this.rawArgs = argv;

  this._name = this._name || basename(argv[1], '.js');

  // 没有子命令(commander方法添加的命令)的时候添加help选项
  if (this.executables && argv.length < 3 && !this.defaultExecutable) {
    argv.push('--help');
  }

  // 从命令行工具的键入值中获取可执行的命令和不可执行的命令
  var parsed = this.parseOptions(this.normalize(argv.slice(2)));
  var args = this.args = parsed.args;

  // 可执行的命令触发该命令注册的事件,不可执行的命令则报错
  var result = this.parseArgs(this.args, parsed.unknown);

  // 执行配置命令或默认命令
  var name = result.args[0];
  if (this._execs[name] && typeof this._execs[name] != "function") {
    return this.executeSubCommand(argv, args, parsed.unknown);
  } else if (this.defaultExecutable) {
    args.unshift(name = this.defaultExecutable);
    return this.executeSubCommand(argv, args, parsed.unknown);
  }

  return result;
};

// 获取js或二进制文件,并执行,不可执行则报错
Command.prototype.executeSubCommand = function(argv, args, unknown) {
  args = args.concat(unknown);

  if (!args.length) this.help();
  if ('help' == args[0] && 1 == args.length) this.help();

  if ('help' == args[0]) {
    args[0] = args[1];
    args[1] = '--help';
  }

  var f = argv[1];
  var bin = basename(f, '.js') + '-' + args[0];

  var baseDir
    , link = readlink(f);

  if (link !== f && link.charAt(0) !== '/') {
    link = path.join(dirname(f), link)
  }
  baseDir = dirname(link);

  var localBin = path.join(baseDir, bin);

  // 区分执行文件为js文件还是二进制文件
  var isExplicitJS = false;
  if (exists(localBin + '.js')) {
    bin = localBin + '.js';
    isExplicitJS = true;
  } else if (exists(localBin)) {
    bin = localBin;
  }

  args = args.slice(1);

  // 区分操作系统,执行js文件或二进制文件
  var proc;
  if (process.platform !== 'win32') {
    if (isExplicitJS) {
      args.unshift(localBin);
      args = (process.execArgv || []).concat(args);

      proc = spawn('node', args, { stdio: 'inherit', customFds: [0, 1, 2] });
    } else {
      proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] });
    }
  } else {
    args.unshift(localBin);
    proc = spawn(process.execPath, args, { stdio: 'inherit'});
  }

  proc.on('close', process.exit.bind(process));
  proc.on('error', function(err) {
    if (err.code == "ENOENT") {
      console.error('\n  %s(1) does not exist, try --help\n', bin);
    } else if (err.code == "EACCES") {
      console.error('\n  %s(1) not executable. try chmod or run with root\n', bin);
    }
    process.exit(1);
  });

  this.runningCommand = proc;
};

// 将短选项拆分,如"-abc"拆分成"-a -b -c",以及"--abc=def"转变成"--abc def"
Command.prototype.normalize = function(args) {
  var ret = []
    , arg
    , lastOpt
    , index;

  for (var i = 0, len = args.length; i < len; ++i) {
    arg = args[i];
    if (i > 0) {
      lastOpt = this.optionFor(args[i-1]);
    }

    if (arg === '--') {
      ret = ret.concat(args.slice(i));
      break;
    } else if (lastOpt && lastOpt.required) {
      ret.push(arg);
    } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
      arg.slice(1).split('').forEach(function(c) {
        ret.push('-' + c);
      });
    } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
      ret.push(arg.slice(0, index), arg.slice(index + 1));
    } else {
      ret.push(arg);
    }
  }

  return ret;
};

// 根据命令行键入值获得可执行的子命令或命令args,以及不可执行的命令unknown
Command.prototype.parseArgs = function(args, unknown) {
  var name;

  if (args.length) {
    name = args[0];
    if (this.listeners(name).length) {// this.listeners由用户配置,或者由Event模块提供,为对象添加绑定的函数???
      this.emit(args.shift(), args, unknown);
    } else {
      this.emit('*', args);
    }
  } else {
    outputHelpIfNecessary(this, unknown);

    if (unknown.length > 0) {
      this.unknownOption(unknown[0]);
    }
  }

  return this;
};

// 根据name属性找到相应的option
Command.prototype.optionFor = function(arg) {
  for (var i = 0, len = this.options.length; i < len; ++i) {
    if (this.options[i].is(arg)) {
      return this.options[i];
    }
  }
};

// 根据命令行中的键入值获取相应配置,子命令或命令记录到args中,不存在的记录到unknownOptions
// 选项触发以选项名注册的事件,用以验证后续键入的参数是否合格
Command.prototype.parseOptions = function(argv) {
  var args = []
    , len = argv.length
    , literal
    , option
    , arg;

  var unknownOptions = [];

  for (var i = 0; i < len; ++i) {
    arg = argv[i];

    if ('--' == arg) {
      literal = true;
      continue;
    }

    if (literal) {
      args.push(arg);
      continue;
    }

    option = this.optionFor(arg);// 通过name查找配置项,loog-flag

    if (option) {
      if (option.required) {
        arg = argv[++i];
        if (null == arg) return this.optionMissingArgument(option);// 报错,缺少必填参数
        this.emit(option.name(), arg);// 以选项名作为事件名,触发事件
      } else if (option.optional) {
        arg = argv[i+1];
        if (null == arg || ('-' == arg[0] && '-' != arg)) {
          arg = null;
        } else {
          ++i;
        }
        this.emit(option.name(), arg);
      } else {
        this.emit(option.name());
      }
      continue;
    }

    if (arg.length > 1 && '-' == arg[0]) {// 没有子命令
      unknownOptions.push(arg);

      if (argv[i+1] && '-' != argv[i+1][0]) {
        unknownOptions.push(argv[++i]);
      }
      continue;
    }

    args.push(arg);
  }

  return { args: args, unknown: unknownOptions };
};

// 提供方法,以键值对的形式返回配置
Command.prototype.opts = function() {
  var result = {}
    , len = this.options.length;

  for (var i = 0 ; i < len; i++) {
    var key = camelcase(this.options[i].name());
    result[key] = key === 'version' ? this._version : this[key];
  }
  return result;
};

// 报错,缺少必填参数
Command.prototype.missingArgument = function(name) {
  console.error();
  console.error("  error: missing required argument `%s'", name);
  console.error();
  process.exit(1);
};

// 报错,缺少必填参数
Command.prototype.optionMissingArgument = function(option, flag) {
  console.error();
  if (flag) {
    console.error("  error: option `%s' argument missing, got `%s'", option.flags, flag);
  } else {
    console.error("  error: option `%s' argument missing", option.flags);
  }
  console.error();
  process.exit(1);
};

// 报错,提示命令不存在
Command.prototype.unknownOption = function(flag) {
  if (this._allowUnknownOption) return;
  console.error();
  console.error("  error: unknown option `%s'", flag);
  console.error();
  process.exit(1);
};

// 带...的可变参数不是最后一个键入值时报错
Command.prototype.variadicArgNotLast = function(name) {
  console.error();
  console.error("  error: variadic arguments must be last `%s'", name);
  console.error();
  process.exit(1);
};

// 注册版本号
Command.prototype.version = function(str, flags) {
  if (0 == arguments.length) return this._version;
  this._version = str;
  flags = flags || '-V, --version';
  this.option(flags, 'output the version number');
  this.on('version', function() {
    process.stdout.write(str + '\n');
    process.exit(0);
  });
  return this;
};

// 添加描述
Command.prototype.description = function(str) {
  if (0 === arguments.length) return this._description;
  this._description = str;
  return this;
};

// 设置别名
Command.prototype.alias = function(alias) {
  if (0 == arguments.length) return this._alias;
  this._alias = alias;
  return this;
};

// 提示包含何种命令及关键字
Command.prototype.usage = function(str) {
  var args = this._args.map(function(arg) {
    return humanReadableArgName(arg);
  });

  var usage = '[options]'
    + (this.commands.length ? ' [command]' : '')
    + (this._args.length ? ' ' + args.join(' ') : '');

  if (0 == arguments.length) return this._usage || usage;
  this._usage = str;

  return this;
};

Command.prototype.name = function() {
  return this._name;
};

// 输出配置命令的最大长度
Command.prototype.largestOptionLength = function() {
  return this.options.reduce(function(max, option) {
    return Math.max(max, option.flags.length);
  }, 0);
};

// 输出帮助信息
Command.prototype.optionHelp = function() {
  var width = this.largestOptionLength();

  // Prepend the help information
  return [pad('-h, --help', width) + '  ' + 'output usage information']
      .concat(this.options.map(function(option) {
        return pad(option.flags, width) + '  ' + option.description;
      }))
      .join('\n');
};

// 输出命令的帮助文档
Command.prototype.commandHelp = function() {
  if (!this.commands.length) return '';

  var commands = this.commands.filter(function(cmd) {
    return !cmd._noHelp;
  }).map(function(cmd) {
    var args = cmd._args.map(function(arg) {
      return humanReadableArgName(arg);
    }).join(' ');

    return [
      cmd._name
        + (cmd._alias ? '|' + cmd._alias : '')
        + (cmd.options.length ? ' [options]' : '')
        + ' ' + args
      , cmd.description()
    ];
  });

  var width = commands.reduce(function(max, command) {
    return Math.max(max, command[0].length);
  }, 0);

  return [
    ''
    , '  Commands:'
    , ''
    , commands.map(function(cmd) {
      var desc = cmd[1] ? '  ' + cmd[1] : '';
      return pad(cmd[0], width) + desc;
    }).join('\n').replace(/^/gm, '    ')
    , ''
  ].join('\n');
};

// 输出帮助文档
Command.prototype.helpInformation = function() {
  var desc = [];
  if (this._description) {
    desc = [
      '  ' + this._description
      , ''
    ];
  }

  var cmdName = this._name;
  if (this._alias) {
    cmdName = cmdName + '|' + this._alias;
  }
  var usage = [
    ''
    ,'  Usage: ' + cmdName + ' ' + this.usage()
    , ''
  ];

  var cmds = [];
  var commandHelp = this.commandHelp();
  if (commandHelp) cmds = [commandHelp];

  var options = [
    '  Options:'
    , ''
    , '' + this.optionHelp().replace(/^/gm, '    ')
    , ''
    , ''
  ];

  return usage
    .concat(cmds)
    .concat(desc)
    .concat(options)
    .join('\n');
};

// 打印帮助不退出
Command.prototype.outputHelp = function(cb) {
  if (!cb) {
    cb = function(passthru) {
      return passthru;
    }
  }
  process.stdout.write(cb(this.helpInformation()));
  this.emit('--help');
};

// 打印帮助并退出
Command.prototype.help = function(cb) {
  this.outputHelp(cb);
  process.exit();
};

// 驼峰式输出
function camelcase(flag) {
  return flag.split('-').reduce(function(str, word) {
    return str + word[0].toUpperCase() + word.slice(1);
  });
}

// 当str的长度小于width时,填充空格使str的长度等于width
function pad(str, width) {
  var len = Math.max(0, width - str.length);
  return str + Array(len + 1).join(' ');
}

// 遍历不可执行的命令,当为--help或-h打印帮助信息
function outputHelpIfNecessary(cmd, options) {
  options = options || [];
  for (var i = 0; i < options.length; i++) {
    if (options[i] == '--help' || options[i] == '-h') {
      cmd.outputHelp();
      process.exit(0);
    }
  }
}

// 把用户键入的选项信息还原为键入值
function humanReadableArgName(arg) {
  var nameOutput = arg.name + (arg.variadic === true ? '...' : '');

  return arg.required
    ? '<' + nameOutput + '>'
    : '[' + nameOutput + ']'
}

// 判断文件是否存在
function exists(file) {
  try {
    if (fs.statSync(file).isFile()) {
      return true;
    }
  } catch (e) {
    return false;
  }
} 

 

猜你喜欢

转载自schifred.iteye.com/blog/2311770
今日推荐