node-cmdln
is a node.js helper lib for creating CLI tools with subcommands
(think git
, svn
, zfs
, brew
, etc.). It is a sister of my earlier
Python lib for this.
You define a subclass of Cmdln
and subcommands as do_NAME
methods.
Minimally you could have a "conan.js" as follows:
#!/usr/bin/env node
var util = require('util');
var cmdln = require('cmdln');
function Conan() {
cmdln.Cmdln.call(this, {
name: 'conan',
desc: 'What is best in life?'
});
}
util.inherits(Conan, cmdln.Cmdln);
Conan.prototype.do_crush = function do_crush(subcmd, opts, args, cb) {
console.log('Yargh!');
cb();
};
Conan.prototype.do_crush.help = 'Crush your enemies.';
cmdln.main(new Conan()); // mainline
With this, you get the following behaviour:
$ node examples/conan.js
What is best in life?
Usage:
conan [OPTIONS] COMMAND [ARGS...]
conan help COMMAND
Options:
-h, --help Show this help message and exit.
Commands:
help (?) Help on a specific sub-command.
crush Crush your enemies.
$ node examples/conan.js help crush
Crush your enemies.
$ node examples/conan.js crush
Yargh!
Option processing (using dashdash)
is integrated. do_crush
above could be replaced with:
Conan.prototype.do_crush = function (subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
if (!args.length) {
console.log('No enemies? Yarg!');
} else {
args.forEach(function (enemy) {
console.log('Smite %s with a %s!', enemy, opts.weapon);
});
}
cb();
};
Conan.prototype.do_crush.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['weapon', 'w'],
helpArg: 'WEAPON',
type: 'string',
default: 'sword',
help: 'Weapon with which to smite.'
}
];
Conan.prototype.do_crush.help = (
'Crush your enemies.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} {{cmd}} [OPTIONS] [ENEMIES...]\n'
+ '\n'
+ '{{options}}'
);
Then we get this behaviour:
$ node examples/conan.js crush Bob
Smite Bob with a sword!
$ node examples/conan.js crush Bob Linda --weapon mattock
Smite Bob with a mattock!
Smite Linda with a mattock!
$ node examples/conan.js crush -h
Crush your enemies.
Usage:
conan crush [OPTIONS] [ENEMIES...]
Options:
-h, --help Show this help.
-w WEAPON, --weapon=WEAPON Weapon with which to smite.
See examples/conan.js for the complete example. Run
node example/conan.js ...
to try it out.
One can generate Bash completion code for a Cmdln
subclass via
cli.bashCompletion()
One possible usage is to add a completion
subcmd to your CLI:
CLI.prototype.do_completion = function (subcmd, opts, args, cb) {
console.log( this.bashCompletion() );
cb();
};
and get users to use that to setup Bash completion:
$ alias conan="node examples/conan.js"
$ conan completion > conan.completion
$ source conan.completion
$ conan <TAB>
crush hear help pulverize see smash
$ conan -<TAB>
--help --verbose --version -h -v -x
$ conan crush --weapon <TAB> # custom 'weapon' completion type
bow-and-array mattock spear sword
$ conan crush --weapon spear <TAB> # custom 'enemy' completion type
King-Osric Subotai Thulsa-Doom _mbsetupuser trentm
See the do_completion
subcommand on "examples/conan.js" for a complete example
of this. See the equivalent in the larger triton
tool for another example:
https://github.com/joyent/node-triton/blob/master/lib/do_completion.js.
Another potential usage could be to pre-generate a completion file and distribute it with your tool.
In general, also please read the comments in the source and browse the examples.
To use this module you create a class that inherits from cmdln.Cmdln
; add
some methods to that class that define the tool's commands, options, etc.;
then pass an instance to cmdln.main()
. Roughly like this:
function CLI() {
cmdln.Cmdln.call(this, {<config>});
}
util.inherits(CLI, cmdln.Cmdln);
...
var cli = new CLI();
cmdln.main(cli);
We'll use the CLI
and cli
names as used above in the following reference:
-
new Cmdln(<config>)
Create a Cmdln subclass instance. See the block comment in the code for full documentation on theconfig
options. -
CLI.prototype.do_<subcmd> = function (subcmd, opts, args, cb)
is how a subcommand is defined. How the subcmd is handled can be customize with some properties (e.g.options
,help
) on the handler function. -
CLI.prototype.do_<subcmd> = <SubCLI>;
Instead of a function handler for a subcommand, ado_<subcmd>
can be set to another Cmdln subclass to support sub-subcommands, likegit remote add|remove|rename|...
. See "examples/fauxgit.js" for an example. -
CLI.prototype.do_<subcmd>.aliases = <array of strings>;
to define one or more aliases for a command. These aliases are shown in the "Commands:" section of the generated help output. -
CLI.prototype.do_<subcmd>.hiddenAliases = <array of strings>;
to define one or more aliases for a command that are not shown in the generated help output. This can be useful when renaming a subcommand in a new version of a tool and still support the old name. -
CLI.prototype.do_<subcmd>.options = <object>;
is how to set the options (in dashdash format) for that subcommand. -
CLI.prototype.do_<subcmd>.synopses = <array of strings>;
Set to the synopsis string(s) for this command, i.e. the part typically in the "SYNOPSIS" section of a man page. See examples/conan.js. This supports some template variables:{{name}}
becomescli.name
(i.e. the tool name).{{cmd}}
becomes the sub-command name.
Setting
synopses
can be used (a) for the{{usage}}
template var in subcmd help (see below) and (b) forerrHelp
forUsageError
s (see below). -
CLI.prototype.do_<subcmd>.helpOpts = <dashdash helpOpts object>;
to override formatting settings foroptions
help output for this command. By default thehelpOpts
passed into the CLI constructor are used. The set of supported helpOpts are defined by dashdash. -
CLI.prototype.do_<subcmd>.help = <string>;
to set the help string for a subcommand. This supports some template variables:{{name}}
becomescli.name
(i.e. the tool name).{{cmd}}
becomes the sub-command name.{{usage}}
becomes a "Usage:\n $synopses" block ifsynopses
are defined (see above).{{options}}
becomes a "Options:\n $option-help" block ifoptions
are provided for the subcmd (see above).
-
CLI.prototype.do_<subcmd>.help = function (subcmd, opts, args, cb)
is an alternate method to handle help for a subcommand. The given function will be run whentool help <subcmd>
is called. -
CLI.prototype.do_<subcmd>.desc = <string>;
can be set to a short string to be used in thetool help
output to summarize subcmd. If not provided, then the first line ofdo_<subcmd>.help
will be used. -
CLI.prototype.do_<subcmd>.hidden = <boolean>;
Set to false to havetool help
output not list this subcmd. -
CLI.prototype.do_<subcmd>.interspersedOptions = <boolean>;
Set to false to havetool <subcmd> ...
not allow interspersed options (i.e. options after the first argument). -
CLI.prototype.do_<subcmd>.allowUnknownOptions = <boolean>;
Set to true to havetool <subcmd> ...
allow unknown options. -
CLI.prototype.do_<subcmd>.completionArgtypes = <array>;
Set to an array of strings to define the Bash completion type for the corresponding positional arg. For example, the following: MyCLI.prototype.do_foo.completionArgtypes = ['fruit', 'file']; would mean thatmycli foo <TAB>
would complete "fruit" (using acomplete_fruit
bash function, typically provided via thespecExtra
arg to<cli>.bashCompletion()
) and the second and subsequent positional args --mycli foo banana <TAB>
-- would use filename completion. -
CLI.prototype.init(opts, args, cb)
Hook run after option processing (this.opts
is set), but before the subcommand handler is run. -
CLI.prototype.fini(subcmd, err, cb)
Hook run after the subcommand handler is run. Hereerr
is the error returned by the invocation of the CLI. This allows afini
method to use or deal with that error, if necessary. To just pass that err on (to the callingmain
) do this:CLI.prototype.fini = function fini(subcmd, err, cb) { // Whatever finalization you want to do here (possibly with a // `finiErr`) ... cb(finiErr || err, subcmd); };
(Note: The call signature to
fini
changed in cmdln v3. See the changelog in CHANGES.md.) -
CLI.prototype.defaultHandler(subcmd, opts, args, cb)
This is a hook function to handle an unknown option. By default it will callback withUnknownCommandError(subcmd)
. It can be overriden, for example as follows:/* * Provide the `jirash KEY-1` shortcut for `jirash issue get KEY-1`. */ JirashCli.prototype.defaultHandler = function defaultHandler( subcmd, opts, args, cb) { var keyRe = /^[A-Z]+-\d+$/; if (keyRe.test(subcmd)) { this.handlerFromSubcmd('issue').dispatch({ subcmd: 'get', opts: {'short': true}, args: [subcmd] }, cb); } else { Cmdln.prototype.defaultHandler.call(this, subcmd, opts, args, cb); } };
-
cli.showErrStack
boolean. Set to true to havecmdln.main()
, if used, print a full stack on a shown error. A common pattern of mine is to set this in the.init()
method if a top-level-v,--verbose
option is given. -
cli.handlerFromSubcmd(<subcmd>)
will return the appropriatedo_<subcmd>
method that handles the given sub-command. This resolves sub-command aliases. -
cli.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 defaultdo_help
implementation. -
cli.bashCompletion()
generates and returns bash completion for the CLI.
This is a convenience method for driving the mainline of your script using
the your defined Cmdln
subclass. There are a number of options to control
how it works. Read the block comment on that function in "lib/cmdln.js" for
the best docs.
cmdln v4 introduced subcmd synopses, errHelp
, and some related
functionality to help provide brief automatic command help for some usage
errors. errHelp
is a brief message after a printed error, giving potentially
helpful info. Some examples from familiar commands (marked here with >
):
$ ls -D
ls: illegal option -- D
> usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
$ git foo
git: 'foo' is not a git command. See 'git --help'.
> Did you mean this?
> fo
Use the following suggestions to get this kind of error-help for your commands:
-
Optionally set
synopses
on your subcmd handlers. E.g.:do_list.synopses = ['{{name}} list [OPTIONS] FILTERS...'];
Doing so allows two things: (a) the use of the
{{usage}}
template var in your command help, and (b) use of those synopses forerrHelp
. -
Optionally use the
{{usage}}
template var in your command help. E.g.:do_list.help = [ 'List instances.', '', '{{usage}}', '', '{{options}}' ].join('\n');
-
Optionally use the
cmdln.UsageError
error class for usage errors in your subcmds. E.g.:function do_list(subcmd, opts, args, callback) { // ... } else if (args.length < 1) { callback(new cmdln.UsageError('missing FILTER args')); return; }
-
Use
cmdln.main()
for your mainlineThis will now attempt to determine
errHelp
from any returned error and print it on stderr -- useoptions.showErrHelp=false
to disable. Or if you are not usingcmdln.main()
, then you can usecmdln.errHelpFromErr(err)
to get errHelp to print, if you like.
Error help is determined by calling err.cmdlnErrHelpFromErr()
, which is
implemented for cmdln's error classes:
cmdln.OptionError
: Show a synopsis of the command's options.cmdln.UsageError
: Show the command's synopses, if available.cmdln.UnknownCommandError
: List possible fuzzy matches.
You can implement that method for custom error classes if you like.
This is a re-export of the dashdash
option processing module that cmdln is using. This is exported so that calling
code can add option types if wanted, via cmdln.dashdash.addOptionType
. E.g.,
var cmdln = require('cmdln');
function parseCommaSepStringNoEmpties(option, optstr, arg) {
return arg.trim().split(/\s*,\s*/g)
.filter(function (part) { return part; });
}
cmdln.dashdash.addOptionType({
name: 'commaSepString',
takesArg: true,
helpArg: 'STRING',
parseArg: parseCommaSepStringNoEmpties
});
// ...
See the node-dashdash documentation for details.
MIT. See LICENSE.txt