cmdln
Advanced tools
Comparing version 3.0.2 to 3.1.0
# node-cmdln Changelog | ||
## 3.1.0 | ||
- [issue #5] Add `helpSubcmds` constructor option to allow control over the | ||
output of the "Commands:" section of top-level help. For example, this code: | ||
helpSubcmds: [ | ||
'help', | ||
{ group: '' }, | ||
'in-empty-group', | ||
{ group: 'Most Excellent Commands' }, | ||
'awesome', | ||
{ group: 'Other Commands', unmatched: true } | ||
] | ||
yields help output something like: | ||
... | ||
Commands: | ||
help (?) Help on a specific sub-command. | ||
in-empty-group Do in-empty-group things. | ||
Most Excellent Commands: | ||
awesome Do awesome things. | ||
Other Commands: | ||
something-else Do something-else things. | ||
By Josh Clulow. | ||
- [issue #4] Add `Cmdln.prototype.helpFromSubcmd(subcmd)` to get the help string | ||
for the given subcommand. This can be useful for tools that want to emit usage | ||
information as part of a usage error. E.g.: | ||
MyCLI.prototype.do_frob = do_frob(subcmd, opts, args, cb) { | ||
if (!opts.frobber) { | ||
return callback(new Error('you forgot the frobber\n' + | ||
this.helpFromSubcmd(subcmd))); | ||
} | ||
// ... | ||
}; | ||
## 3.0.2 | ||
@@ -4,0 +47,0 @@ |
189
lib/cmdln.js
/* | ||
* Copyright (c) 2014, Trent Mick. All rights reserved. | ||
* Copyright (c) 2014, Joyent, Inc. All rights reserved. | ||
* Copyright 2015 Joyent, Inc. | ||
*/ | ||
var DEBUG = false; | ||
@@ -166,2 +167,16 @@ if (DEBUG) { | ||
* help output. | ||
* - @param helpSubcmds {Array} Control the output of the "Commands:" | ||
* section of top-level help output. By default all commands are | ||
* listed in the order defined in code. `helpSubcmds` allows one to | ||
* specify the order and, optionally, groupings with group headers. E.g.: | ||
* helpSubcmds: [ | ||
* 'help', | ||
* { group: '' }, // an empty group, results in a blank line | ||
* 'foo', | ||
* 'bar', | ||
* { group: 'Bling' }, // a group header | ||
* 'bloom', | ||
* // Use `unmatched: true` to include remaining subcmds. | ||
* { group: 'Other Commands', unmatched: true } | ||
* ] | ||
* - @param options {Array} Custom options (in the format used by | ||
@@ -188,2 +203,3 @@ * [dashdash](https://github.com/trentm/node-dashdash)). If not | ||
assert.optionalString(config.helpBody, 'config.helpBody') | ||
assert.optionalObject(config.helpSubcmds, 'config.helpSubcmds'); | ||
@@ -195,2 +211,3 @@ this.name = config.name || this.constructor.name.toLowerCase(); | ||
this.helpBody = config.helpBody; | ||
this.helpSubcmds = config.helpSubcmds || null; | ||
if (!this.helpOpts.indent) | ||
@@ -200,2 +217,8 @@ this.helpOpts.indent = space(4); | ||
this.helpOpts.indent = space(this.helpOpts.indent); | ||
if (!this.helpOpts.groupIndent) { | ||
var gilen = Math.round(this.helpOpts.indent.length / 2); | ||
this.helpOpts.groupIndent = space(gilen); | ||
} else if (typeof (this.helpOpts.groupIndent) === 'number') { | ||
this.helpOpts.groupIndent = space(this.helpOpts.groupIndent); | ||
} | ||
if (!this.helpOpts.maxCol) this.helpOpts.maxCol = 80; | ||
@@ -220,3 +243,3 @@ if (!this.helpOpts.minHelpCol) this.helpOpts.minHelpCol = 20; | ||
// Load subcmds (do_* methods) and aliases (`do_*.aliases`). | ||
this._subcmdOrder = []; | ||
var enumOrder = []; | ||
this._handlerFromName = {}; | ||
@@ -281,3 +304,3 @@ this._nameFromAlias = {}; | ||
} | ||
self._subcmdOrder.push(name); | ||
enumOrder.push(name); | ||
self._nameFromAlias[name] = name; | ||
@@ -289,2 +312,63 @@ (func.aliases || []).forEach(function (alias) { | ||
}); | ||
if (self.helpSubcmds !== null) { | ||
/* | ||
* Reconcile the provided subcommand order (and group headings) with | ||
* the discovered options. | ||
*/ | ||
var unmatchedNames = []; | ||
var matchedNames = []; | ||
enumOrder.forEach(function (enumName) { | ||
if (self.helpSubcmds.indexOf(enumName) === -1) { | ||
unmatchedNames.push(enumName); | ||
} else { | ||
matchedNames.push(enumName); | ||
} | ||
}); | ||
var unmatchCount = 0; | ||
self._subcmdOrder = []; | ||
self.helpSubcmds.forEach(function (sc) { | ||
if (typeof (sc) === 'object') { | ||
assert.string(sc.group, 'helpSubcmds.*.group'); | ||
assert.optionalBool(sc.unmatched, 'helpSubcmds.*.unmatched'); | ||
self._subcmdOrder.push(sc); | ||
if (sc.unmatched) { | ||
if (++unmatchCount > 1) { | ||
throw (new Error(format('"unmatched" directive used ' + | ||
'more than once in "helpSubcmds" option: %j', sc))); | ||
} | ||
/* | ||
* Include all of the unmatched names here: | ||
*/ | ||
while (unmatchedNames.length > 0) { | ||
self._subcmdOrder.push(unmatchedNames.shift()); | ||
} | ||
} | ||
return; | ||
} | ||
/* | ||
* If this is not a group heading object, it must be the name | ||
* of a handler to include in the output: | ||
*/ | ||
assert.string(sc); | ||
if (matchedNames.indexOf(sc) === -1) { | ||
throw (new Error('command handler included in help order ' + | ||
'but not found: ' + sc)); | ||
} | ||
self._subcmdOrder.push(sc); | ||
}); | ||
if (unmatchedNames.length > 0) { | ||
throw (new Error('"helpSubcmds" error: unmatched command ' + | ||
'handlers found: ' + unmatchedNames.join(', ') + '.')); | ||
} | ||
} else { | ||
self._subcmdOrder = enumOrder; | ||
} | ||
// p('_subcmdOrder:', this._subcmdOrder); | ||
@@ -416,2 +500,3 @@ // p('_handlerFromName: ', this._handlerFromName); | ||
var indent = helpOpts.indent; | ||
var gindent = helpOpts.groupIndent; | ||
@@ -444,3 +529,25 @@ var lines = []; | ||
indent, helpOpts.minHelpCol - indent.length - 2); | ||
this._subcmdOrder.forEach(function (name) { | ||
this._subcmdOrder.forEach(function (name, idx) { | ||
if (typeof (name) === 'object') { | ||
if (idx > 0) { | ||
/* | ||
* If this is not the first line, print a blank line to | ||
* visually separate this group from previous lines. | ||
*/ | ||
lines.push(''); | ||
} | ||
/* | ||
* If the group name is not blank, print the group heading. | ||
* If it is blank, the caller only wants the separator line | ||
* printed above. | ||
*/ | ||
assert.string(name.group, 'name.group'); | ||
if (name.group) { | ||
lines.push(format('%s%s:', gindent, name.group)); | ||
} | ||
return; | ||
} | ||
assert.string(name, 'name'); | ||
var handler = self._handlerFromName[name]; | ||
@@ -454,5 +561,5 @@ if (handler.hidden) { | ||
} | ||
var summary = handler.desc | ||
|| (typeof (handler.help) === 'string' && handler.help) | ||
|| ''; | ||
var summary = handler.desc || | ||
(typeof (handler.help) === 'string' && handler.help) || | ||
''; | ||
summary = summary.split('\n', 1)[0]; // just leading line | ||
@@ -491,2 +598,42 @@ summary = summary.replace(/{{name}}/g, self.name); | ||
/** | ||
* Return the help content for the given sub-command string (aka the | ||
* subcmd *name*). | ||
* | ||
* *Limitation*: If the command has a `.help` *function*, then the function is | ||
* returned. It is up to the caller to call it, if they like. The `.help` | ||
* function is defined to be async and is *not* defined to return the | ||
* string, so running it isn't useful here. | ||
* | ||
* @param alias {String} The sub-command name or alias. | ||
* @throws `UnknownCommandError` if there is no such sub-command. | ||
* @returns The help string, `null` if no help, or the `.help` function if | ||
* so defined for that sub-command. | ||
*/ | ||
Cmdln.prototype.helpFromSubcmd = function helpFromSubcmd(alias) { | ||
var handler = this.handlerFromSubcmd(alias); | ||
if (!handler) { | ||
throw new UnknownCommandError(alias); | ||
} | ||
if (!handler.help) { | ||
return; | ||
} else if (typeof (handler.help) === 'function') { | ||
return handler.help; | ||
} else { | ||
var help = handler.help; | ||
help = help.replace(/{{name}}/g, this.name); | ||
if (~help.indexOf('{{options}}') && handler.options) { | ||
var parser = new dashdash.Parser({options: handler.options}); | ||
var helpOpts = (handler.helpOpts | ||
? objMerge(this.helpOpts, handler.helpOpts) : this.helpOpts); | ||
help = help.replace('{{options}}', | ||
'Options:\n' + parser.help(helpOpts)); | ||
} | ||
help = help.trimRight(); | ||
return help; | ||
} | ||
}; | ||
/** | ||
* Dispatch to the appropriate "do_SUBCMD" function. | ||
@@ -518,3 +665,2 @@ */ | ||
Cmdln.prototype.do_help = function do_help(subcmd, opts, args, callback) { | ||
var self = this; | ||
if (args.length === 0) { | ||
@@ -525,4 +671,4 @@ this.printHelp(callback); | ||
var alias = args[0]; | ||
var name = this._nameFromAlias[alias]; | ||
if (!name) { | ||
var handler = this.handlerFromSubcmd(alias); | ||
if (!handler) { | ||
callback(new UnknownCommandError(alias)); | ||
@@ -532,18 +678,13 @@ return; | ||
var func = this._handlerFromName[name]; | ||
if (!func.help) { | ||
try { | ||
var help = this.helpFromSubcmd(alias); | ||
} catch (e) { | ||
callback(e); | ||
} | ||
if (!help) { | ||
callback(new CmdlnError({message: format('no help for "%s"', alias)})); | ||
} else if (typeof (func.help) === 'function') { | ||
func.help(subcmd, opts, args, callback); | ||
} else if (typeof (help) === 'function') { | ||
help(subcmd, opts, args, callback); | ||
} else { | ||
var help = func.help; | ||
help = help.replace(/{{name}}/g, self.name); | ||
if (~help.indexOf('{{options}}') && func.options) { | ||
var parser = new dashdash.Parser({options: func.options}); | ||
var helpOpts = (func.helpOpts | ||
? objMerge(this.helpOpts, func.helpOpts) : this.helpOpts); | ||
help = help.replace('{{options}}', | ||
'Options:\n' + parser.help(helpOpts)); | ||
} | ||
console.log(help.trimRight()); | ||
console.log(help); | ||
callback(); | ||
@@ -550,0 +691,0 @@ } |
{ | ||
"name": "cmdln", | ||
"version": "3.0.2", | ||
"version": "3.1.0", | ||
"description": "helper lib for creating CLI tools with subcommands; think `git`, `svn`, `zfs`", | ||
@@ -5,0 +5,0 @@ "author": "Trent Mick (http://trentm.com)", |
@@ -203,3 +203,7 @@ `node-cmdln` is a node.js helper lib for creating CLI tools with subcommands | ||
- `<Cmdln>.helpFromSubcmd(<subcmd>)` will return the help string for | ||
that subcmd *or*, if defined, the help function defined for that subcmd. | ||
This is used by the default `do_help` implementation. | ||
## `cmdln.main()` | ||
@@ -206,0 +210,0 @@ |
# 2.1? | ||
- support for sub-subcommands | ||
- bash completion generation support | ||
@@ -5,0 +4,0 @@ - docs: see below |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
48213
745
219