The complete solution for node.js interactive interfaces, focused in bots, based on and inspired by Commander.js.
- Installation
- Command parsing
- Specify the argument syntax
- Variadic arguments
- Option parsing
- Subcommands
- Load plugins
- Output
- Metadata
- Automated help
- Configuration
- Examples
- License
$ npm install bot-commander
Commands are defined with the .command()
method, it will return the new command to configure it with a fluent api. The .action()
command sets the callback to be called when the command is recognized by default nothing will be called creating a command without action can be useful for creating subcommand APIs.
.description()
method serves as documentation for the command, .alias()
can be used to define command alias so both command names are interchangeable.
Options can be passed with the call to .command()
. Specifying true
for opts.noHelp
will remove the option from the generated help output.
const bot = require(bot-commander);
bot
.command('test')
.alias('testAlias')
.description('This is a test command')
.action( a => {
//This will be called when this command is found in the input
});
bot.
.command('onlyCommand')
.action( a => {
//This will be called when this command is found in the input
});
let input = 'test';
//This will parse the input and call the 'test' command action.
bot.parse(input);
const bot = require(bot-commander);
bot.
.command('test <required> [optional]')
.action( (meta, required, optional) => {
if (optional) {
console.log(optional);
}
console.log(required);
});
bot.parse('test req');
//The output will be 'req'
bot.parse('test req opt');
//The output will be 'opt req'
Angled brackets (e.g. <required>
) indicate required input. Square brackets (e.g. [optional]
) indicate optional input.
The arguments are applied to the callback function in the same order as the are found, the first argument will always be the metadata described in Metadata, all arguments will appear after it.
Argument definition and parsing support single and double quoted arguments, so the communication it is not confined to one word values.
const bot = require(bot-commander);
bot.
.command('test ["multi word argument"]')
.action( (meta, arg) => {
console.log(arg);
});
bot.parse("test 'this is the first argument'");
//The output will be 'this is the first argument'
The last argument of a command can be variadic, and only the last argument. To make an argument variadic you have to append ...
to the argument name. Here is an example:
const bot = require(bot-commander);
bot
.command('rmdir <dir> [otherDirs...]')
.action((meta, dir, otherDirs) => {
console.log('rmdir %s', dir);
if (otherDirs) {
otherDirs.forEach(oDir => console.log('rmdir %s', oDir));
}
});
bot.parse('rmdir dir1 dir2 dir3');
An Array
is used for the value of a variadic argument.
Options with commander are defined with the .option()
method, also serving as documentation for the options. The example below parses args and options.
const bot = require(bot-commander);
bot
.command('pizza')
.option('-p, --peppers', 'Add peppers')
.option('-P, --pineapple', 'Add pineapple')
.option('-b, --bbq-sauce', 'Add bbq sauce')
.option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
.action((meta, opts) => {
console.log('you ordered a pizza with:');
if (opts.peppers) console.log(' - peppers');
if (opts.pineapple) console.log(' - pineapple');
if (opts.bbqSauce) console.log(' - bbq');
console.log(' - %s cheese', opts.cheese);
});
Short flags may be passed as a single arg, for example -abc
is equivalent to -a -b -c
. Multi-word options such as "--template-engine" are camel-cased, becoming templateEngine
etc.
Options can have their own arguments, that are defined the same way as regular arguments, by default they are treated as string except if the option name contains -no-
like in --no-flag
this will make the option boolean and have a default value of true
and setting the flag will set the value to false
even if using a short version.
const bot = require(bot-commander);
bot
.command('pizza')
.option('-p, --no-pineapple', 'Remove pineapple')
.option('-opt <required>', 'option with required argument')
.option('-opt2 [optional]', 'option with optional argument')
.action((meta, opts) => {
if (opts.pineapple) console.log('You ordered a pizza with pineapple');
});
bot.parse('pizza -p'); //No output
bot.parse('--no-pineapple'); //No output
bot.parse('pizza'); //'You ordered a pizza with pineapple'
Arguments can be separated from the option using spaces or equals.
Options can also have it's own values, and can be parsed with custom functions to fit your needs.
The third parameter of the .option()
method accepts a default value, a regex or a function. In case a function is passed it will be called when a value is parsed and its return value will be stored for the action callback.
function range(val) {
return val.split('..').map(Number);
}
function list(val) {
return val.split(',');
}
function collect(val, memo) {
memo.push(val);
return memo;
}
function increaseVerbosity(v, total) {
return total + 1;
}
bot
.command('test')
.option('-i, --integer <n>', 'An integer argument', parseInt)
.option('-f, --float <n>', 'A float argument', parseFloat)
.option('-r, --range <a>..<b>', 'A range', range)
.option('-l, --list <items>', 'A list', list)
.option('-o, --optional [value]', 'An optional value')
.option('-c, --collect [value]', 'A repeatable value', collect, [])
.option('-v, --verbose', 'A value that can be increased', increaseVerbosity, 0)
.action((meta, opts) => {
console.log(' int: %j', opts.integer);
console.log(' float: %j', opts.float);
console.log(' optional: %j', opts.optional);
opts.range = opts.range || [];
console.log(' range: %j..%j', opts.range[0], opts.range[1]);
console.log(' list: %j', opts.list);
console.log(' collect: %j', opts.collect);
console.log(' verbosity: %j', opts.verbose);
console.log(' args: %j', opts.args);
});
bot.parse('test -i 3 -f 3.2 -r 1..3 -o -c 1 -c 4 -vvv')
bot
.command('pizza')
.option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')
.option('-d --drink [drink]', 'Drink', /^(coke|pepsi|izze)$/i)
.action((meta, opts) => {
console.log(' size: %j', opts.size);
console.log(' drink: %j', opts.drink);
});
const bot = require(bot-commander);
cmd.
let cmd = bot.
.command('pack')
.showHelpOnEmpty();
cmd
.command('install [name]')
.description('install one or more packages');
cmd
.command('search [query]')
.description('search with optional query');
cmd
.command('list')
.description('list packages installed')
.action(a => console.log('list'));
bot.parse('pack list');
//Output will be 'out'
The commands can be configured in a multilevel hierarchy this gives a great flexibility when creating interfaces. If a command does not have an action or a subcommand nothing will be called, except if the command is configured to show help on empty with .showHelpOnEmpty()
in that case the command help will be returned.
When a subcommand is created, a help command is created by default that shows the usage of the command and subcommands. Take into account that the help shown is different for each level as it only shows the usage of the commands in the same level.
const bot = require(bot-commander);
let cmd = bot
.command('pack')
cmd
.command('install [name]')
.description('install one or more packages');
bot.parse('help');
//Will output only help and pack usage
bot.parse('pack help'); //or 'pack -h'
//Will output only help and install usage
The parent command can have an action if desired, it will be called if no subcommand is used, in case you want to require a subcommand you can force it with bot.command('main <subcommand>')
as with any command or if you only want to show the help without an error then use .showHelpOnEmpty()
.
Another feature of the library is the ability to load commands from external files, it is really easy with .load(path)
if the path is a file and exports function it will be called with parser as the only argument so it can add commands or even call load itself.
Take into account that relative paths are resolved from the main script path.
plugin.js
module.exports = bot => {
bot.command('test1')
.action(a => console.log('test1'));
};
main.js
const bot = require(bot-commander);
bot.load('plugin.js');
bot.parse('test1');
//Output will be 'test1'
If the path is a directory it will load every file that could be loaded in that directory. The load function can also be used under a command, in that case the plugins will create subcommands for the main command.
As the library is intended for interactive usage and mainly bots, it will not output anything in the console unless your actions print anything. All communication from the library itself is done via a configurable send function, it will only be used for help and missing arguments. By default the send function is undefined, so be sure to configure it before parsing anything with .setSend(cb)
the callback function receive 2 arguments, metadata and the message to send.
The .send()
function can be used in your actions to output anything and will use the function previously configured using .setSend()
. This function will check that the message is defined before calling the configured function and will return the same that the function returns.
const bot = require(bot-commander);
bot
.setSend((meta, message) => console.log(message))
.command('test')
.action(meta => {
//ret will be null as console.log does not have a return value
let ret = bot.send(meta, 'message');
});
bot.parse('test');
//Will send 'message' through the configured send function.
The send function is shared by the whole hierarchy of commands so it can be set in the main object or at any command or subcommand and it will work for every one.
As you probably have noted, through the whole library there are metadata arguments, this may seem a bit strange at first but the library it is designed to configure a stateless parser and then reuse many times even simultaneously, think of an irc bot receiving commands from several users. The metadata object is passed from the parser call to actions and send function to be able to use all the metadata around the text being parsed this could be useful for time data, from and to information, the channel, etc. The library does not make use of it or manipulate in any way, you can use it as you need.
For example using a fictional irc library with .on()
and .send()
functions this could be a simple irc bot.
const bot = require(bot-commander);
bot
.command('hello')
.action(meta => {
let message = `${meta.from} says hello world to the channel`;
delete meta.from;
bot.send(meta, message);
})
bot
.setSend((meta, message) => {
if (meta.from) {
irc.send(meta.from, message);
} else {
irc.send(meta.channel, message);
}
});
irc.on(event => {
//Event could be something like: {from: 'user1', channel: '#channel', message: ''}
bot.parse(event.message, event);
});
The help information is auto-generated based on the information bot commander already knows about your commands, so the following --help
info is for free.
This is the help output from the general example, it will be sent when parsing help
Usage: [options] [command]
Commands:
help [cmd] display help for [cmd]
copy <file> <dest> Copy file to dest
pizza [options] Order your pizza
sub <command> Subcomand
exit|quit Exit program
Options:
-h, --help output usage information
Every command has it's own command help, using the same example parsing copy -h
, copy --help
or help copy
will render this:
Usage: copy [options] <file> <dest>
Copy file to dest
Options:
-h, --help output usage information
Returns the help information as a string, this can be helpful for some error handling in your actions, or for customizing the help implicit command.
If the first command you define is help
the implicit help command will not be created so your command will be able to send a customized help.
The help command is different for every node in the command hierarchy so you can redefine whatever you want.
const bot = require(bot-commander);
bot
.command('help')
.action(meta => {
let message = bot.help() + '\n Show examples under the main help';
bot.send(meta, message);
});
bot
.command('test')
In the case the input does not contain any valid command, it is not replying with any kind of help, I did miss that condition and fixing it would break implementations. If this behavior is desirable, it is easy to configure the parser to do it.
const bot = require(bot-commander);
bot.
.action( meta => bot.outputHelp(meta)) //Notice this action is at the top level
.command('do_that')
bot.parse('do_this'); //It will reply with the default help
Aside from the option argument in the command constructor (that currently is used only for noHelp), there are several functions that can alter the parsing and error handling for each command or the whole hierarchy.
The shared configuration options are: .setSend()
, .allowUnknownOption()
, lowerCase()
and .showHelpOnError()
.
.setSend()
: has already been discussed on the Output section..allowUnknownOption(boolean)
: Allows to disable errors when an unknown option is present in the command line. Default is to show an error..lowerCase(boolean)
: Configures the parser to check commands in lower case (options and arguments are not changed)..showHelpOnError(boolean)
: Allows to show or not the help when a parsing error is found, the parsing error is always reported but you can opt to not show the full help. Default is true.
The configuration options that only affects one command are this three:
.prefix()
: Allows to configure a prefix (string) or an array of prefixes to search for at the start of parsing if they are not present the line will not be parsed, this is useful for bots listening to group channels where you need to tell the bot what to parse. It can be used on any command but it is mainly used in the main object. Default is null..showHelpOnEmpty(boolean)
: Force to show the help when no command or subcommand is identified in the parsed line. Default is false. *.setParseOptions(object)
: Sets a new object as parse options for the command, this options are{send: null, allowUnknownOption: false, showHelpOnError: true }
this function allows you to configure different options from the full hierarchy and any subcommand created after this call will inherit this options. After setting a new option object you can safely call any of the shared setting functions and will only affect this command and all subcommands created after the call.
const bot = require('bot-commander');
bot
.setSend(function(metadata, text) {
console.log(text);
});
bot
.command('copy <file> <dest>')
.description('Copy file to dest')
.action(function(metadata, file, dest, opts) {
console.log(`Copying file ${file} to ${dest}`);
});
bot
.command('pizza')
.option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')
.option('-d --drink [drink]', 'Drink', /^(coke|pepsi|izze)$/i)
.description('Order your pizza')
.action(function(metadata, opts) {
let order = `You ordered a ${opts.size} pizza`;
if (opts.drink === true) {
order += ' with a random drink';
} else if (opts.drink) {
order += ` with a cup of ${opts.drink}`;
}
console.log(order);
});
bot
.command('nohelp', {noHelp: true})
.description('hidden command')
.action(function(metadata, opts) {
console.log('It works!!');
});
const command = bot
.command('sub <command>')
.description('Subcomand');
command
.command('echo <echo>')
.description('echo command')
.action(function(metadata, echo) {
console.log(echo);
});
command
.command('hello [name]')
.description('hello command')
.action(function(metadata, name) {
if (name) {
console.log(`Hello ${name}`);
} else {
console.log('Hello world!');
}
});
More Demos can be found in the examples directory.
MIT