CLI Simple, Stupid. Automatic discovery of parameters names. 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. Support to sync / async. Support to subcommands down to N levels.
JavaScript
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
test
.gitignore
.jshintrc
.travis.yml
README.md
package-lock.json
package.json

README.md

CLIss

Build Status npm npm

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.