UNPKG

argp

Version:
1,215 lines (1,034 loc) 29.1 kB
"use strict"; var events = require ("events"); var util = require ("util"); var path = require ("path"); var fs = require ("fs"); var wrap = require ("./wrap"); var debug = process.argv[1] === "debug"; var script = path.basename (process.argv[debug ? 2 : 1], ".js"); var mainScript = script; var cast = function (value){ if (value === "undefined") return undefined; if (value === "null") return null; if (value === "true") return true; if (value === "false") return false; var v = Number (value); return isNaN (v) ? value : v; }; var Argp = module.exports = function (options){ events.EventEmitter.call (this); options = options || {}; this._once = options.once; this._exitStatus = 0; this._argv = null; this._argp = null; this._usages = []; this._description = null; this._version = null; this._email = null; this._rePackageEmail = /<(.+)>/; this._packageVersion = false; this._allowUndefinedOptions = false; this._allowUndefinedArguments = false; this._sort = false; this._columns = 80; this._showHelp = false; this._showUsage = false; this._emitArguments = false; this._command = false; this._commandInstances = {}; //Shortcuts to the arguments and options of each command this._commands = {}; this._lines = []; this._arguments = {}; this._argumentsArray = []; this._options = {}; this._optionsShort = {}; this._optionsLong = {}; var me = this; this._ignore = false; this._ignoreFn = function (){ me._ignore = true; }; this._inputError = false; }; util.inherits (Argp, events.EventEmitter); Argp.prototype._error = function (str){ //If an input array is used, don't exit if (this.listeners ("error").length){ this._inputError = true; return this.emit ("error", new Error (str)); } console.error (script + ": " + str); var error = this._errorTry (); if (error) console.error (error); process.exit (this._exitStatus || 1); }; Argp.prototype._errorTry = function (){ var s = this._command ? script : mainScript; var str; if (this._showHelp && this._showUsage){ str = "'" + s + " --help' or '" + s + " --usage'"; }else if (this._showHelp && !this._showUsage){ str = "'" + s + " --help'"; }else if (!this._showHelp && this._showUsage){ str = "'" + s + " --usage'"; }else if (this._command){ //If the command doesn't have a help or usage message, show the main help //and usage return this._argp._errorTry (); }else{ return; } return "Try " + str + " for more information"; }; Argp.prototype._errorChoice = function (value){ this._error ("Unrecognized choice '" + value + "'"); }; Argp.prototype._errorConvert = function (name, type){ this._error ("Option '" + name + "' is not a " + type); }; Argp.prototype._errorAbbreviation = function (name){ this._error ("Option '" + name + "' is ambiguous"); }; Argp.prototype._errorNotExpectedValue = function (name){ this._errorExpectedValue (name, "not"); }; Argp.prototype._errorExpectedValue = function (name, negate){ this._error ("Option '" + name + "' " + (negate ? "does not require" : "requires") + " an argument"); }; Argp.prototype._errorUnrecognizedOption = function (name){ this._error ("Unrecognized option '" + name + "'"); }; Argp.prototype._errorUnrecognizedArgument = function (name){ this._error ("Unrecognized argument '" + name + "'"); }; Argp.prototype._errorExpectedCommand = function (name, num, min){ this._error ("Command '" + name + "' expects " + (min ? min + " " : "") + num + " argument/s"); }; Argp.prototype._errorEmptyNegatedFlag = function (){ this._error ("A negated flag must contain a long name, eg. '--no-' is " + "invalid, '--no-flag' is valid"); }; Argp.prototype._columnize = function (col1, col2){ var blank = " "; var spaces = 30 - col1.length; if (spaces < 0 && col2){ col1 += "\n" + blank; }else{ for (var i=0; i<spaces; i++){ col1 += " "; } } if (col2){ var arr = wrap (col2, this._columns - 30, " ").split ("\n"); for (var i=0; i<arr.length; i++){ if (i) col1 += "\n" + blank; col1 += arr[i]; } } return col1; }; Argp.prototype._newArgument = function (str){ if (!this._emitArguments && this._sort){ this._argumentsArray.push (str); return; } this._ignore = false; this.emit ("argument", this._argv, str, this._ignoreFn); if (this._ignore) return; this._argv[str] = true; }; Argp.prototype._convertType = function (o){ if (o.opt.type === Number){ var v = Number (o.value); if (isNaN (v)){ return this._errorConvert (o.long || o.short, "Number"); } //Note: Number(null) returns 0, the default value o.value = v; }else if (o.opt.type === Boolean){ if (o.value === "true"){ o.value = true; }else if (o.value === "false"){ o.value = false; }else{ return this._errorConvert (o.long || o.short, "Boolean"); } }else if (o.opt.type === Array){ o.value = o.value.split (","); for (var i=0; i<o.value.length; i++){ //Convert the type of each element o.value[i] = cast (o.value[i]); } } }; Argp.prototype._newOption = function (o){ this._ignore = false; //Normalize the value if (o.flag){ o.value = o.short && o.opt && o.opt.negate ? false : !o.negate; }else{ if (o.opt){ if (o.value === undefined){ //Defined and no input value, set default o.value = o.opt.default; }else{ //Convert to the configured type this._convertType (o); if (this._inputError) return; } //Validate with the choices if (o.opt.choices){ var found = false; for (var i=0; i<o.opt.choices.length; i++){ if (o.opt.choices[i] === o.value){ found = true; break; } } if (!found){ return this._errorChoice (o.value); } } //Use the reviver, if any if (o.opt.reviver){ o.value = o.opt.reviver.call (this, o.value); } }else if (o.value === undefined){ //Undefined and no input value, set null o.value = null; }else{ //Undefined options are converted automatically to the type they represent o.value = cast (o.value); } } //At this point o.value contains the input value or the default whether it's //a flag or not this.emit ("option", this._argv, o.long || o.short, o.value, !!o.long, this._ignoreFn); if (this._ignore) return; if (o.opt){ //Check for --help, --usage or --version if (this._showHelp && o.opt.id === "help"){ return this.printHelp (); } if (this._showUsage && o.opt.id === "usage"){ return this.printUsage (); } if (this._version && o.opt.id === "version"){ return this.printVersion (); } if (o.opt.type === Array){ //The user may cache the default array, so it cannot be replaced by the //new one, so all the elements must be copied var arr = this._argv[o.opt.id]; o.value.forEach (function (v){ arr.push (v); }); }else{ this._argv[o.opt.id] = o.value; } }else{ this._argv[o.long || o.short] = o.value; } }; Argp.prototype._fullname = function (name, negate){ var o = this._optionsLong[name]; //Don't need to check for o.negate === negate //Exact match if (o) return name; //Check abbreviations var matches = 0; var re = new RegExp ("^" + name); var lastMatch = null; for (var p in this._optionsLong){ if (re.test (p)){ matches++; if (matches === 2){ return this._errorAbbreviation ("--" + (negate ? "no-" : "") + name); } lastMatch = p; } } //If null, the option is not defined return lastMatch; }; Argp.prototype._read = function (argv){ var me = this; var onlyArguments = false; //Properties: short/long, flag, negate, value, opt var option; var free = function (){ //This function must be called on finish, when "--" option is found and when //any short and long options are found if (!option) return; //Check any previous option waiting for a value and, if any, free it; the //previous option doesn't have a value and therefore: //- it's a flag (if undefined) //- doesn't have a value (no error if optional, error if mandatory) //At this point the option has been validated if (option.opt){ //Defined option if (option.opt.optional){ //No value me._newOption (option); }else{ //Requires a value return me._errorExpectedValue (option.long ? "--" + option.long : "-" + option.short); } }else{ //Undefined options with no value are always a flag option.flag = true; me._newOption (option); } option = null; }; var value = function (str){ if (me._arguments[str]){ //Defined argument between an option and a possible value, eg: --a arg 1, //where "arg" is an argument and "a" has "1" as a value //Simply save the argument and proceed me._newArgument (str); return; } //At this point the string contains the value of the previous option option.value = str; me._newOption (option); option = null; }; var long = function (str){ //Free the previous option free (); if (me._inputError) return; var o; var name; if (str[2] === "n" && str[3] === "o" && str[4] === "-"){ //Negated flag //Get the full name str = str.substring (5); if (str){ //Make sure there's a name, eg: --no-a //--no- doesn't have a name, so "--no-" is the name name = str; str = me._fullname (name, true); if (me._inputError) return; }else{ return me._errorEmptyNegatedFlag (); } o = str ? me._optionsLong[str] : null; if ((o && !o.flag) || (!me._allowUndefinedOptions && !o)){ //Defined no-flag option or undefined option and no undefined allowed return me._errorUnrecognizedOption ("--no-" + (str || name)); } //If --a is defined and --no-a is read, we must omit the option because //the value of --no-a, false, it's the same as the default value of --a, //false if (o && !o.negate) return; //The option doesn't require a value me._newOption ({ opt: o, flag: true, negate: true, long: str || name }); return; } str = str.substring (2); //Check for the value in the same token var emptyStringValue = false; var value; var i = str.indexOf ("="); if (i !== -1){ value = str.substring (i + 1); if (!value){ //Empty string value, --a= emptyStringValue = true; value = undefined; } str = str.substring (0, i); } //Get the full name name = str; str = me._fullname (str); if (me._inputError) return; o = str ? me._optionsLong[str] : null; if (o){ if (o.flag){ if (value){ //A flag with a value return me._errorNotExpectedValue (str); } //If o.negate, this is the default value of the flag, eg: --no-a is //defined and we read --a. The default value of --no-a is true, the same //as --a, so we can omit it if (!o.negate){ me._newOption ({ opt: o, flag: true, long: str }); } return; } }else if (!me._allowUndefinedOptions){ //Undefined option and no undefined allowed return me._errorUnrecognizedOption ("--" + (str || name)); } if (value || emptyStringValue){ me._newOption ({ opt: o, long: str || name, value: value }); }else{ //Wait for the value option = { opt: o, long: str || name }; } }; var short = function (str){ //Note: Cannot configure a negated short option //Free the previous option free (); if (me._inputError) return; str = str.substring (1); var name; var o; //Grouped options for (var i=0; i<str.length; i++){ //Free the previous option free (); if (me._inputError) return; name = str[i]; o = me._optionsShort[name]; if (o){ if (o.flag){ me._newOption ({ opt: o, flag: true, short: name }); if (me._inputError) return; }else{ if (!i){ //First option //Check whether the value is in the same token if (str.length > 1){ //Assumption: if an undefined option follows the first defined //no-flag option, the next undefined characters to the first //defined option are the value of this option if (!me._optionsShort[str[i + 1]]){ me._newOption ({ opt: o, short: name, value: str.substring (1) }); return; }else if (o.optional){ me._newOption ({ opt: o, short: name, value: null }); }else{ //The option requires a value return me._errorExpectedValue ("-" + name); } }else{ //Wait for the value option = { opt: o, short: name }; } }else if (i < str.length - 1){ //Options between the first and the last if (o.optional){ //No value me._newOption ({ opt: o, short: name }); }else{ //The option requires a value return me._errorExpectedValue ("-" + name); } }else{ //Last option //Wait for the value option = { opt: o, short: name }; } } }else{ if (!me._allowUndefinedOptions){ //Undefined option and no undefined allowed return me._errorUnrecognizedOption ("-" + name); } //Wait for the value option = { opt: null, short: name }; } } }; var argument = function (str){ //Check for the trailing arguments if this is a command var curr = me._currentArgument; //The arguments of a command have a higher priority than the defined //arguments, eg: foo 1 bar, where "foo" is a command that expects 2 //arguments and "bar" is a defined argument. In this case, "bar" belongs to //the "foo"'s arguments: { foo: [1, "bar"], bar: false } if (me._command && curr){ me._argv[curr.name].push (cast (str)); var length = me._argv[curr.name].length; if (length === curr.trailing.eq || length === curr.trailing.max){ //Limit reached me._currentArgument = null; } return; } var arg = me._arguments[str]; if (!me._allowUndefinedArguments && !arg){ //Undefined argument and no undefined allowed return me._errorUnrecognizedArgument (str); } if (me._command && arg && arg.trailing){ //Store the new argument me._currentArgument = { name: str, trailing: arg.trailing }; return; } me._newArgument (str); }; var validateCommand = function (){ if (!me._command) return; var curr = me._currentArgument; if (curr){ if (curr.trailing.eq !== undefined){ return me._errorExpectedCommand (curr.name, curr.trailing.eq); } if (me._argv[curr.name].length < curr.trailing.min){ return me._errorExpectedCommand (curr.name, curr.trailing.min, "minimum"); } } }; var str; for (var i=0; i<argv.length; i++){ str = argv[i]; if (onlyArguments){ this._newArgument (str); continue; } if (str === "--"){ free (); if (this._inputError) return; onlyArguments = true; continue; } if (str === "-"){ //Special case if (option){ //Allow the option value "-" option.value = str; this._newOption (option); if (this._inputError) return; option = null; }else{ //Assumption: an option with name "-" is an argument argument (str); } continue; } if (str[0] === "-"){ if (str[1] === "-"){ //Long option long (str); if (this._inputError) return; }else{ if (option && option.opt && (option.opt.type === Number || option.opt.type === Array)){ //Special case //Option with a negative Number value, eg: --a -12.34, or Array //beginning with a nevative number, eg: --a -1.2,foo option.value = str; this._newOption (option); if (this._inputError) return; option = null; continue; } //Short option short (str); if (this._inputError) return; } }else if (option){ //Option waiting for a value value (str); if (this._inputError) return; }else{ argument (str); } }; free (); if (this._inputError) return; validateCommand (); }; Argp.prototype.printHelp = function (code){ var h = ""; var prefix = " "; if (this._usages.length){ var s = ""; for (var i=0; i<this._usages.length; i++){ s += (i ? "\n " : "") + wrap ((i ? "" : "Usage: ") + h + this._usages[i], i ? this._columns - 7 : this._columns, prefix); } h = s; }else{ if (Object.keys (this._options).length){ h += " [options]"; } if (Object.keys (this._arguments).length){ h += " [arguments]"; } h = wrap ("Usage: " + script + h, this._columns, prefix); } if (this._description) h += "\n\n" + wrap (this._description, this._columns); if (this._lines.length) h += "\n"; var me = this; this._lines.forEach (function (o){ if (o.text !== undefined){ h += "\n" + wrap (o.text, me._columns, o.prefix, true); return; } if (o.columns){ h += "\n" + me._columnize (o.columns[0] + " ", o.columns[1]); return; } var line = " "; if (o.arg){ var name = o.arg; o = me._arguments[o.arg]; if (o.hidden) return; line += o.trailing && o.synopsis ? o.synopsis : name; }else{ if (o.hidden) return; if (o.short){ if (o.long){ line += "-" + o.short + ", --" + (o.flag && o.negate ? "no-" + o.long : o.long); o.aliases.forEach (function (alias){ line += ", --" + alias; }); if (!o.flag){ line += (o.optional ? "[=" + o.metavar + "]" : "=" + o.metavar); } }else{ line += "-" + o.short; var alias = ""; if (o.aliases.length){ alias = "="; o.aliases.forEach (function (alias){ line += ", --" + alias; }); } if (!o.flag){ line += (o.optional ? "[" + alias + o.metavar + "]" : (alias || " ") + o.metavar); } } }else if (o.long){ line += " --" + (o.flag && o.negate ? "no-" + o.long : o.long); o.aliases.forEach (function (alias){ line += ", --" + alias; }); if (!o.flag){ line += (o.optional ? "[=" + o.metavar + "]" : "=" + o.metavar); } } } h += "\n" + me._columnize (line + " ", o.description); }); if (this._footer) h += "\n\n" + wrap (this._footer, this._columns); if (this._email) h += "\n\n" + wrap ("Report bugs to <" + this._email + ">.", this._columns); console.log (h); //Only exit if the are no error listeners if (this.listeners ("error").length){ //Simulate an error to stop parsing the remaining options this._inputError = true; }else{ process.exit (arguments.length === 1 ? code : this._exitStatus); } }; Argp.prototype.printUsage = function (code){ var usage = ""; var prefix = " "; //Arguments if (this._usages.length){ var s = ""; for (var i=0; i<this._usages.length; i++){ s += (i ? "\n " : "") + wrap ((i ? "" : "Usage: ") + usage + this._usages[i], i ? this._columns - 7 : this._columns, prefix); } usage = s; }else{ var short = ""; var shortArgs = []; var o; //Short names go first for (var p in this._optionsShort){ o = this._optionsShort[p]; if (o.hidden || o.long || o.aliases.length) continue; if (o.flag){ short += p; }else{ //Short option with value, they go after the short flags shortArgs.push (o); } } if (short){ usage += " [-" + short + "]"; } shortArgs.forEach (function (o){ usage += " [-" + o.short + (o.optional ? "[" + o.metavar + "]" : " " + o.metavar) + "]"; }); //Long names var longArgs = {}; for (var p in this._optionsLong){ o = this._optionsLong[p]; if (o.hidden || longArgs[p]) continue; usage += " ["; if (o.short){ usage += "-" + o.short + (o.long ? "|" : ""); } if (o.long){ //Short name with aliases usage += "--" + o.long; } var me = this; o.aliases.forEach (function (alias){ //Remember the alias longArgs[alias] = true; usage += "|--" + alias; }); usage += (o.flag ? "]" : (o.optional ? "[=" + o.metavar + "]" : "=" + o.metavar) + "]"); } //Arguments if (Object.keys (this._arguments).length){ for (var p in this._arguments){ o = this._arguments[p]; if (o.hidden) continue; usage += " [" + (o.trailing && o.synopsis ? o.synopsis : p) + "]"; } } usage = wrap ("Usage: " + script + usage, this._columns, prefix); } console.log (usage); //Only exit if the are no error listeners if (this.listeners ("error").length){ //Simulate an error to stop parsing the remaining options this._inputError = true; }else{ process.exit (arguments.length === 1 ? code : this._exitStatus); } }; Argp.prototype.printVersion = function (code){ process.stdout.write (this._version ? wrap (this._version, this._columns) + "\n" : ""); //Only exit if the are no error listeners if (this.listeners ("error").length){ //Simulate an error to stop parsing the remaining options this._inputError = true; }else{ process.exit (arguments.length === 1 ? code : this._exitStatus); } }; Argp.prototype._argument = function (name, o){ this._lines.push ({ arg: name }); //Store the argument o.hidden = !!o.hidden; o.synopsis = o.synopsis || null; o.trailing = o.trailing || null; o.description = o.description || null; this._arguments[name] = o; }; Argp.prototype._option = function (o){ //Text message if (o.text !== undefined || o.columns){ this._lines.push (o); return; } o.flag = !o.metavar; o.id = o.long || o.short; o.description = o.description || null; o.hidden = !!o.hidden; o.aliases = o.aliases || []; //Clean up if (o.flag){ delete o.reviver; delete o.default; delete o.optional; delete o.type; delete o.choices; o.negate = !!o.negate; o.default = o.negate; }else{ o.type = o.type || String; o.optional = !!o.optional; o.reviver = o.reviver || null; o.choices = o.optional ? null : o.choices || null; delete o.negate; if (o.default === undefined){ if (o.type === Number){ o.default = 0; }else if (o.type === Boolean){ o.default = false; }else if (o.type === Array){ o.default = []; }else{ o.default = null; } } } //Store the option this._lines.push (o); this._options[o.id] = o; //Shortcuts if (o.short) this._optionsShort[o.short] = o; if (o.long) this._optionsLong[o.long] = o; var me = this; o.aliases.forEach (function (alias){ //Aliases are always long names me._options[alias] = me._optionsLong[alias] = o; }); }; Argp.prototype._default = function (){ var me = this; this._lines.forEach (function (o){ if (o.text !== undefined || o.columns || o.arg) return; me._argv[o.id] = o.default; }); for (var p in this._arguments){ this._argv[p] = this._arguments[p].trailing ? [] : false; } }; Argp.prototype.allowUndefinedArguments = function (){ this._allowUndefinedArguments = true; return this; }; Argp.prototype.allowUndefinedOptions = function (){ this._allowUndefinedOptions = true; return this; }; Argp.prototype.arguments = function (){ return this._arguments; }; Argp.prototype.argv = function (input){ //The Argp and Command instances execute this code var uncache = function (){ if (!instance._once) return; //Uncache the files ["index.js", "command.js", "argp.js", "body.js", "error.js", "wrap.js"] .forEach (function (filename){ delete require.cache[__dirname + path.sep + filename]; }); //The user may have a reference to the instance so it should be freed, eg. //var p = argp.createParser (). If the user doesn't free the instance, eg. //p = null, it will retain an empty object, {} for (var p in instance){ delete instance[p]; } instance.__proto__ = null; delete instance.constructor; }; var argv = input || process.argv.slice (debug ? 3 : 2); //The command name is always the first parameter var commandName = argv[0]; var command = this._commandInstances[commandName]; var instance; var o; var scriptStr = script; //Check whether the input data belongs to the main parser or to a command if (command){ instance = command; script += " " + commandName; o = command._argv = {}; //Store the first argument command._argv[commandName] = []; if (command._trailing){ command._currentArgument = { name: commandName, trailing: command._trailing }; } argv.shift (); }else{ instance = this; o = this._argv = {}; } instance._inputError = false; //Add the version option if readPackage() was used (last option) if (instance._packageVersion){ instance._option ({ short: "v", long: "version", description: "Output version information and exit" }); } //Set default values instance._default (); instance.emit ("start", instance._argv); //Read options instance._read (argv); if (instance._inputError){ uncache (); return null; } //Emit arguments if sort is enabled if (instance._sort){ instance._emitArguments = true; instance._argumentsArray.forEach (function (str){ instance._newArgument (str); }); } if (input){ script = scriptStr; } instance.emit ("end", o); uncache (); return o; }; Argp.prototype.body = function (){ this._lines = []; return new Body (this); }; Argp.prototype.columns = function (n){ this._columns = n < 32 ? 80 : n return this; }; Argp.prototype.command = function (name, o){ if (this._commands[name]){ throw new Error ("The command \"" + name + "\" is already defined"); } var c = this._commandInstances[name] = new Command (this, name, o); //Create the shortcuts this._commands[name] = { arguments: c._arguments, options: c._options }; return c; }; Argp.prototype.commands = function (){ return this._commands; }; Argp.prototype.description = function (str){ this._description = str; return this; }; Argp.prototype.email = function (str){ this._email = str; return this; }; Argp.prototype.exitStatus = function (code){ this._exitStatus = code; return this; }; Argp.prototype._createFail = function (str){ var msg = script + ": " + (str || "%s"); var error = this._errorTry (); if (error){ msg += "\n" + error; } return { message: msg, code: this._exitStatus || 1 }; }; Argp.prototype.fail = function (str, code){ var fail = this._createFail (str instanceof Error ? str.message : str); console.error (fail.message); process.exit (code || fail.code); }; Argp.prototype.footer = function (str){ this._footer = str; return this; }; Argp.prototype.main = function (){ //Better visual organization return this; }; Argp.prototype.readPackage = function (p, options){ if (typeof p === "object"){ options = p; p = null; } options = options || {}; var email = options.email !== false; var version = options.version !== false; var description = options.description !== false; try{ var json = JSON.parse (fs.readFileSync (p || "package.json", { encoding: "utf8" })); if (description && json.description){ this._description = json.description; } if (version && json.version){ //The option is added within argv() this._version = "v" + json.version; this._packageVersion = true; } if (email && json.author){ if (json.author.email){ this._email = json.author.email; }else{ var res = this._rePackageEmail.exec (json.author.name || json.author); if (res){ this._email = res[1]; } } } }catch (e){ if (e instanceof SyntaxError) throw new Error ("JSON SyntaxError reading " + "the package.json: " + e.message); if (p) throw new Error (e); } return this; }; Argp.prototype.options = function (filter){ if (filter){ if (filter.short) return this._optionsShort; if (filter.long) return this._optionsLong; } return this._options; }; Argp.prototype.sort = function (){ this._sort = true; return this; }; Argp.prototype.usages = function (arr){ this._usages = arr; return this; }; var Body = require ("./body"); var Command = require ("./command");