UNPKG

capt

Version:

Command line tool for creating backbone.js applications with coffeescript

957 lines (844 loc) 24.1 kB
/** * JavaScript Option Parser (parseopt) * Copyright (C) 2010 Mathias Panzenböck <grosser.meister.morti@gmx.net> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * Construct a new OptionParser. * See the demo folder the end of this file for example usage. * * @param object params optional Parameter-Object * * ===== Parameter-Object ===== * { * minargs: integer, optional * maxargs: integer, optional * program: string, per default inferred from process.argv * strings: object, optional * Table of strings used in the output. See below. * options: array, optional * Array of option definitions. See below. * } * * ===== String-Table ===== * { * help: string, default: 'No help available for this option.' * usage: string, default: 'Usage' * options: string, default: 'OPTIONS' * arguments: string, default: 'ARGUMENTS' * required: string, default: 'required' * default: string, default: 'default' * base: string, default: 'base' * metavars: object, optional * Table of default metavar names per type. * Per default the type name in capital letters or derived * from the possible values. * } * * ===== Option Definition ===== * { * // Only when passed to the OptionParser constructor: * name: string or array * names: string or array, alias of name * Only one of both may be used at the same time. * * Names can be long options (e.g. '--foo') and short options * (e.g. '-f'). The first name is used to indentify the option. * Names musst be unique and may not contain '='. * * Short options may be combined when passed to a programm. E.g. * the options '-f' and '-b' can be combined to '-fb'. Only one * of these combined options may require an argument. * * Short options are separated from ther arguments by space, * long options per '='. If a long option requires an argument * and none is passed using '=' it also uses the next commandline * argument as it's argument (like short options). * * If '--' is encountered all remaining arguments are treated as * arguments and not as options. * * // General fields: * target: string, per deflault inferred from first name * This defines the name used in the returned options object. * Multiple options may have the same target. * default: any, default: undefined * The default value associated with a certain target and is * overwritten by each new option with the same target. * type: string, default: 'string', see below * required: boolean, default: false * redefinable: boolean, default: true * help: string, optional * details: array, optional * short list of details shown in braces after the option name * e.g. integer type options add 'base: '+base if base !== undefined * metavar: string or array, per deflault inferred from type * onOption: function (value) -> boolean, optional * Returning true canceles any further option parsing * and the parse() method returns null. * * // Type: string (alias: str) * // Type: boolean (alias: bool) * // Type: object (alias: obj) * * // Type: integer (alias: int) * min: integer, optional * max: integer, optional * NaN: boolean, default: false * base: integer, optional * * // Type: float (alias: number) * min: float, optional * max: float, optional * NaN: boolean, default: false * * // Type: flag * value: boolean, default: true * default: boolean, default: false * * // Type: option * value: any, per default inferred from first name * * // Type: enum * ignoreCase: boolean, default: true * values: array or object where the user enteres the field name of * the object and you get the value of the field * * // Type: record * create: function () -> object, default: Array * args: array of type definitions (type part of option definitions) * * // Type: custom * argc: integer, default: -1 * Number of required arguments. * -1 means one optional argument. * parse: function (string, ...) -> value * stringify: function (value) -> string, optional * } * * ===== Option-Arguments ===== * For the following types exactly one argument is required: * integer, float, string, boolean, object, enum * * The following types have optional arguments: * flag * * The following types have no arguments: * option * * Custom types may set this through the argc field. */ function OptionParser (params) { this.optionsPerName = {}; this.defaultValues = {}; this.options = []; if (params !== undefined) { this.minargs = params.minargs == 0 ? undefined : params.minargs; this.maxargs = params.maxargs; this.program = params.program; this.strings = params.strings; if (this.minargs > this.maxargs) { throw new Error('minargs > maxargs'); } } if (this.strings === undefined) { this.strings = {}; } defaults(this.strings, { help: 'No help available for this option.', usage: 'Usage', options: 'OPTIONS', arguments: 'ARGUMENTS', required: 'required', default: 'default', base: 'base', metavars: {} }); defaults(this.strings.metavars, METAVARS); if (this.program === undefined) { this.program = process.argv[0] + ' ' + process.argv[1]; } if (params !== undefined && params.options !== undefined) { for (var i in params.options) { var opt = params.options[i]; var names; if (opt instanceof Array || typeof(opt) == 'string') { opt = undefined; names = opt; } else { names = opt.names; if (names === undefined) { names = opt.name; delete opt.name; } else { delete opt.names; } } this.add(names, opt); } } } OptionParser.prototype = { /** * Parse command line options. * * @param array args Commandline arguments. * If undefined process.argv.slice(2) is used. * * @return object * { * arguments: array * options: object, { target -> value } * } */ parse: function (args) { if (args === undefined) { args = process.argv.slice(2); } var data = { options: {}, arguments: [] }; for (var name in this.defaultValues) { var value = this.defaultValues[name]; if (value !== undefined) { data.options[this.optionsPerName[name].target] = value; } } var got = {}; var i = 0; for (; i < args.length; ++ i) { var arg = args[i]; if (arg == '--') { ++ i; break; } else if (/^--.+$/.test(arg)) { var j = arg.indexOf('='); var name, value = undefined; if (j == -1) { name = arg; } else { name = arg.substring(0,j); value = arg.substring(j+1); } var optdef = this.optionsPerName[name]; if (optdef === undefined) { throw new Error('unknown option: '+name); } if (value === undefined) { if (optdef.argc < 1) { value = optdef.value; } else if ((i + optdef.argc) >= args.length) { throw new Error('option '+name+' needs '+optdef.argc+' arguments'); } else { value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc)); i += optdef.argc; } } else if (optdef.argc == 0) { throw new Error('option '+name+' does not need an argument'); } else if (optdef.argc > 1) { throw new Error('option '+name+' needs '+optdef.argc+' arguments'); } else { value = optdef.parse(value); } if (!optdef.redefinable && optdef.target in got) { throw new Error('cannot redefine option '+name); } got[optdef.target] = true; data.options[optdef.target] = value; if (optdef.onOption && optdef.onOption(value) === true) { return null; } } else if (/^-.+$/.test(arg)) { if (arg.indexOf('=') != -1) { throw new Error('illegal option syntax: '+arg); } var tookarg = false; arg = arg.substring(1); for (var j = 0; j < arg.length; ++ j) { var name = '-'+arg[j]; var optdef = this.optionsPerName[name]; var value; if (optdef === undefined) { throw new Error('unknown option: '+name); } if (optdef.argc < 1) { value = optdef.value; } else { if (tookarg || (i+optdef.argc) >= args.length) { throw new Error('option '+name+' needs '+optdef.argc+' arguments'); } value = optdef.parse.apply(optdef, args.slice(i+1, i+1+optdef.argc)); i += optdef.argc; tookarg = true; } if (!optdef.redefinable && optdef.target in got) { throw new Error('redefined option: '+name); } got[optdef.target] = true; data.options[optdef.target] = value; if (optdef.onOption && optdef.onOption(value) === true) { return null; } } } else { data.arguments.push(arg); } } for (; i < args.length; ++ i) { data.arguments.push(args[i]); } var argc = data.arguments.length; if ((this.maxargs !== undefined && argc > this.maxargs) || (this.minargs !== undefined && argc < this.minargs)) { var msg = 'illegal number of arguments: ' + argc; if (this.minargs !== undefined) { msg += ', minumum is ' + this.minargs; if (this.maxargs !== undefined) { msg += ' and maximum is ' + this.maxargs; } } else { msg += ', maximum is ' + this.maxargs; } throw new Error(msg); } for (var i in this.options) { var optdef = this.options[i]; if (optdef.required && !(optdef.target in got)) { throw new Error('missing required option: ' + optdef.names[0]); } } return data; }, /** * Add an option definition. * * @param string or array names Option names * @param object optdef Option definition */ add: function (names, optdef) { if (typeof(names) == 'string') { names = [names]; } else if (names === undefined || names.length == 0) { throw new Error('no option name given'); } if (optdef === undefined) { optdef = {}; } optdef.names = names; for (var i in names) { var name = names[i]; var match = /(-*)(.*)/.exec(name); if (name.length == 0 || match[1].length < 1 || match[1].length > 2 || match[2].length == 0 || (match[1].length == 1 && match[2].length > 1) || match[2].indexOf('=') != -1) { throw new Error('illegal option name: ' + name); } if (name in this.optionsPerName) { throw new Error('option already exists: '+name); } } if (optdef.target === undefined) { var target = names[0].replace(/^--?/,''); if (target.toUpperCase() == target) { // FOO-BAR -> FOO_BAR target = target.replace(/[^a-zA-Z0-9]+/,'_'); } else { // foo-bar -> fooBar target = target.split(/[^a-zA-Z0-9]+/); for (var i = 1; i < target.length; ++ i) { var part = target[i]; if (part) { target[i] = part[0].toUpperCase() + part.substring(1); } } target = target.join(''); } optdef.target = target; } this._initType(optdef, optdef.names[0]); if (optdef.redefinable === undefined) { optdef.redefinable = true; } if (optdef.required === undefined) { optdef.required = false; } if (optdef.help === undefined) { optdef.help = this.strings.help; } else { optdef.help = optdef.help.trim(); } for (var i in names) { this.optionsPerName[names[i]] = optdef; } if (optdef.default !== undefined) { this.defaultValues[names[0]] = optdef.default; } this.options.push(optdef); }, /** * Show an error message, usage and exit program with exit code 1. * * @param string msg The error message * @param WriteStream out Where to write the message. * If undefined process.stdout is used. */ error: function (msg, out) { if (!out) { out = process.stdout; } out.write('*** '+msg+'\n\n'); this.usage(undefined, out); process.exit(1); }, /** * Print usage message. * * @param string help Optional additional help message. * @param WriteStream out Where to write the message. * If undefined process.stdout is used. */ usage: function (help, out) { if (!out) { out = process.stdout; } out.write(this.strings.usage+': '+this.program+' ['+ this.strings.options+']'+(this.maxargs != 0 ? ' ['+this.strings.arguments+']\n' : '\n')); out.write('\n'); out.write(this.strings.options+':\n'); for (var i in this.options) { var optdef = this.options[i]; var optnames = []; var metavar = optdef.metavar; if (metavar instanceof Array) { metavar = metavar.join(' '); } for (var j in optdef.names) { var optname = optdef.names[j]; if (metavar !== undefined) { if (optdef.argc < 2 && optname.substring(0,2) == '--') { if (optdef.argc < 0) { optname = optname+'[='+metavar+']'; } else { optname = optname+'='+metavar; } } else { optname = optname+' '+metavar; } } optnames.push(optname); } var details = optdef.details !== undefined ? optdef.details.slice() : []; if (optdef.required) { details.push(this.strings.required); } else if (optdef.argc > 0 && optdef.default !== undefined) { details.push(this.strings.default+': '+optdef.stringify(optdef.default)); } if (details.length > 0) { details = ' (' + details.join(', ') + ')'; } if (metavar !== undefined) { optnames[0] += details; out.write(' '+optnames.join('\n ')); } else { out.write(' '+optnames.join(', ')+details); } if (optdef.help) { var lines = optdef.help.split('\n'); for (var j in lines) { out.write('\n '+lines[j]); } } out.write('\n\n'); } if (help !== undefined) { out.write(help); if (help[help.length-1] != '\n') { out.write('\n'); } } }, _initType: function (optdef, name) { optdef.name = name; if (optdef.type === undefined) { optdef.type = 'string'; } else if (optdef.type in TYPE_ALIAS) { optdef.type = TYPE_ALIAS[optdef.type]; } switch (optdef.type) { case 'flag': if (optdef.value === undefined) { optdef.value = true; } optdef.parse = parseBool; optdef.argc = -1; if (optdef.default === undefined) { optdef.default = this.defaultValues[name]; if (optdef.default === undefined) { optdef.default = false; } } break; case 'option': optdef.argc = 0; if (optdef.value === undefined) { optdef.value = name.replace(/^--?/,''); } break; case 'enum': this._initEnum(optdef, name); break; case 'integer': case 'float': this._initNumber(optdef, name); break; case 'record': if (optdef.args === undefined || optdef.args.length == 0) { throw new Error('record '+name+' needs at least one argument'); } optdef.argc = 0; var metavar = []; for (var i in optdef.args) { var arg = optdef.args[i]; if (arg.target === undefined) { arg.target = i; } this._initType(arg, name+'['+i+']'); if (arg.argc < 1) { throw new Error('argument '+i+' of option '+name+ ' has illegal number of arguments'); } if (arg.metavar instanceof Array) { for (var j in arg.metavar) { metavar.push(arg.metavar[j]); } } else { metavar.push(arg.metavar); } delete arg.metavar; optdef.argc += arg.argc; } if (optdef.metavar === undefined) { optdef.metavar = metavar; } var onOption = optdef.onOption; if (onOption !== undefined) { optdef.onOption = function (values) { return onOption.apply(this, values); }; } if (optdef.create === undefined) { optdef.create = Array; } optdef.parse = function () { var values = this.create(); var parserIndex = 0; for (var i = 0; i < arguments.length;) { var arg = optdef.args[parserIndex ++]; var raw = []; for (var j = 0; j < arg.argc; ++ j) { raw.push(arguments[i+j]); } values[arg.target] = arg.parse.apply(arg, raw); i += arg.argc; } return values; }; break; case 'custom': if (optdef.argc === undefined || optdef.argc < -1) { optdef.argc = -1; } if (optdef.parse === undefined) { throw new Error( 'no parse function defined for custom type option '+name); } break; default: optdef.argc = 1; optdef.parse = PARSERS[optdef.type]; if (optdef.parse === undefined) { throw new Error('type of option '+name+' is unknown: '+optdef.type); } } initStringify(optdef); var count = 1; if (optdef.metavar === undefined) { optdef.metavar = this.strings.metavars[optdef.type]; } if (optdef.metavar === undefined) { count = 0; } else if (optdef.metavar instanceof Array) { count = optdef.metavar.length; } if (optdef.argc == -1) { if (count > 1) { throw new Error('illegal number of metavars for option '+name+ ': '+JSON.stringify(optdef.metavar)); } } else if (optdef.argc != count) { throw new Error('illegal number of metavars for option '+name+ ': '+JSON.stringify(optdef.metavar)); } }, _initEnum: function (optdef, name) { optdef.argc = 1; if (optdef.ignoreCase === undefined) { optdef.ignoreCase = true; } if (optdef.values === undefined || optdef.values.length == 0) { throw new Error('no values for enum '+name+' defined'); } initStringify(optdef); var labels = []; var values = {}; if (optdef.values instanceof Array) { for (var i in optdef.values) { var value = optdef.values[i]; var label = String(value); values[optdef.ignoreCase ? label.toLowerCase() : label] = value; labels.push(optdef.stringify(value)); } } else { for (var label in optdef.values) { var value = optdef.values[label]; values[optdef.ignoreCase ? label.toLowerCase() : label] = value; labels.push(optdef.stringify(label)); } labels.sort(); } optdef.values = values; if (optdef.metavar === undefined) { optdef.metavar = '<'+labels.join(', ')+'>'; } optdef.parse = function (s) { var value = values[optdef.ignoreCase ? s.toLowerCase() : s]; if (value !== undefined) { return value; } throw new Error('illegal value for option '+name+': '+s); }; }, _initNumber: function (optdef, name) { optdef.argc = 1; if (optdef.NaN === undefined) { optdef.NaN = false; } if (optdef.min > optdef.max) { throw new Error('min > max for option '+name); } var parse, toStr; if (optdef.type == 'integer') { parse = function (s) { var i = NaN; if (s.indexOf('.') == -1) { i = parseInt(s, optdef.base) } return i; }; if (optdef.base === undefined) { toStr = dec; } else { switch (optdef.base) { case 8: toStr = oct; break; case 10: toStr = dec; break; case 16: toStr = hex; break; default: toStr = function (val) { return val.toString(optdef.base); }; var detail = this.strings.base+': '+optdef.base; if (optdef.details) { optdef.details.push(detail); } else { optdef.details = [detail]; } } } } else { parse = parseFloat; toStr = dec; } if (optdef.metavar === undefined) { if (optdef.min === undefined && optdef.max === undefined) { optdef.metavar = this.strings.metavars[optdef.type]; } else if (optdef.min === undefined) { optdef.metavar = '...'+toStr(optdef.max); } else if (optdef.max === undefined) { optdef.metavar = toStr(optdef.min)+'...'; } else { optdef.metavar = toStr(optdef.min)+'...'+toStr(optdef.max); } } optdef.parse = function (s) { var n = parse(s); if ((!this.NaN && isNaN(n)) || (optdef.min !== undefined && n < optdef.min) || (optdef.max !== undefined && n > optdef.max)) { throw new Error('illegal value for option '+name+': '+s); } return n; }; } }; function initStringify (optdef) { if (optdef.stringify === undefined) { optdef.stringify = STRINGIFIERS[optdef.type]; } if (optdef.stringify === undefined) { optdef.stringify = stringifyAny; } } function defaults (target, defaults) { for (var name in defaults) { if (target[name] === undefined) { target[name] = defaults[name]; } } } function dec (val) { return val.toString(); } function oct (val) { return '0'+val.toString(8); } function hex (val) { return '0x'+val.toString(16); } const TRUE_VALUES = {true: true, on: true, 1: true, yes: true}; const FALSE_VALUES = {false: true, off: true, 0: true, no: true}; function parseBool (s) { s = s.trim().toLowerCase(); if (s in TRUE_VALUES) { return true; } else if (s in FALSE_VALUES) { return false; } else { throw new Error('illegal boolean value: '+s); } } function id (x) { return x; } const PARSERS = { boolean: parseBool, string: id, object: JSON.parse }; const TYPE_ALIAS = { int: 'integer', number: 'float', bool: 'boolean', str: 'string', obj: 'object' }; const METAVARS = { string: 'STRING', integer: 'INTEGER', float: 'FLOAT', boolean: 'BOOLEAN', object: 'OBJECT', enum: 'VALUE', custom: 'VALUE' }; function stringifyString(s) { if (/[\s'"\\<>,]/.test(s)) { // s = "'"+s.replace(/\\/g,'\\\\').replace(/'/g, "'\\''")+"'"; s = JSON.stringify(s); } return s; } function stringifyPrimitive(value) { return ''+value; } function stringifyAny (value) { if (value instanceof Array) { var buf = []; for (var i in value) { buf.push(stringifyAny(value[i])); } return buf.join(' '); } else if (typeof(value) == 'string') { return stringifyString(value); } else { return String(value); } } function stringifyInteger (value) { if (this.base === undefined) { return value.toString(); } switch (this.base) { case 8: return oct(value); case 16: return hex(value); default: return value.toString(this.base); } } function stringifyRecord (record) { var buf = []; for (var i = 0; i < this.args.length; ++ i) { var arg = this.args[i]; buf.push(arg.stringify(record[arg.target])); } return buf.join(' '); } const STRINGIFIERS = { string: stringifyString, integer: stringifyInteger, boolean: stringifyPrimitive, float: stringifyPrimitive, object: JSON.stringify, enum: stringifyAny, custom: stringifyAny, record: stringifyRecord }; exports.OptionParser = OptionParser;