CLIss
CLI Simple, Stupid. Automatic discovery of parameters names and support to subcommands down to N levels. Provides an easy and minimal setup by passing in only a function reference without the need of declaring all expected options names or create a help section by hand.
Side note: It is worth taking a look at MagiCLI, which is a module capable to create a CLI interface automatically for a module, instead of creating one by hand.
Goals
- Simple and easy API
- Easy minimal setup, extracting options names from functions parameters
- Out of the box support to sync or async (Promise) functions
- Subcommands down to N levels
- Automatic Help section generation, that can be improved only when needed
Installation
$ npm install cliss
Usage
Through this section we'll be going from the most minimal usage of the module, where options names are extracted from functions parameters:
const func = (param1, param2) => `${param1}_${param2}`;
cliss(func);
to a version using all the possible options it provides:
const cliSpec = {
name,
description,
version,
options: [{
name,
description,
required,
type
}],
pipe: {
stdin: (stdinValue, args, positionalArgs, argsAfterEndOfOptions) => {},
before: (args, positionalArgs, argsAfterEndOfOptions) => {},
after: (result, parsedArgs, positionalArgs, argsAfterEndOfOptions) => {}
},
action: () => {},
commands: [{}]
};
const clissOptions = {
command: {
subcommandsDelimiter
},
options: {
validateRequiredParameters
},
version: {
option
},
help: {
option,
stripAnsi
},
pipe: {
stdin: (stdinValue, args, positionalArgs, argsAfterEndOfOptions) => {},
before: (args, positionalArgs, argsAfterEndOfOptions) => {},
after: (result, parsedArgs, positionalArgs, argsAfterEndOfOptions) => {}
}
};
cliss(cliSpec, clissOptions);
A CLI for a function (the most simple and minimal use case)
cliss(functionReference)
Creating a CLI for a function by doing nothing more than passing it as a parameter to cliss. The options names will be the same as the parameters expected by the function.
'use strict';
const cliss = require('cliss');
const aFunctionWithWeirdParametersDefinition = (param1, param2, { someProp: [[ param3 ]] = [[]] } = {}, ...args) => {
let result = `param1: ${param1} \n`;
result += `param2: ${param2} \n`;
result += `param3: ${param3} \n`;
result += `args: ${args.join(',')}`;
return result;Run the program passing with the following options:
};
cliss(aFunctionWithWeirdParametersDefinition);
Calling it via CLI with --help
will give you:
Options:
--param1
--param2
--param3
--args
Passing in the options:
node cli.js --param2=PARAM2 --param1=PARAM1 --param3=PARAM3 --args=a --args=r --args=g --args=s
Or passing options + arguments (arguments for the "...args" parameter in this case):
node cli.js --param2=PARAM2 --param1=PARAM1 --param3=PARAM3 a r g s
Will result in:
param1: PARAM1
param2: PARAM2
param3: PARAM3
args: a,r,g,s
Note that the order of the options doesn't need to match the order of the parameters.
Improving the help section
cliss(cliSpec)
Great, but probably one would like to improve a bit the --help
section of the module, by providing to the end user the name (the command's name for calling it via CLI), description and version of the module. In this case a Object Literal will be used instead of just a function reference.
'use strict';
const cliss = require('../');
cliss({
name: 'some-command',
description: 'Just an example that will do nothing but concat all the parameters.',
version: '1.0.0',
action: (param1, param2, { someProp: [[ param3 ]] = [[]] } = {}, ...args) => {
let result = `param1: ${param1} \n`;
result += `param2: ${param2} \n`;
result += `param3: ${param3} \n`;
result += `args: ${args.join(',')}`;
return result;
}
});
Now, when calling it with --help
, a better help section will be shown:
Description:
Just an example that will do nothing but concat all the parameters.
Usage:
$ some-command [options] [args...]
Options:
--param1
--param2
--param3
--args
Providing more information about the expected options
cliss(cliSpec)
The options were effortlessly extracted from the parameters names, but cliss provides a way for one to provide more information about each of them. The Object Literal passed in the cliSpec parameter can have a property named options, which expects an Array of objects, containing the name of the option plus some of the following properties:
-
required
To tell if the parameter is required.
-
description
To give hints or explain what the option is about.
-
type
To define how the parser should treat the option (Array, Object, String, Number, etc.). Check yargs-parser for instructions about type, as it is the engine being used to parse the options.
-
alias
To define an alias for the option.
Following the last example, let's improve it to:
- give more information about param1
- check args as required
cliss({
name: 'some-command',
description: 'Just an example that will do nothing but concat all the parameters.',
version: '1.0.0',
options: [{
name: 'param1',
description: 'This param is the base value to compute everything else.',
required: true,
type: 'String'
}, {
name: 'args',
required: true
}],
action: (param1, param2, { someProp: [[ param3 ]] = [[]] } = {}, ...args) => {
let result = `param1: ${param1} \n`;
result += `param2: ${param2} \n`;
result += `param3: ${param3} \n`;
result += `args: ${args.join(',')}`;
return result;
}
});
Call --help
, and note that the Usage section will also be affected. Now [options] [args...] will be shown as <args...>, because both of them are required.
Description:
Just an example that will do nothing but logging all the parameters.
Usage:
$ some-command <options> <args...>
Options:
--param1 String Required - This param is the base value to compute
everything else.
--param2
--param3
--args Required
Run the program with the following options:
node cli.js --param1=001 --param2=002 --param3=PARAM3 a r g s
And check the result to see how param1 was indeed treated as a string, while param2 was parsed as a number:
param1: 001
param2: 2
param3: PARAM3
args: a,r,g,s
Pipe: STDIN, Before and After
cliss(cliSpec)
A property named pipe can also be defined on cliSpec in order to handle stdin and also, some steps of the execution flow (before and after). To define a single handle for all the commands, the pipe option can be defined on Cliss options as will be shown later on the documentation. The pipeline execution of a command is:
stdin (command.pipe.stdin || clissOptions.pipe.stdin) =>
clissOptions.pipe.before =>
command.pipe.before =>
command.action =>
command.pipe.after =>
clissOptions.pipe.after =>
stdout
Where each of these steps can be handled if needed.
The properties expected by pipe are:
-
stdin
(stdinValue, args, positionalArgs, argsAfterEndOfOptions)
-
before
(args, positionalArgs, argsAfterEndOfOptions)
To transform the data being input, before it is passed in to the main command action.
-
after
(result, parsedArgs, positionalArgs, argsAfterEndOfOptions)
To transform the output (for example, to JSON.stringify an Object Literal)
Note: stdin and before must always return args, and after must always return result, as these values will be passed in for the next function in the pipeline.
To better explain with an example, let's modify the previous one to:
- get param3 from stdin
- use before to reverse ...args array
- use after to decorate the output
Check the pipe property on the following code:
cliss({
name: 'some-command',
description: 'Just an example that will do nothing but concat all the parameters.',
version: '1.0.0',
options: [{
name: 'param1',
description: 'This param is needed to compute everything else.',
required: true,
type: 'String'
}, {
name: 'args',
required: true
}],
pipe: {
stdin: (stdinValue, args, positionalArgs, argsAfterEndOfOptions) => {
args.param3 = stdinValue;
return args;
},
before: (args, positionalArgs, argsAfterEndOfOptions) => {
positionalArgs.reverse();
return args;
},
after: (result, parsedArgs, positionalArgs, argsAfterEndOfOptions) => {
return `======\n${result}\n======`;
}
},
action: (param1, param2, { someProp: [[ param3 ]] = [[]] } = {}, ...args) => {
let result = `param1: ${param1} \n`;
result += `param2: ${param2} \n`;
result += `param3: ${param3} \n`;
result += `args: ${args.join(',')}`;
return result;
}
});
Calling it as:
echo "fromSTDIN" | node cli.js --param1=001 --param2=002 a r g s
Will result in:
=======
param1: 001
param2: 2
param3: fromSTDIN
args: s,g,r,a
=======
Subcommands
Subcommands can be defined in a very simple way. Thinking naturally, a subcommand should be just a command that comes nested into another one, and it is exactly how it's done.
Here one more property of the cliSpec is introduced: commands. It is an Array that can contains N commands, including the commands property (commands can be nested down to N levels).
As each subcommand is a command itself, they also counts with its own --help
section, and possibly its own --version
(if it is not defined for a subcommand, the one defined for the root will be shown).
The following example will introduce:
- 1 subcommand, thas has no action, and contains more 2 subcommands
- 1 subcommand that contains an action
cliss({
name: 'some-command',
description: 'Just an example that will do nothing but concat all the parameters.',
version: '1.0.0',
options: [{
name: 'param1',
description: 'This param is the base value to compute everything else.',
required: true,
type: 'String'
}, {
name: 'args',
required: true
}],
pipe: {
stdin: (stdinValue, args, positionalArgs, argsAfterEndOfOptions) => {
args.param3 = stdinValue;
return args;
},
before: (args, positionalArgs, argsAfterEndOfOptions) => {
positionalArgs.reverse();
return args;
},
after: (result, parsedArgs, positionalArgs, argsAfterEndOfOptions) => {
return `======\n${result}\n======`;
}
},
action: (param1, param2, { someProp: [[ param3 ]] = [[]] } = {}, ...args) => {
let result = `param1: ${param1} \n`;
result += `param2: ${param2} \n`;
result += `param3: ${param3} \n`;
result += `args: ${args.join(',')}`;
return result;
},
commands: [{
name: 'subcommand1',
commands: [{
name: 'action1',
options: [{
name: 'param',
required: true
}],
action: param => `subcommand1 action1 param: ${param}`
}, {
name: 'action2',
action: () => 'subcommand1 action2'
}]
}, {
name: 'subcommand2',
action: () => console.log('subcommand2')
}]
});
Call --help
to see that a new section Commands: is presented:
Description:
Just an example that will do nothing but concat all the parameters.
Usage:
$ some-command <options> <args...>
$ some-command [command]
Options:
--param1 String Required - This param is needed to compute
everything else.
--param2
--param3
--args Required
Commands:
subcommand1
subcommand2
Each subcomand has its own help section, check:
node cli.js subcommand1 --help
Usage:
$ some-command subcommand1
node cli.js subcommand2 --help
:
Usage:
$ some-command subcommand2 <command>
Commands:
action1
action2
Just call the commands names separated by space:
node cli.js subcommand2 action1 --param=VALUE
Result:
subcommand2 action1 param: VALUE
Cliss options
cliss(cliSpec, clissOptions)
An Object Literal
with the following options can be passed in as the second parameter to cliss:
-
command
- subcommandsDelimiter
To define a delimiter for a subcommand to be used instead of a white space. For example, if
'-'
is passed in, the subcommands should be called as subcommand1-action1
instead of subcommand1 action1
.
-
options
- validateRequiredParameters
If set to
true
, the required parameters will be checked before the command action is called, and the help section will be shown in case a required parameter is missing.
-
help
-
option
To define a different option name to show the help section. For example, if 'helpsection'
is passed in, --helpsection
must be used instead of --help
.
-
stripAnsi
Set to true
to strip all ansi escape codes (colors, underline, etc.) and output just a raw text.
-
version
- option
To define a different option name to show the version. For example, if
'moduleversion'
is passed in, --moduleversion
must be used instead of --version
.
-
pipe
As it is defined on cliSpec for each command, pipe can also be defined in clissOptions to implement for all commands a unique way to handle stdin and also, some steps of the execution flow (before and after) in case it is needed. The pipeline execution of a command is:
stdin (command.pipe.stdin || clissOptions.pipe.stdin) =>
=> clissOptions.pipe.before =>
=> command.pipe.before =>
=> command.action =>
=> command.pipe.after =>
=> clissOptions.pipe.after
=> stdout
The properties expected by pipe are:
-
stdin
(stdinValue, args, positionalArgs, argsAfterEndOfOptions)
-
before
(args, positionalArgs, argsAfterEndOfOptions)
To transform the data being input, before it is passed in to the main command action.
-
after
(result, parsedArgs, positionalArgs, argsAfterEndOfOptions)
To transform the output (for example, to JSON.stringify an Object Literal)
Note: stdin and before must always return args, and after must always return result, as these values will be passed in for the next function in the pipeline.