UNPKG

move

Version:

A programming language

844 lines (754 loc) 23 kB
var sys = require('sys'); const VERSION = "1.15"; // Thrown by Parser in the event of a commandline error. var CommandLineError = new Error('Command line error'); // Thrown by Parser if the user passes in '-h' or '--help' var HelpNeeded = new Error('Help Needed'); // Thrown by Parser if the user passes in '-v' or '--version' var VersionNeeded = new Error('Version needed'); // Regex for floating point numbers const FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/; // Regex for parameters const PARAM_RE = /^-(-|\.$|[^\d\.])/; // The set of values that indicate a flag option when passed as the // +'type'+ parameter of #opt. const FLAG_TYPES = ['flag', 'bool', 'boolean']; // The set of values that indicate a single-parameter (normal) option when // passed as the +'type'+ parameter of #opt. // // A value of +io+ corresponds to a readable IO resource, including // a filename, URI, or the strings 'stdin' or '-'. const SINGLE_ARG_TYPES = ['int', 'integer', 'string', 'double', 'float', 'date']; // The set of values that indicate a multiple-parameter option (i.e., that // takes multiple space-separated values on the commandline) when passed as // the +'type'+ parameter of #opt. const MULTI_ARG_TYPES = ['ints', 'integers', 'strings', 'doubles', 'floats', 'dates']; // The complete set of legal values for the +'type'+ parameter of #opt. const TYPES = [].concat(FLAG_TYPES, SINGLE_ARG_TYPES, MULTI_ARG_TYPES); const INVALID_SHORT_ARG_REGEX = /[\d-]/ function _flatten(arr) { var fun = function(memo, value) { if (Array.isArray(value)) value.reduce(fun, memo); else memo.push(value); return memo; }; return arr.reduce(fun, []); } // The commandline parser. // // If it's necessary to instantiate this class (for more complicated // argument-parsing situations), be sure to call #parse to actually // produce the output hash. var Parser = exports.Parser = function() { this._version = null; this.leftovers = []; this.specs = {}; this.long = {}; this.short = {}; this.order = []; this.constraints = []; this.stop_words = []; this._stop_on_unknown = false; if( arguments.length > 0 && arguments[0].length > 0 ) { var args = arguments[0]; var didSomething = false; var func = args.pop(); if (typeof func === 'function') { func.apply(this,args); didSomething = true; } var func = args.pop(); if (Array.isArray(func)) { var self = this; func.forEach(function(spec){ if (Array.isArray(spec)) { Parser.prototype.opt.apply(self, spec); } else { spec = String(spec); leftoffs = 0; if (m = (/^\s+/).exec(spec)) { leftoffs = m[0].length; } spec = self.wrap(spec, { prefix: leftoffs, width: self.width() - leftoffs - 1 }); self.banner(spec); } }); didSomething = true; } if (!didSomething) { throw new Error('invalid argument type ('+(typeof func)+') for first arg'); } } }; Parser.prototype.opt = function(name, _desc, _opts) { var desc = _desc || ""; var opts = _opts || {}; if( name in this.specs ) throw new Error("You already have an argument named '" + name + "'"); // fill in :type if( 'type' in opts ) { if( opts.type.constructor == String ) { switch(opts.type) { case 'boolean': case 'bool': opts.type = 'flag' break; case 'integer': opts.type = 'int' break; case 'integers': opts.type = 'ints'; break; case 'double': opts.type = 'float'; break; case 'doubles': opts.type = 'floats'; break; default: if(TYPES.indexOf(opts.type) === -1) { throw new Error("unsupported argument type '"+opts.type+"'"); } } } else if(opts.type == String) { opts.type = "string"; } else if(opts.type == Number) { opts.type = "float"; } else if(opts.type == Boolean) { opts.type = "flag"; } else if(opts.type == Date) { opts.type = "date"; } else { throw new Error("unsupported argument type '"+opts.type+"'"); } } else { opts.type = null; } // for options with :multi => true, an array default doesn't imply // a multi-valued argument. for that you have to specify a :type // as well. (this is how we disambiguate an ambiguous situation; // see the docs for Parser#opt for details.) if( opts.multi && opts.def && opts.def.constructor == Array && !opts.type) { var disambiguated_default = opts.def[0]; } else { var disambiguated_default = opts.def; } if( typeof disambiguated_default == 'undefined' || disambiguated_default === null ) { var type_from_default = null; } else if( disambiguated_default.constructor == Number ) { if( (disambiguated_default+'').match(/^[0-9]+$/) ) { var type_from_default = 'int'; } else { var type_from_default = 'float'; } } else if( disambiguated_default.constructor == Boolean ) { var type_from_default = 'flag'; } else if( disambiguated_default.constructor == String ) { var type_from_default = 'string'; } else if( disambiguated_default.constructor == Date ) { var type_from_default = 'date'; } else if( disambiguated_default.constructor == Array ) { if( opts.def.length < 1 ) { throw new Error("multiple argument type cannot be deduced from an empty Array"); } if( opts.def[0] && opts.def[0].constructor == Number ) { if( (opts.def[0]+'').match(/^[0-9]+$/) ) { var type_from_default = 'ints'; } else { var type_from_default = 'floats'; } } else if( opts.def[0].constructor == String ) { var type_from_default = 'strings'; } else if( opts.def[0].constructor == Date ) { var type_from_default = 'dates'; } else { throw new Error("unsupported multiple argument type"); } } else { throw new Error("unsupported argument type"); } if(opts.type && type_from_default && opts.type != type_from_default) { throw new Error("type specification and default type don't match (default type is "+type_from_default+")"); } opts.type = opts.type || type_from_default || 'flag'; if( !opts.type ) { opts.type = 'flag'; } // fill in :long opts.long = !(typeof opts.long == 'undefined' || opts.long === null) ? (opts.long+'') : (name+'').replace(/_/g, '-'); if( m = opts.long.match(/^--([^-].*)$/) ) { opts.long = m[1]; } else if( opts.long.match(/^[^-]/) ) { opts.long = opts.long; } else { throw new Error("invalid long option name " + opts.long); } if(opts.long in this.long) { throw new Error("long option name "+opts.long+" is already taken; please specify a (different) long"); } // fill in :short if( typeof opts.short == 'undefined' || opts.short === null || opts.short == 'none' || opts.short.match(/^.$/) ) { opts.short = opts.short; } else if( m = opts.short.match(/^-(.)$/) ) { opts.short = m[1]; } else { throw new Error("invalid short option name '" + opts.short + "'"); } if(opts.short) { if(this.short[opts.short]) { throw new Error("short option name " + opts.short +" is already taken; please specify a (different) short"); } if(opts.short.match(INVALID_SHORT_ARG_REGEX)) { throw new Error("a short option name can't be a number or a dash"); } } // fill in :default for flags if( opts.type == 'flag' && !opts.def ) { opts.def = false; } // autobox :default for :multi (multi-occurrence) arguments if(opts.def && opts.multi && Array.isArray(opts.def)) { opts.def = [opts.def]; } // fill in :multi opts.multi = opts.multi || false; opts.desc = opts.desc || desc; this.long[opts.long] = name if(opts.short && opts.short != 'none') { this.short[opts.short] = name } this.specs[name] = opts; this.order.push(['opt', name]); }; // Sets the version string. If set, the user can request the version // on the commandline. Should probably be of the form "<program name> // <version number>". Parser.prototype.version = function(s) { if( s ) { this._version = s; } else { this._version = null; } return this._version; }; // Adds text to the help display. Can be interspersed with calls to // #opt to build a multi-section help page. Parser.prototype.banner = function(s) { this.order.push(['text', s]); }; Parser.prototype.text = Parser.prototype.banner; // Marks two (or more!) options as requiring each other. Only handles // undirected (i.e., mutual) dependencies. Parser.prototype.depends = function() { var syms = Array.prototype.slice.call(arguments); syms.forEach(function(sym) { if( !this.specs[sym] ) { throw new Error("unknown option '"+sym+"'"); } },this); this.constraints.push(['depends', syms]); }; // Marks two (or more!) options as conflicting. Parser.prototype.conflicts = function() { var syms = Array.prototype.slice.call(arguments); syms.forEach(function(sym) { if( !this.specs[sym] ) { throw new Error("unknown option '"+sym+"'"); } },this); this.constraints.push(['conflicts', syms]); }; // Defines a set of words which cause parsing to terminate when // encountered, such that any options to the left of the word are // parsed as usual, and options to the right of the word are left // intact. Parser.prototype.stop_on = function() { this.stop_words = _flatten(Array.prototype.slice.call(arguments)); }; // Similar to #stop_on, but stops on any unknown word when encountered // (unless it is a parameter for an argument). This is useful for // cases where you don't know the set of subcommands ahead of time, // i.e., without first parsing the global options. Parser.prototype.stop_on_unknown = function() { this._stop_on_unknown = true; }; // Parses the commandline Parser.prototype.parse = function(_cmdline) { if( typeof _cmdline == 'undefined' ) { var cmdline = process.argv; } else { var cmdline = _cmdline; } var vals = {} var required = {} if(this._version && !(this.specs['version'] || this.long['version']) ) { this.opt('version', "Print version and exit"); } if(!this.specs['help'] && !this.long['help']) { this.opt('help', "Show this message"); } for(var sym in this.specs) { var opts = this.specs[sym]; if(opts.required) { required[sym] = true; } vals[sym] = opts.def; if(opts.multi && !opts.def) { // multi arguments default to [], not nil vals[sym] = []; } } this._resolve_default_short_options(); // resolve symbols var given_args = {}; this.leftovers = this._each_arg(cmdline, function(arg, params) { if( m = arg.match(/^-([^-])$/) ) { var sym = this.short[m[1]]; } else if( m = arg.match(/^--([^-]\S*)$/) ) { var sym = this.long[m[1]]; } else { throw new Error("invalid argument syntax: '" + arg + "'"); } if( typeof sym == 'undefined' ) { throw new Error("unknown argument '" + arg + "'"); } if(sym in given_args && !this.specs[sym].multi) { throw new Error("option '" + arg + "' specified multiple times"); } given_args[sym] = given_args[sym] || {}; given_args[sym].arg = arg given_args[sym].params = given_args[sym].params || []; // The block returns the number of parameters taken. var num_params_taken = 0 if(params) { if(SINGLE_ARG_TYPES.indexOf(this.specs[sym].type) !== -1) { given_args[sym].params.push([params.shift()]); // take the first parameter num_params_taken = 1; } else if(MULTI_ARG_TYPES.indexOf(this.specs[sym].type) !== -1) { given_args[sym].params.push(params) // take all the parameters num_params_taken = params.length; } } return num_params_taken; }); // check for version and help args if('version' in given_args) { throw VersionNeeded; } if('help' in given_args) { throw HelpNeeded; } // check constraint satisfaction this.constraints.forEach(function(tuple) { var type = tuple[0]; var syms = tuple[1]; //_detect(syms, function(sym) { return given_args[sym] } ); for (var k in syms) { constraint_sym = given_args[syms[k]]; if (constraint_sym) break; } if(!constraint_sym) { return; } switch(type) { case 'depends': syms.forEach( function(sym) { if( !(sym in given_args) ) { throw new Error("--" + this.specs[constraint_sym].long +" requires --"+ this.specs[sym].long); } },this); break; case 'conflicts': syms.forEach( function(sym) { if( sym in given_args && (sym != constraint_sym) ) { throw new Error("--" + this.specs[constraint_sym].long +" conflicts with --"+ this.specs[sym].long); } },this); break; } },this); for( var sym in required) { var val = required[sym]; if( !(sym in given_args) ) { throw new Error("option '" + sym + "' must be specified"); } } // parse parameters for( var sym in given_args ) { var given_data = given_args[sym]; var arg = given_data.arg; var params = given_data.params; var mapparams = function(fun){ return params.map(function(pg) { return pg.map(fun); }); } opts = this.specs[sym] if(params.length < 1 && opts.type != 'flag') { throw new Error("option '"+arg+"' needs a parameter"); } vals[sym+'_given'] = true; // mark argument as specified on the commandline var selfScoper = this; switch( opts.type) { case 'flag': vals[sym] = !opts.def; break; case 'int': case 'ints': vals[sym] = mapparams(function(p){ return selfScoper._parse_integer_parameter(p, arg); }); break; case 'float': case 'floats': vals[sym] = mapparams(function(p) { return selfScoper._parse_float_parameter(p, arg); }); break; case 'string': case 'strings': vals[sym] = mapparams(function(p) { return p+''; }); break; case 'date': case 'dates': vals[sym] = mapparams(function(p) { return selfScoper._parse_date_parameter(p, arg); }); break; } if(SINGLE_ARG_TYPES.indexOf(opts.type) !== -1) { if(!opts.multi) { // single parameter vals[sym] = vals[sym][0][0]; } else { // multiple options, each with a single parameter vals[sym] = vals[sym].map(function(p){ return p[0]; }); } } else if(MULTI_ARG_TYPES.indexOf(opts.type) !== -1 && !opts.multi ) { vals[sym] = vals[sym][0] // single option, with multiple parameters } // else: multiple options, with multiple parameters } /* // allow openstruct-style accessors class << vals def method_missing(m, *args) self[m] || self[m.to_s] end end */ return vals; }; Parser.prototype.educate = function(message) { if (message) { sys.error(message); } this.width(); // just calculate it now; otherwise we have to be careful not to // call this unless the cursor's at the beginning of a line. var left = {} for( var name in this.specs ) { var spec = this.specs[name]; left[name] = "--"+spec.long+((spec.short && spec.short != 'none') ? ", -"+spec.short : ""); switch(spec.type) { case 'flag': left[name] += ""; break; case 'int': left[name] += " <i>"; break; case 'ints': left[name] += " <i+>"; break; case 'string': left[name] += " <s>"; break; case 'strings': left[name] += " <s+>"; break; case 'float': left[name] += " <f>"; break; case 'floats': left[name] += " <f+>"; break; case 'io': left[name] += " <filename/uri>"; break; case 'ios': left[name] += " <filename/uri+>"; break; case 'date': left[name] += " <date>"; break; case 'dates': left[name] += " <date+>"; break; } } var leftcol_width = Object.keys(left).map(function(k){return left[k].length}).reduce(function(a,b){return Math.max(a,b)}, 0); var rightcol_start = leftcol_width + 9; // spaces var indent = " "; if( !(this.order.length > 0 && this.order[0][0] == 'text') ) { if(this._version) { sys.error(this._version); } sys.error("Options:"); } this.order.forEach(function(ordering) { var what = ordering[0]; var opt = ordering[1]; if(what === 'text') { sys.error(this.wrap(opt)); return; } var spec = this.specs[opt]; process.stderr.write(indent + left[opt]); //TODO: justify this text var desc = spec.desc; if (Array.isArray(spec.def)) var default_s = spec.def.join(', '); else var default_s = spec.def +''; if(spec.def) { if(spec.desc.match(/\.$/)) { desc += " (Default: "+default_s+")"; } else { desc += " (default: "+default_s+")"; } } sys.error(this.wrap(desc, { width: this.width() - rightcol_start - 1, prefix: rightcol_start, leftoffs: rightcol_start-(left[opt].length+indent.length), })); },this); } Parser.prototype.width = function() { if (process.stdout.isTTY) { return process.stdout.columns || require('tty').getWindowSize(process.stdout.fd)[1]; } else { return 80; } } Parser.prototype.wrap = function(str, _opts) { var opts = _opts || {}; if (str && str.length) { var self = this; var lines = str.split("\n"); lines = lines.map(function(line){ return self.padStr(" ",opts.leftoffs) + self._wrap_line(line, opts).join("\n"); }); lines = lines.join("\n"); return lines; } return ""; } Parser.prototype._wrap_line = function(str, _opts) { var opts = _opts || {}; var prefix = opts.prefix || 0; var width = opts.width || (this.width() - 1); var start = 0; var ret = []; while (str.length) { var hunk; if (str.length > width) { hunk = str.substr(start, width); str = str.substr(width).replace(/^\s+/, ''); } else { hunk = str; } ret.push((ret.length ? this.padStr(" ",prefix) : '') + hunk); if (hunk === str) break; } //sys.p(ret); return ret; }; Parser.prototype.padStr = function(str, num) { var ret = ''; while(ret.length < num) ret += ' '; return ret; } Parser.prototype._parse_integer_parameter = function(param, arg) { if( !param.match(/^\d+$/) ) { throw new Error("option '"+arg +"' needs an integer"); } return parseInt(param); }; Parser.prototype._parse_float_parameter = function(param, arg) { if(!param.match(FLOAT_RE)) { throw new Error("option '"+arg+"' needs a floating-point number"); } return parseFloat(param); }; Parser.prototype._parse_date_parameter = function(param, arg) { var parsed = Date.parse(param); if(isNaN(parsed)) { throw new Error("option '"+arg+"' needs a date"); } else { return parsed } }; Parser.prototype._resolve_default_short_options = function() { this.order.forEach(function(ordering) { var type = ordering[0]; var name = ordering[1]; if( type != 'opt' ) { return; } var opts = this.specs[name]; if( opts.short ) { return; } var c = false; var copts = opts.long.split(''); for( var i=0; i < copts.length; i++ ) { var d = copts[i]; if( !d.match(INVALID_SHORT_ARG_REGEX) && !this.short[d] ) { c = d; break; } } if(c) { opts.short = c; this.short[c] = name; } }, this); }; Parser.prototype._collect_argument_parameters = function(args, start_at) { var params = [] var pos = start_at while(args[pos] && !args[pos].match(PARAM_RE) && this.stop_words.indexOf(args[pos]) === -1) { params.push(args[pos]); pos++; } return params; }; Parser.prototype._each_arg = function(args, callback) { var remains = []; var i = 0; while (i < args.length) { if(this.stop_words.indexOf(args[i]) !== -1) { return remains.concat(args.slice(i)); } if( args[i].match(/^--$/) ) { // arg terminator return remains.concat(args.slice((i + 1))); } else if( m = args[i].match(/^--(\S+?)=(.*)$/) ) { // long argument with equals callback.call(this,"--"+m[1], [m[2]]); i++; } else if( args[i].match( /^--(\S+)$/) ) { // long argument params = this._collect_argument_parameters(args, i + 1) if( params.length > 0 ) { var num_params_taken = callback.call(this,args[i], params); if(!num_params_taken) { if(this._stop_on_unknown) { return remains.concat(args.slice(i + 1)); } else { remains.concat(params); } } i += 1 + num_params_taken; } else { // long argument no parameter callback.call(this,args[i], null); i++; } } else if( m = args[i].match(/^-(\S+)$/) ) { // one or more short arguments var shortargs = m[1].split(''); for( var j = 0; j < shortargs.length; j++ ) { var a = shortargs[j]; if(j == (shortargs.length - 1)) { params = this._collect_argument_parameters(args, i + 1) if(params.length > 0) { var num_params_taken = callback.call(this,"-"+a, params); if(!num_params_taken) { if(this._stop_on_unknown) { return remains.concat(args.slice(i + 1)); } else { remains.concat(params); } } i += 1 + num_params_taken; } else { // argument no parameter callback.call(this,"-"+a, null); i += 1 } } else { callback.call(this,"-"+a, null); } } } else { if(this._stop_on_unknown) { return remains.concat(args.slice(i)); } else { remains.push(args[i]); i++; } } } return remains; }; exports.options = function() { var args = Array.prototype.slice.call(arguments); var argv = (args.length > 1) ? args.shift() : process.argv; this.p = new Parser(args); try { vals = this.p.parse(argv); argv.splice(0,argv.length); this.p.leftovers.forEach(function(l) { argv.push(l); }); return vals; } catch(err) { if( err == HelpNeeded ) { this.p.educate(); process.exit(1); } else if( err == VersionNeeded ) { sys.error(this.p._version); process.exit(0); } else { //throw err; sys.error(err); process.exit(1); } return null; } }