123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584 |
- var _ = require("underscore"), chalk = require('chalk');
- function ArgParser() {
- this.commands = {}; // expected commands
- this.specs = {}; // option specifications
- }
- ArgParser.prototype = {
- /* Add a command to the expected commands */
- command : function(name) {
- var command;
- if (name) {
- command = this.commands[name] = {
- name: name,
- specs: {}
- };
- }
- else {
- command = this.fallback = {
- specs: {}
- };
- }
- // facilitates command('name').options().cb().help()
- var chain = {
- options : function(specs) {
- command.specs = specs;
- return chain;
- },
- opts : function(specs) {
- // old API
- return this.options(specs);
- },
- option : function(name, spec) {
- command.specs[name] = spec;
- return chain;
- },
- callback : function(cb) {
- command.cb = cb;
- return chain;
- },
- help : function(help) {
- command.help = help;
- return chain;
- },
- usage : function(usage) {
- command._usage = usage;
- return chain;
- }
- };
- return chain;
- },
- nocommand : function() {
- return this.command();
- },
- options : function(specs) {
- this.specs = specs;
- return this;
- },
- opts : function(specs) {
- // old API
- return this.options(specs);
- },
- globalOpts : function(specs) {
- // old API
- return this.options(specs);
- },
- option : function(name, spec) {
- this.specs[name] = spec;
- return this;
- },
- usage : function(usage) {
- this._usage = usage;
- return this;
- },
- printer : function(print) {
- this.print = print;
- return this;
- },
- script : function(script) {
- this._script = script;
- return this;
- },
- scriptName : function(script) {
- // old API
- return this.script(script);
- },
- help : function(help) {
- this._help = help;
- return this;
- },
- colors: function() {
- // deprecated - colors are on by default now
- return this;
- },
- nocolors : function() {
- this._nocolors = true;
- return this;
- },
- parseArgs : function(argv) {
- // old API
- return this.parse(argv);
- },
- nom : function(argv) {
- return this.parse(argv);
- },
- parse : function(argv) {
- this.print = this.print || function(str, code) {
- console.log(str);
- process.exit(code || 0);
- };
- this._help = this._help || "";
- this._script = this._script || process.argv[0] + " "
- + require('path').basename(process.argv[1]);
- this.specs = this.specs || {};
- var argv = argv || process.argv.slice(2);
- var arg = Arg(argv[0]).isValue && argv[0],
- command = arg && this.commands[arg],
- commandExpected = !_(this.commands).isEmpty();
- if (commandExpected) {
- if (command) {
- _(this.specs).extend(command.specs);
- this._script += " " + command.name;
- if (command.help) {
- this._help = command.help;
- }
- this.command = command;
- }
- else if (arg) {
- return this.print(this._script + ": no such command '" + arg + "'", 1);
- }
- else {
- // no command but command expected e.g. 'git -v'
- var helpStringBuilder = {
- list : function() {
- return 'one of: ' + _(this.commands).keys().join(", ");
- },
- twoColumn : function() {
- // find the longest command name to ensure horizontal alignment
- var maxLength = _(this.commands).max(function (cmd) {
- return cmd.name.length;
- }).name.length;
- // create the two column text strings
- var cmdHelp = _.map(this.commands, function(cmd, name) {
- var diff = maxLength - name.length;
- var pad = new Array(diff + 4).join(" ");
- return " " + [ name, pad, cmd.help ].join(" ");
- });
- return "\n" + cmdHelp.join("\n");
- }
- };
- // if there are a small number of commands and all have help strings,
- // display them in a two column table; otherwise use the brief version.
- // The arbitrary choice of "20" comes from the number commands git
- // displays as "common commands"
- var helpType = 'list';
- if (_(this.commands).size() <= 20) {
- if (_(this.commands).every(function (cmd) { return cmd.help; })) {
- helpType = 'twoColumn';
- }
- }
- this.specs.command = {
- position: 0,
- help: helpStringBuilder[helpType].call(this)
- }
- if (this.fallback) {
- _(this.specs).extend(this.fallback.specs);
- this._help = this.fallback.help;
- } else {
- this.specs.command.required = true;
- }
- }
- }
- if (this.specs.length === undefined) {
- // specs is a hash not an array
- this.specs = _(this.specs).map(function(opt, name) {
- opt.name = name;
- return opt;
- });
- }
- this.specs = this.specs.map(function(opt) {
- return Opt(opt);
- });
- if (argv.indexOf("--help") >= 0 || argv.indexOf("-h") >= 0) {
- return this.print(this.getUsage());
- }
- var options = {};
- var args = argv.map(function(arg) {
- return Arg(arg);
- })
- .concat(Arg());
- var positionals = [];
- /* parse the args */
- var that = this;
- args.reduce(function(arg, val) {
- /* positional */
- if (arg.isValue) {
- positionals.push(arg.value);
- }
- else if (arg.chars) {
- var last = arg.chars.pop();
- /* -cfv */
- (arg.chars).forEach(function(ch) {
- that.setOption(options, ch, true);
- });
- /* -v key */
- if (!that.opt(last).flag) {
- if (val.isValue) {
- that.setOption(options, last, val.value);
- return Arg(); // skip next turn - swallow arg
- }
- else {
- that.print("'-" + (that.opt(last).name || last) + "'"
- + " expects a value\n\n" + that.getUsage(), 1);
- }
- }
- else {
- /* -v */
- that.setOption(options, last, true);
- }
- }
- else if (arg.full) {
- var value = arg.value;
- /* --key */
- if (value === undefined) {
- /* --key value */
- if (!that.opt(arg.full).flag) {
- if (val.isValue) {
- that.setOption(options, arg.full, val.value);
- return Arg();
- }
- else {
- that.print("'--" + (that.opt(arg.full).name || arg.full) + "'"
- + " expects a value\n\n" + that.getUsage(), 1);
- }
- }
- else {
- /* --flag */
- value = true;
- }
- }
- that.setOption(options, arg.full, value);
- }
- return val;
- });
- positionals.forEach(function(pos, index) {
- this.setOption(options, index, pos);
- }, this);
- options._ = positionals;
- this.specs.forEach(function(opt) {
- if (opt.default !== undefined && options[opt.name] === undefined) {
- options[opt.name] = opt.default;
- }
- }, this);
- // exit if required arg isn't present
- this.specs.forEach(function(opt) {
- if (opt.required && options[opt.name] === undefined) {
- var msg = opt.name + " argument is required";
- msg = this._nocolors ? msg : chalk.red(msg);
- this.print("\n" + msg + "\n" + this.getUsage(), 1);
- }
- }, this);
- if (command && command.cb) {
- command.cb(options);
- }
- else if (this.fallback && this.fallback.cb) {
- this.fallback.cb(options);
- }
- return options;
- },
- getUsage : function() {
- if (this.command && this.command._usage) {
- return this.command._usage;
- }
- else if (this.fallback && this.fallback._usage) {
- return this.fallback._usage;
- }
- if (this._usage) {
- return this._usage;
- }
- // todo: use a template
- var str = "\n"
- if (!this._nocolors) {
- str += chalk.bold("Usage:");
- }
- else {
- str += "Usage:";
- }
- str += " " + this._script;
- var positionals = _(this.specs).select(function(opt) {
- return opt.position != undefined;
- })
- positionals = _(positionals).sortBy(function(opt) {
- return opt.position;
- });
- var options = _(this.specs).select(function(opt) {
- return opt.position === undefined;
- });
- // assume there are no gaps in the specified pos. args
- positionals.forEach(function(pos) {
- str += " ";
- var posStr = pos.string;
- if (!posStr) {
- posStr = pos.name || "arg" + pos.position;
- if (pos.required) {
- posStr = "<" + posStr + ">";
- } else {
- posStr = "[" + posStr + "]";
- }
- if (pos.list) {
- posStr += "...";
- }
- }
- str += posStr;
- });
- if (options.length) {
- if (!this._nocolors) {
- // must be a better way to do this
- str += chalk.blue(" [options]");
- }
- else {
- str += " [options]";
- }
- }
- if (options.length || positionals.length) {
- str += "\n\n";
- }
- function spaces(length) {
- var spaces = "";
- for (var i = 0; i < length; i++) {
- spaces += " ";
- }
- return spaces;
- }
- var longest = positionals.reduce(function(max, pos) {
- return pos.name.length > max ? pos.name.length : max;
- }, 0);
- positionals.forEach(function(pos) {
- var posStr = pos.string || pos.name;
- str += posStr + spaces(longest - posStr.length) + " ";
- if (!this._nocolors) {
- str += chalk.grey(pos.help || "")
- }
- else {
- str += (pos.help || "")
- }
- str += "\n";
- }, this);
- if (positionals.length && options.length) {
- str += "\n";
- }
- if (options.length) {
- if (!this._nocolors) {
- str += chalk.blue("Options:");
- }
- else {
- str += "Options:";
- }
- str += "\n"
- var longest = options.reduce(function(max, opt) {
- return opt.string.length > max && !opt.hidden ? opt.string.length : max;
- }, 0);
- options.forEach(function(opt) {
- if (!opt.hidden) {
- str += " " + opt.string + spaces(longest - opt.string.length) + " ";
- var defaults = (opt.default != null ? " [" + opt.default + "]" : "");
- var help = opt.help ? opt.help + defaults : "";
- str += this._nocolors ? help: chalk.grey(help);
- str += "\n";
- }
- }, this);
- }
- if (this._help) {
- str += "\n" + this._help;
- }
- return str;
- }
- };
- ArgParser.prototype.opt = function(arg) {
- // get the specified opt for this parsed arg
- var match = Opt({});
- this.specs.forEach(function(opt) {
- if (opt.matches(arg)) {
- match = opt;
- }
- });
- return match;
- };
- ArgParser.prototype.setOption = function(options, arg, value) {
- var option = this.opt(arg);
- if (option.callback) {
- var message = option.callback(value);
- if (typeof message == "string") {
- this.print(message, 1);
- }
- }
- if (option.type != "string") {
- try {
- // infer type by JSON parsing the string
- value = JSON.parse(value)
- }
- catch(e) {}
- }
- if (option.transform) {
- value = option.transform(value);
- }
- var name = option.name || arg;
- if (option.choices && option.choices.indexOf(value) == -1) {
- this.print(name + " must be one of: " + option.choices.join(", "), 1);
- }
- if (option.list) {
- if (!options[name]) {
- options[name] = [value];
- }
- else {
- options[name].push(value);
- }
- }
- else {
- options[name] = value;
- }
- };
- /* an arg is an item that's actually parsed from the command line
- e.g. "-l", "log.txt", or "--logfile=log.txt" */
- var Arg = function(str) {
- var abbrRegex = /^\-(\w+?)$/,
- fullRegex = /^\-\-(no\-)?(.+?)(?:=(.+))?$/,
- valRegex = /^[^\-].*/;
- var charMatch = abbrRegex.exec(str),
- chars = charMatch && charMatch[1].split("");
- var fullMatch = fullRegex.exec(str),
- full = fullMatch && fullMatch[2];
- var isValue = str !== undefined && (str === "" || valRegex.test(str));
- var value;
- if (isValue) {
- value = str;
- }
- else if (full) {
- value = fullMatch[1] ? false : fullMatch[3];
- }
- return {
- str: str,
- chars: chars,
- full: full,
- value: value,
- isValue: isValue
- }
- }
- /* an opt is what's specified by the user in opts hash */
- var Opt = function(opt) {
- var strings = (opt.string || "").split(","),
- abbr, full, metavar;
- for (var i = 0; i < strings.length; i++) {
- var string = strings[i].trim(),
- matches;
- if (matches = string.match(/^\-([^-])(?:\s+(.*))?$/)) {
- abbr = matches[1];
- metavar = matches[2];
- }
- else if (matches = string.match(/^\-\-(.+?)(?:[=\s]+(.+))?$/)) {
- full = matches[1];
- metavar = metavar || matches[2];
- }
- }
- matches = matches || [];
- var abbr = opt.abbr || abbr, // e.g. v from -v
- full = opt.full || full, // e.g. verbose from --verbose
- metavar = opt.metavar || metavar; // e.g. PATH from '--config=PATH'
- var string;
- if (opt.string) {
- string = opt.string;
- }
- else if (opt.position === undefined) {
- string = "";
- if (abbr) {
- string += "-" + abbr;
- if (metavar)
- string += " " + metavar
- string += ", ";
- }
- string += "--" + (full || opt.name);
- if (metavar) {
- string += " " + metavar;
- }
- }
- opt = _(opt).extend({
- name: opt.name || full || abbr,
- string: string,
- abbr: abbr,
- full: full,
- metavar: metavar,
- matches: function(arg) {
- return opt.full == arg || opt.abbr == arg || opt.position == arg
- || opt.name == arg || (opt.list && arg >= opt.position);
- }
- });
- return opt;
- }
- var createParser = function() {
- return new ArgParser();
- }
- var nomnom = createParser();
- for (var i in nomnom) {
- if (typeof nomnom[i] == "function") {
- createParser[i] = _(nomnom[i]).bind(nomnom);
- }
- }
- module.exports = createParser;
|