Commander – tsmx https://tsmx.net pragmatic IT Sun, 30 Apr 2023 19:25:41 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 https://tsmx.net/wp-content/uploads/2020/09/cropped-tsmx-klein_transparent-2-32x32.png Commander – tsmx https://tsmx.net 32 32 Commander options hands-on – advanced CLI’s with NodeJS https://tsmx.net/commander-options/ Sat, 11 Sep 2021 21:32:52 +0000 https://tsmx.net/?p=773 Read more]]> The Commander package is a great utility for building a CLI with NodeJS. This article provides a comprehensive hands-on for the various option features of Commander.

When building a “good-old” command line interface (CLI) with NodeJS, the Commander package is of first choice. Commander offers you many features to design your CLI. The most comprehensive one is using options for a program or command. In this hands-on we’ll focus on this options and how to use them in your code.

Note: This article and the commander-options GitHub test project were created using Commander 10.0.1

Introduction

What are options?

First, let’s explore a bit the Commander terminology to get familiar with all the terms used in the packages documentation. In general you have a program, commands and options.

To break this down, let’s have a look at a practical example with the following well-known OpenSSL command for creating a self signed certificate.

$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365

This command line has the following components.

TermTypeValue
opensslprogram
reqcommand
-x509optiontrue
-newkeyoptionrsa:4096
-keyoutoptionkey.pem
-outoptioncert.pem
-daysoption365

Understanding this terminology, let’s go ahead with a short crash course on how to create a program and commands with Commander and add options to them.

Defining and consuming options with Commander

First, a program is created by simply requiring-in the Commander package. To that program, you can add sub-commands by calling .command(...). To both, the program itself and all commands, options can be added. Most commonly this is done by calling the.option(...)method. The calls to .option(...) can be chained as they all return a reference to the underlying porgram/command object.

The values passed to the various options by the user in the command line are rertieved by calling program.opts()or using the options argument that is passed to a command’s action handler.

const { program } = require('commander');

// create a sub-command with options passed to the action handler

const doSomething = (options) => {
  // ... use options passed to the command ...
}

program
  .command('something')
  .option(...)
  .option(...)
  .action(doSomething);

// create options for the program itself and use them
program
  .option(...)
  .option(...);

program.parse();

const options = program.opts();
// ... use options passed to the program ...

For more details on all program and command features, please refer to the comprehensive Commander documentation.

For the sake of simplicity, all following examples will be using the program instance directly. Keep in mind, that all this is also perfectly fine for any command. Let’s go for it…

Understanding the .option function signatures

Before diving into the various option features of Commander, let’s have a quick look on the .option(...) function for declaring options. This function has the following signatures…

.option(optionDefinition, optionHelp, optionDefault)

.option(optionDefinition, optionHelp, optionProcessor, optionDefault)

The arguments here are:

ArgumentDescriptionExample
optionDefinitiondeclaration of the option including
– short name starting with a single dash
– long name starting with double dashes
– an optional value placeholder
'-t, --test <value>'
optionHelphelp text displayed for the option'a test value'
optionDefault[optional] the default value for the option'myValue'
optionProcessor[optional] a function used for custom processing of the options value(value, previous) => { return value + previous; }

Hands-on examples structure

In this hands-on we will use the following 3-parts structure of code snippets for every example of the various Commander options:

  1. A NodeJS snippet demonstrating how to setup an option in your CLI’s NodeJS code
  2. How to call the program invoking this option in the bash/command-line assuming app.js is the main file
  3. The JSON object returned for the invocation (either by calling program.opts() or directly passed to a command handler)

Like this…

program
  .option('-m, --my-option <value>', 'my test option');
$ node app.js -m test
{
  myOption: 'test'
}

Commander option types

Standard options

The standard option type is receiving a simple value that is then passed on to your code. For this, a value placeholder in angle brackets <> is placed right after the option’s name.

program
  .option('-t, --test <value>', 'option with an explicitly passed value');

Note that the name of the value (here ‘value’) doesn’t matter. The option’s value is passed – like for any option type – as an property named after the long option name followed by the double dashes (here ‘test’).

$ node app.js -t myValue
{
  test: 'myValue'
}

If a standard option is omitted when calling the program or command, the resulting options object doesn’t contain the option at all.

$ node app.js
{}

To change that behaviour, you can add a default value that is used if no other value was passed for the option.

program
  .option('-t, --test <value>', 'option with an explicitly passed value', 'defaultValue');
$ node app.js
{
  test: 'defaultValue'
}

Specifying a default value may be helpful if you want to rely on that the option is always present in the passed options object.

Boolean options

For simple boolean options, you can omit the <value> placeholder when defining the option. When calling the program with this option, its value in the passed options object is set to true.

program
  .option('-b, --bool', 'boolean option');
$ node app.js -b
{
  bool: true
}

Please note that as for any other option, it will be omitted in the passed options object if it wasn’t present in the command line and didn’t have any standard value. Calling the above example without passing -b will result into an empty options object {}.

So if you want a boolean option to be true or false even if it’s not put in by the user at the command line, you must specify a default value.

program
  .option('-b, --bool', 'boolean option', false);
$ node app.js
{
  bool: false
}

Boolean-or-value options

For more flexibility, you can add an optional non-boolean argument in square brackets to a boolean option. If the argument is passed at the command line, the resulting options objects property will have that value, otherwise it’ll be true.

program
  .option('-bv, --boolvariant [value]', 'bool option with optional non-bool value');

Calling the program with the optional argument will pass it to the options object.

$ node app.js -bv test
{
  boolvariant: 'test'
}

Calling the program without the optional argument will pass true to the options object.

$ node app.js -bv
{
  boolvariant: true
}

Also here you can pass a default value for the option if you want to ensure it is always present in the options object.

program
  .option('-bv, --boolvariant [value]', 'bool option with optional non-bool value', false);
$ node app.js
{
  boolvariant: false
}

Variadic options

In contrast to standard options taking one value, variadic options can receive 1..n values separated by a whitespace. Variadic options are specifyied by adding ... to the value placeholder. The options JSON object passed back will contain an array holding all the values.

program
  .option('-v, --variadic <value...>', 'variadic option taking 1..n values');
$ node app.js -v v1 v2 v3
{
  variadic: [
    'v1',
    'v2',
    'v3'
  ]
}

Negated “–no” options

Commander has a nice little feature for options having a long name starting with --no-. This will automatically set the option property to false if it is passed by the user.

program
  .option('-no, --no-option', 'option with negation (name starting with \'--no-\')');
$ node app.js -no
{
  option: false
}

Option names containing dashes

For a better readability and user experience it is quite common to have the long name of an option splitted by dashes, like --human-readable of the ls command (which is by far better then --humanreadable would be).

Question here is: how are those options passed in the resulting options object? Using dashes in a JSON object’s keys is not forbidden, BUT it would force you to use the much more complicated bracket notation instead of the usual dot notation to access those keys.

To avoid that, Commander will remove all dashes from the options long name and convert all characters following a dash to uppercase. So the result is a camel-cased property name which can easily be accessed with the usual dot notation.

program
  .option('-s, --split-name-option <value>', 'option with a name splitted by \'-\'');
$ node app.js -s test
{
  splitNameOption: 'test'
}

Required options

All types seen so far are optional, meaning you can omit them when running your program. To set an option as required, you can use the .requiredOption function. Its behaviour is like .option except that Commander will throw an error and terminate the program if the option is not specified at the command line.

program
  .requiredOption('-r, --required <value>', 'required option');
$ node app.js
error: required option '-r, --required <value>' not specified

Custom processed options

Remembering the signature of the .option function, it is possible to define a custom processing method for an option. This custom processing will step in after Commander has parsed all command line arguments and before the resulting options JSON object is passed back to your code.

A custom processing function receives two parameters: the current value to be processed and the previous value. The previous value is either…

  • the passed default value for the first processing of the option, or
  • the result of the last processing for recurring processings of the option.
const concatOption = (value, previous) => {
  return previous + value;
}

program
  .option('-c, --concat <item>', 'concatenation of all occurences', concatOption, 'a');
$ node app.js -c yy -c zz
{
  concat: 'ayyzz'
}

In our example we have the option -c occurring twice and a default value of 'a'. Therefore, the parameters of our custom processing function will look like this:

  1. First occurence: value = 'yy', previous = 'a'
  2. Second occurence: value = 'zz', previous = 'ayy'

Of course you can also declare custom processing function that ignore the previous argument and just do something with the current value.

const listOption = (value, _previous) => {
  return value.split(',');
}

program
  .option('-l, --list <items>', 'list from a comma separated string', listOption);
$ node app.js -l a,b,c
{
  list: [
    'a',
    'b',
    'c'
  ]
}

Just keep in mind, that if an option occures multiple times and previous is ignored, only the last one will be passed back to your code.

$ node app.js -l a,b,c -l 1,2,3,4
{
  list: [
    '1',
    '2',
    '3',
    '4'
  ]
}

Trying it all out

Example program for Commander’s option features

After exploring the option features of Commander, it’s time to try it out. To do so, let’s use the following small NodeJS example program to play around with all the features and see what’s passed back to your code. It is also available in the commander-option GitHub repo.

#!/usr/bin/env node

const { program } = require('commander');

const listOption = (value, _previous) => {
  return value.split(',');
}

const concatOption = (value, previous) => {
  return previous + value;
}

const logProgramOptions = (prog) => {
  const options = prog.opts();
  console.log('program options:');
  if (options) {
    console.log(JSON.stringify(options, null, 2));
  }
  else {
    console.log('no options');
  }
}

program
  .command('test')
  .option('-x, --x-option', 'command test option')
  .action((options) => {
    console.log('test command executed');
    console.log(JSON.stringify(options, null, 2));
    logProgramOptions(program);
  });

program
  .option('-t, --test <value>', 'option with an explicitly passed value')
  .option('-d, --default <value>', 'option with a default value', 'standardValue')
  .option('-b, --bool', 'boolean option')
  .option('-v, --variadic <value...>', 'variadic option taking 1..n values')
  .option('-bd, --booldefault', 'boolean option with a default value', false)
  .option('-bv, --boolvariant [value]', 'boolean option with optional non-boolean value', false)
  .option('-o, --option', 'option for testing interaction with --no-option')
  .option('-no, --no-option', 'option with negation (name starting with \'--no-\')')
  .option('-s, --split-name-option <value>', 'option with a name splitted by \'-\'')
  .option('-l, --list <items>', 'list from a comma separated string', listOption)
  .option('-c, --concat <item>', 'concatenation of all occurences', concatOption, '')
  .requiredOption('-r, --required <value>', 'required option')
  .action(() => {
    console.log('program executed');
    logProgramOptions(program);
  });

program.parse();

Sidenote: NodeJS shebang

For ease of use, the shebang #!/usr/bin/env node is added on top of the main file. This allows you to execute NodeJS programs directly at the command line after setting execution rights using chmod, e.g. chmod ug+x app.js.

Then instead of calling…

$ node app.js -o ...

…you can directly execute the program…

$ ./app.js -o ...

A very good in-depth description of the NodeJS shebang can be found here.

Happy coding 🙂

Useful links

]]>