diff --git a/README.md b/README.md index 7d579aa..6632f2e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ Then on the command line: node example.js --name='Your Name' --age 43 --height=1.234 --pets=fred,bob --hobby biking --hobby=snowboarding - ## Defining Flags @@ -50,12 +49,13 @@ To define flags, use one of the defineX functions exported by the `flags` module All the define methods take the same arguments: - flags.defineX(name, defaultValue, opt_description, opt_validator); + flags.defineX(name, defaultValue, opt_description, opt_validator, opt_isSecret); name - The flag's name defaultValue - The default value if not specified on the command line description - [optional] Description to show in the help text validator - [optional] Function for validating the input, should throw if the input isn't valid. + isSecret - [optional] Whether the flag shold be omitted from the help text. ## Querying Flag Values @@ -81,7 +81,6 @@ If you want to change flags between test cases, you may call: ## TODOs * Support --flagsfile - * Handle --help and --helpshort internally * Support multi space separated flags, e.g. --files file1 file2 file3 * Set up for npm install diff --git a/examples/example.js b/examples/example.js index 2d30bce..a6b1289 100644 --- a/examples/example.js +++ b/examples/example.js @@ -10,11 +10,15 @@ var flags = require('flags'); -flags.defineString('name', 'Billy Noone', 'Your name'); -flags.defineInteger('age', 21, 'Your age in whole years'); -flags.defineNumber('height', 1.80, 'Your height in meters'); -flags.defineStringList('pets', [], 'Comma separated list of your pets'); -flags.defineMultiString('hobby', [], 'A hobby'); +flags.defineString('name', 'Billy Noone', 'Your name.'); +flags.defineInteger('age', 21, 'Your age in whole years.\nThis is a really ' + + 'long and informative description that tells you how to accurately ' + + 'specify your age as a flag. It\'s not really useful, just using it to ' + + 'test out the help text with long descriptions.'); +flags.defineNumber('height', 1.80, 'Your height in meters.'); +flags.defineStringList('pets', [], 'Comma separated list of your pets.'); +flags.defineMultiString('hobby', ['chess', 'tv'], 'A hobby.'); +flags.defineBoolean('active', false, 'Whether the user is active.') flags.parse(); @@ -28,6 +32,3 @@ info.push('Pets : ' + flags.get('pets').join(', ')); info.push('Hobbies : \n ' + flags.get('hobby').join('\n ')); console.log(info.join('\n')); - -console.log('\nHelp Text:'); -flags.help(); \ No newline at end of file diff --git a/lib/flags.js b/lib/flags.js index 2a2da01..3dfa96c 100644 --- a/lib/flags.js +++ b/lib/flags.js @@ -22,6 +22,15 @@ var FLAGS = exports.FLAGS = {}; exports.exitOnError = true; +/** + * Allows an app to add extra usage information that will be shown in the help + * message, above the flags. + * @type {string} + */ +exports.usageInfo = 'Usage: node ' + + process.argv[1].split('/').pop() + ' [options]'; + + /** * Defines a string flag. e.g. --servername=bob * @param {string} name The flag name, should be [a-zA-Z0-9]+. @@ -31,9 +40,13 @@ exports.exitOnError = true; * @param {function(string)=} opt_validator Optional validator function that * will be called when parsing flags. Should throw if the input is not * valid. + * @param {boolean} opt_secret Whether the flag is 'secret' and should not be + * shown in the help text. */ -exports.defineString = function(name, defaultValue, opt_description, opt_validator) { - addFlag(name, new Flag(name, defaultValue, opt_description, opt_validator)); +exports.defineString = function( + name, defaultValue, opt_description, opt_validator, opt_secret) { + addFlag(name, new Flag + (name, defaultValue, opt_description, opt_validator, opt_secret)); }; @@ -46,9 +59,13 @@ exports.defineString = function(name, defaultValue, opt_description, opt_validat * @param {function(string)=} opt_validator Optional validator function that * will be called when parsing flags. Should throw if the input is not * valid. + * @param {boolean} opt_secret Whether the flag is 'secret' and should not be + * shown in the help text. */ -exports.defineBoolean = function(name, defaultValue, opt_description, opt_validator) { - addFlag(name, new BooleanFlag(name, defaultValue, opt_description, opt_validator)); +exports.defineBoolean = function( + name, defaultValue, opt_description, opt_validator, opt_secret) { + addFlag(name, new BooleanFlag( + name, defaultValue, opt_description, opt_validator, opt_secret)); }; @@ -61,9 +78,13 @@ exports.defineBoolean = function(name, defaultValue, opt_description, opt_valida * @param {function(string)=} opt_validator Optional validator function that * will be called when parsing flags. Should throw if the input is not * valid. + * @param {boolean} opt_secret Whether the flag is 'secret' and should not be + * shown in the help text. */ -exports.defineInteger = function(name, defaultValue, opt_description, opt_validator) { - addFlag(name, new IntegerFlag(name, defaultValue, opt_description, opt_validator)); +exports.defineInteger = function( + name, defaultValue, opt_description, opt_validator, opt_secret) { + addFlag(name, new IntegerFlag( + name, defaultValue, opt_description, opt_validator, opt_secret)); }; @@ -76,39 +97,51 @@ exports.defineInteger = function(name, defaultValue, opt_description, opt_valida * @param {function(string)=} opt_validator Optional validator function that * will be called when parsing flags. Should throw if the input is not * valid. + * @param {boolean} opt_secret Whether the flag is 'secret' and should not be + * shown in the help text. */ -exports.defineNumber = function(name, defaultValue, opt_description, opt_validator) { - addFlag(name, new NumberFlag(name, defaultValue, opt_description, opt_validator)); +exports.defineNumber = function( + name, defaultValue, opt_description, opt_validator, opt_secret) { + addFlag(name, new NumberFlag( + name, defaultValue, opt_description, opt_validator, opt_secret)); }; /** * Defines a string list flag. e.g. --anmial=frog,bat,chicken * @param {string} name The flag name, should be [a-zA-Z0-9]+. - * @param {!Array.} defaultValue The default value, should the flag not be - * explicitly specified. + * @param {!Array.} defaultValue The default value, should the flag not + * be explicitly specified. * @param {string=} opt_description Optional description to use in help text. * @param {function(string)=} opt_validator Optional validator function that * will be called when parsing flags. Should throw if the input is not * valid. + * @param {boolean} opt_secret Whether the flag is 'secret' and should not be + * shown in the help text. */ -exports.defineStringList = function(name, defaultValue, opt_description, opt_validator) { - addFlag(name, new StringListFlag(name, defaultValue, opt_description, opt_validator)); +exports.defineStringList = function( + name, defaultValue, opt_description, opt_validator, opt_secret) { + addFlag(name, new StringListFlag( + name, defaultValue, opt_description, opt_validator, opt_secret)); }; /** * Defines a multi string flag. e.g. --allowedip=127.0.0.1 --allowedip=127.0.0.2 * @param {string} name The flag name, should be [a-zA-Z0-9]+. - * @param {!Array.} defaultValue The default value, should the flag not be - * explicitly specified. + * @param {!Array.} defaultValue The default value, should the flag not + * be explicitly specified. * @param {string=} opt_description Optional description to use in help text. * @param {function(string)=} opt_validator Optional validator function that * will be called when parsing flags. Should throw if the input is not * valid. + * @param {boolean} opt_secret Whether the flag is 'secret' and should not be + * shown in the help text. */ -exports.defineMultiString = function(name, defaultValue, opt_description, opt_validator) { - addFlag(name, new MultiStringFlag(name, defaultValue, opt_description, opt_validator)); +exports.defineMultiString = function( + name, defaultValue, opt_description, opt_validator, opt_secret) { + addFlag(name, new MultiStringFlag( + name, defaultValue, opt_description, opt_validator, opt_secret)); }; @@ -116,9 +149,14 @@ exports.defineMultiString = function(name, defaultValue, opt_description, opt_va * Dumps the help text to the console. */ exports.help = function() { - // TODO: make this suck less and automatically hook up to --help + if (exports.usageInfo) { + console.log(exports.usageInfo + '\n'); + } + console.log('Options:'); for (var flag in FLAGS) { - console.log('--' + flag + ' : ' + FLAGS[flag].description); + if (!FLAGS[flag].isSecret) { + console.log(FLAGS[flag].toHelpString()); + } } }; @@ -129,6 +167,7 @@ exports.help = function() { exports.reset = function() { parseCalled = false; FLAGS = exports.FLAGS = {}; + registerInternalFlags(); }; @@ -202,6 +241,12 @@ exports.parse = function(opt_args) { parseCalled = true; + // Intercept the --help flag. + if (FLAGS.help.get()) { + exports.help(); + process.exit(0); + } + if (i != args.length) { return args.slice(i + 1); } else { @@ -210,7 +255,6 @@ exports.parse = function(opt_args) { }; - // Private helpers //================== @@ -240,17 +284,59 @@ function addFlag(name, flag) { } +function wrapText(text, maxLen) { + var lines = text.split('\n'); + var out = []; + for (var i = 0; i < lines.length; i++) { + // Adjust the maxLength for to take into account the lack of indent on the + // first line. + var maxLenx = maxLen + (out.length == 0 ? 4 : 0); + var line = lines[i]; + if (line.length < maxLenx) { + // Line doesn't exceed length so just push it. + out.push(line) + } else { + // Wrap the line on spaces. + // TODO : Would be nice to split URLs and long phrases that have no + // natural spaces. + var current = wrapLine(line, ' ', maxLenx, out); + if (current != '') { + out.push(current); + } + } + } + return out.join('\n '); +} + + +function wrapLine(line, delimiter, maxLen, out) { + var parts = line.split(delimiter); + var current = ''; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + var next = current + part + delimiter; + if (next.length > maxLen) { + out.push(current); + current = part + delimiter; + } else { + current = next; + } + } + return current; +} + /** * @constructor */ -function Flag(name, defaultValue, description, validator) { +function Flag(name, defaultValue, description, validator, isSecret) { this.name = name; this.defaultValue = defaultValue; this.description = description; this.validator = validator; this.currentValue = null; this.isSet = false; + this.isSecret = !!isSecret; } @@ -274,11 +360,16 @@ Flag.prototype.parseInput = function(inp) { }; +Flag.prototype.toHelpString = function() { + return wrapText(' --' + this.name + ': ' + this.description, 70) + '\n' + + ' (default: ' + JSON.stringify(this.defaultValue) + ')'; +}; + /** * @constructor */ -function BooleanFlag(flag, defaultValue, converter, description, validator) { +function BooleanFlag() { BooleanFlag.super_.apply(this, arguments); } util.inherits(BooleanFlag, Flag); @@ -299,11 +390,17 @@ BooleanFlag.prototype.parseInput = function(inp) { }; +BooleanFlag.prototype.toHelpString = function() { + return wrapText(' --[no]' + this.name + ': ' + this.description, 70) + '\n' + + ' (default: ' + JSON.stringify(this.defaultValue) + ')'; +}; + + /** * @constructor */ -function IntegerFlag(flag, defaultValue, converter, description, validator) { +function IntegerFlag() { IntegerFlag.super_.apply(this, arguments); } util.inherits(IntegerFlag, Flag); @@ -317,11 +414,16 @@ IntegerFlag.prototype.parseInput = function(inp) { }; +IntegerFlag.prototype.toHelpString = function() { + return IntegerFlag.super_.prototype.toHelpString.call(this) + + '\n (an integer)'; +}; + /** * @constructor */ -function NumberFlag(flag, defaultValue, converter, description, validator) { +function NumberFlag() { NumberFlag.super_.apply(this, arguments); } util.inherits(NumberFlag, Flag); @@ -335,11 +437,17 @@ NumberFlag.prototype.parseInput = function(inp) { }; +NumberFlag.prototype.toHelpString = function() { + return NumberFlag.super_.prototype.toHelpString.call(this) + + '\n (a number)'; +}; + + /** * @constructor */ -function StringListFlag(flag, defaultValue, converter, description, validator) { +function StringListFlag() { StringListFlag.super_.apply(this, arguments); } util.inherits(StringListFlag, Flag); @@ -354,7 +462,7 @@ StringListFlag.prototype.parseInput = function(inp) { /** * @constructor */ -function MultiStringFlag(flag, defaultValue, converter, description, validator) { +function MultiStringFlag() { MultiStringFlag.super_.apply(this, arguments); this.currentValue = []; } @@ -366,3 +474,15 @@ MultiStringFlag.prototype.set = function(input) { this.currentValue.push(input); this.isSet = true; }; + + + +// Internal flags +//================= + +function registerInternalFlags() { + exports.defineBoolean('help', false, 'Shows this help text.', null, true); + // TODO: --flagsfile +} + +registerInternalFlags();