CommOp

Solution for complex cli interfaces.
Installation
$ npm install -g commop
Writing another module
This is not an option parser module. If you writing small script, which handles one simple task,
please use another module, there is many option parsers: commander, dashdash,
minimist or yargs.
For some of my tasks I had to write complicated cli interface. With global options,
options per command, command sets, localized messages, environment as fallback/override and so on.
So, I decided to write helper module which can use any option parser module for parsing argv and then
build a cli I want.
Example
{
"options": {
"verbose": {"type": "boolean", "global": true, "alias": "v"},
"arduino": {"type": "string", "global": true, "alias": "A"},
"include": {"type": "string", "alias": "I"},
"define": {"type": "string", "alias": "D"},
"port": {"type": "string", "alias": "p"},
"baudrate": {"type": "number", "alias": "r"},
},
"commands": {
"compile": {"options": ["inc", "define"], "run": "compile"},
"upload": {"options": ["port", "baudrate"], "run": "upload"},
"platform": {
"sub": {
"add": {"run": "addPlatform"},
"remove": {"run": "removePlatform"}
}
}
}
}
var commop = require ('commop');
var config = require ('./commop.json');
var launcher = new commop (config);
function compile (cmd, next) {
if (cmd.options.inc) {
}
}
module.exports.compile = compile;
launcher.start ();
I had to create cli for arduino supporting commands like compile
, upload
and platform
.
Each task should have verbose
and arduino
options.
And compile
and upload
commands should have sketch
and board
options.
Afterall compile
must be configurable via -D
, includes via -I
options.
Upload must support port
and baudrate
options.
platform
is actually set of commands,
like platform add <>
and platform remove <>
.
Option parsing
I've tried a lot of option parsing modules, and found out that it is matter of preference which module to use.
For my own tasks (almost) any option parser can handle job. But some needs advanced validation,
in some cases no requirements is a must, and some loves pirates.
commop
uses bundled minimist
module and supports yargs
and dashdash
interfaces.
Interface
There is no fancy interface to add options. Module takes configuration and
launches tasks tied to that configuration.
var commop = require ('commop');
var config = require ('./commop.json');
var launcher = new commop (cmdConfig);
var cmd;
cmd = launcher.findCommand ();
cmd = launcher.findCommand (process.argv);
cmd = launcher.findCommand (process.argv);
launcher.start ();
launcher.start (process.argv, require.main, function (cmd, data) {
});
Features
Command handler(s)
Using list of tasks with run
key
Each command must have a handler, described by run
key in command configuration.
If handler is an array, then tasks launched one after one. Task laso can be a promise or function,
which return a promise.
function task (cmd, data, next) {
}
Command objects usually contains config
key with associated configuration structure,
branch
key with list of parsed command names, positional
with positional parameters,
options
— list of applicable options, also failedOptions
and errors
Data should be modified and returned via next
callback or promise's resolve.
Next task will be launched regardless of return status of previous task.
If your task rely on data from previous command, assert data section.
Those tasks can be object methods, you just have to provide an origin as parameter to the start
call.
If origin is not provided, require.main
is used instead (it is your main module exports).
var theProgram = new Program ();
launcher.start (null, theProgram, function (cmd, data) {
});
Using external script with script
key
If you defined script
key for command, this script will run. You can pass
options as environment variables. Script's stdout
and stderr
you'll get via
data.scriptStderr
and data.scriptStdout
. If command configuration contains
anyway
key, then script error will be written to the data.scriptError
and
callback will be called.
var testConfig2 = {
options: {
BBB: {type: "string"},
DDD: {type: "string"}
},
commands: {
node: {
script: nodePath + " -e 'console.log (process.argv)' AAA=${BBB} CCC=${DDD}",
options: ["BBB", "DDD"]
}
}
};
var co = new CommOp (testConfig2);
co.start (null, null, function (cmd, data) {
console.log (data.scriptStderr, data.scriptStdout)
});
Also, if you need to launch different scripts on different OS'es,
use next format:
script: {
win32: nodePath + " -e 'console.log (process.argv)' AAA=%BBB% CCC=%DDD%",
default: nodePath + " -e 'console.log (process.argv)' AAA=${BBB} CCC=${DDD}"
}
Usage/help localization
Each message is localizable,for more information please refer test/04-l10n.js
Option sharing
Option can be global or shared between specified commands.
Implied, conflicting and required options
Examples at test/02-options.js
Usage generator
If your commands and options have properly defined descriptions,
commop
will generate usage automatically. By default, if there is no command specified,
usage will be displayed automatically. But you can override this behaviour.
launcher.showUsage = false;
launcher.start ([], null, function (cmd, data) {
if ("usage" in cmd) {
if (cmd.branch[0] === 'xxx') {
launcher.helpForCommand (["xxx"]);
}
}
});
var usageString = launcher.usage();
Help generator for command
Command help displayed automatically if you added a help
keyword before actual command.
Also, you can provide your own help handler by setting run
key for a help
command in config.
launcher.start (["help", "cmd"], null, function (cmd, data) {
if ("usage" in cmd) {
}
});
var helpString = launcher.helpForCommand (["cmd"]);
Configuration
Configuration is a javascript object. Options and commands configurations under
options
and commands
keys.
Configuration for option
"options": {
"verbose": {
"type": "boolean",
"global": true,
"alias": "v",
"description": "be verbose",
"default": false,
"env": "VERBOSE"
},
"arduino": {"type": "string", "global": true, "alias": "A"},
"include": {"type": "string", "alias": "I"},
"define": {"type": "string", "alias": "D"},
"port": {"type": "string", "alias": "p"},
"baudrate": {"type": "number", "alias": "r"},
}
Configuration keys:
-
type
handling depends on option parsing module
-
global
options available for all commands
-
Single alias
or list of aliases
-
description
needed for usage/help generation
-
default
value
-
env
keys to use for that option
Configuration for command
"commands": {
"compile": {"options": ["inc", "define"], "run": "compile"},
"upload": {"options": ["port", "baudrate"], "run": "upload"},
"platform": {
"sub": {
"add": {"run": "addPlatform"},
"remove": {"run": "removePlatform"}
}
}
}
Configuration keys:
-
run
task or task set to run command
-
sub
subcommands
-
description
needed for usage/help generation
-
options
is applicable options (implies
is WIP):
options: ["port", "baudrate"]
options: {
"port": {"required": true, "conflicts": "board"},
"board": null,
"baudrate": {"implies": "port"}
}
Note
Implied/conflicting options works quite badly with booleans.
Using boolean options you will allways get value, either
true or false even if that option is not present.
Additional flags
-
envMode
can be override
or fallback
. In fallback mode every missing option
will be filled from environment variable. In override mode environment values
takes over precedence over argv options.
-
ignoreUnknownCommands
allows to ignore unknown commands
See also
https://github.com/freeformsystems/cli-command complex, many deps, don't have option relationship, events, +sections, +pod
License
MIT