cli: options vs. commands

2020-08-04

 | 

~6 min read

 | 

1061 words

I’ve building a CLI to help me manage my notes and I’ve been using commander to help. I’ve heard great things about commander, and while the documentation is thorough, for a first time reader like myself, the distinction between an option and a command was not immediately evident. Particularly because it seemed like there was some overlap and both were applied to the main application.

So, I spent the morning doing some exploration. Here are some of my notes.

Setting Options

One of the very first examples provided in the documentation is that of attaching some options to a new program:

commander-options.js
const { program } = require("commander")

program
    .option("-d, --debug", "output extra debugging")
    .option("-s, --small", "small pizza size")
    .option("-p, --pizza-type <type>", "flavour of pizza")

program.parse(process.argv)

if (program.debug) console.log(program.opts())
console.log("pizza details:")
if (program.small) console.log("- small pizza size")
if (program.pizzaType) console.log(`- ${program.pizzaType}`)

Here, we set a new program and start applying some options, --debug, --small, --pizza-type (which takes a required type). Then, after parsing the arguments in the process, the program will determine how to handle each present option. In this case, the handling is printing out different results to the console for each present option.

Assuming our application is called pizza, some of the different ways to invoke these options are:

$ pizza --small
$ pizza --pizza-type mushroom
$ pizza -sp mushroom

These would print:

- small pizza size
- mushroom
- small pizza size
- mushroom

The first two lines are for each of the first commands respectively. The last two lines is for the third execution alone.

Options then allow us to pass along information in the form of flags (which sometimes take or require a parameter) to the program.

Defining Commands

Commands are different from options in that they’re the name of an action to take rather than additional information to be used in taking that action. Also from the docs:

commander-command.js
// Command implemented using action handler (description is supplied separately to `.command`)
// Returns new command for configuring.
program
    .command("clone <source> [destination]")
    .description("clone a repository into a newly created directory")
    .action((source, destination) => {
        console.log("clone command called")
    })

// Command implemented using stand-alone executable file (description is second parameter to `.command`)
// Returns `this` for adding more commands.
program
    .command("start <service>", "start named service")
    .command(
        "stop [service]",
        "stop named service, or all if no name supplied",
    )

// Command prepared separately.
// Returns `this` for adding more commands.
program.addCommand(build.makeBuildCommand())

Here, the commands are also attached directly to the root program. But the action that’s taken is described before parsing the arguments (this is more clear looking at an example).

Commands With Their Own Options

So far, we’ve seen options at the program level and commands too. Commander, however, allows sub-commands and options.

For example:

command-with-option.js
program
    .command("serve", { isDefault: true })
    .description("launch web server")
    .option("-p,--port <port_number>", "web port")
    .action((opts) => {
        console.log(`server on port ${opts.port}`)
    })

Here, the command is able to access the option, even though the argv has not yet been parsed, as they are passed into the action as the first argument.

Wrapping Up

So, how do we pull all of this together into a mental model that can be easily remembered? One way would be to look at something familiar and see how it’s constructed! For me, that’s git. While no expert, I’m a huge fan and love learning about git. It also fits this pattern of options and commands quite well. Commands include push, pull, add, and merge while options are what you see when you type any command with a --help, for example, for git push the options are:

git-push
GIT-PUSH(1)                                                                                Git Manual                                                                               GIT-PUSH(1)

NAME
       git-push - Update remote refs along with associated objects

SYNOPSIS
       git push [--all | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
                  [--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-v | --verbose]
                  [-u | --set-upstream] [-o <string> | --push-option=<string>]
                  [--[no-]signed|--signed=(true|false|if-asked)]
                  [--force-with-lease[=<refname>[:<expect>]]]
                  [--no-verify] [<repository> [<refspec>...]]

Meanwhile, git itself, has both options and commands:

git/
git --help
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
   clone     Clone a repository into a new directory
   init      Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
   add       Add file contents to the index
   mv        Move or rename a file, a directory, or a symlink
   restore   Restore working tree files
   rm        Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)
   bisect    Use binary search to find the commit that introduced a bug
   diff      Show changes between commits, commit and working tree, etc
   grep      Print lines matching a pattern
   log       Show commit logs
   show      Show various types of objects
   status    Show the working tree status

grow, mark and tweak your common history
   branch    List, create, or delete branches
   commit    Record changes to the repository
   merge     Join two or more development histories together
   rebase    Reapply commits on top of another base tip
   reset     Reset current HEAD to the specified state
   switch    Switch branches
   tag       Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.

The options are everything listed at the top (with the exception of the [args] line):

git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]

Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!