Skip to content

Commit

Permalink
Make --help nicer
Browse files Browse the repository at this point in the history
  • Loading branch information
dpup committed Mar 29, 2011
1 parent 42885ad commit e43ecce
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 36 deletions.
5 changes: 2 additions & 3 deletions README.md
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

17 changes: 9 additions & 8 deletions examples/example.js
Expand Up @@ -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();

Expand All @@ -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();
170 changes: 145 additions & 25 deletions lib/flags.js
Expand Up @@ -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]+.
Expand All @@ -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));
};


Expand All @@ -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));
};


Expand All @@ -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));
};


Expand All @@ -76,49 +97,66 @@ 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.<string>} defaultValue The default value, should the flag not be
* explicitly specified.
* @param {!Array.<string>} 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.<string>} defaultValue The default value, should the flag not be
* explicitly specified.
* @param {!Array.<string>} 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));
};


/**
* 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());
}
}
};

Expand All @@ -129,6 +167,7 @@ exports.help = function() {
exports.reset = function() {
parseCalled = false;
FLAGS = exports.FLAGS = {};
registerInternalFlags();
};


Expand Down Expand Up @@ -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 {
Expand All @@ -210,7 +255,6 @@ exports.parse = function(opt_args) {
};



// Private helpers
//==================

Expand Down Expand Up @@ -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;
}


Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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 = [];
}
Expand All @@ -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();

0 comments on commit e43ecce

Please sign in to comment.