Documentation
Commands

Commands

In this guide, we will be creating a command-line interface for an example application that performs mathematical operations on its arguments.

Basic features

Our application is named calc and performs the following operations:

  • add - adds multiple numbers (defaults to 0 if no number)
  • sub - subtracts two numbers (defaults to NaN if less than two numbers)
  • mult - multiplies multiple numbers (defaults to 1 if no number)
  • div - divides two numbers (defaults to NaN if less than two numbers)

Advanced features

In addition to the basic operations, our application is capable of combining operations in a tail-recursive manner. For example, the following expression could be invoked:

calc add 1 sub 2 mult 3 div 4 2

And would be evaluated as 1 + (2 - (3 * (4 / 2))), which equals -3.

Option definitions

In this section we are going to define the command-line options for our application. The first thing we need is to import the necessary library types:

import type { Options, OptionValues } from 'tsargp';

Reusable definitions

Let's define a reusable option definition for the parameters of a multi-argument operation. It has a single numbers option that receives (unlimited) positional arguments:

const multiOpts = {
  numbers: {
    type: 'numbers',
    preferredName: 'numbers',
    desc: 'The numbers to operate on.',
    default: [],
    positional: true,
    group: 'Arguments',
  },
} as const satisfies Options;

Since this option accepts positional arguments, we do not want to give it a name. However, we give it a preferred name that will be displayed in error messages, as well as a separate group for it to be displayed in the help message. This option is optional and defaults to an empty array.

Now let's define a similar option for the parameters of a dual-argument operation. It has the same definition as above, with additional constraints: it is required and accepts at most two values.

const binaryOpts = {
  numbers: {
    ...multiOpts.numbers,
    limit: 2,
    required: true,
    default: undefined, // override this setting
  },
} as const satisfies Options;

Operation definitions

With this in place, we can define each one of the basic operations, as follows:

const addOpts = {
  add: {
    type: 'command',
    names: ['add'],
    desc: 'A command that adds multiple numbers.',
    options: (): Options => ({ ...multiOpts, ...mainOpts }),
    exec({ param }): number {
      const vals = param as OptionValues<typeof multiOpts & typeof mainOpts>;
      const other = vals.add ?? vals.sub ?? vals.mult ?? vals.div ?? 0;
      return vals.numbers.reduce((acc, val) => acc + val, other);
    },
  },
} as const satisfies Options;

Notice how we use an options callback to provide the option definitions for each command. This is necessary because JavaScript does not allow us to reference the containing object from one of its members, before its initialization. (The mainOpts variable will be defined later, and the above definitions will be embedded in it.)

Inside the command callback, we perform a typecast to access the values parsed for the command. This is necessary because the library does not know the concrete type of our option values when it calls the callback (the callback type cannot be defined in terms of a generic type parameter). So it must pass an opaque reference to them.

From those values, we select the value resulting from the next operation (if any), falling back to a default if this was the last operation. We then return the accumulated result of the operations performed so far in the stack (except if, in the case of sub and mult, two arguments are provided before a recursive call, then the result of that call is ignored).

In TypeScript, it's necessary to specify the return type of both the options and exec callback attributes. Otherwise, the compiler will not be able to resolve the type of the containing object.

Main definitions

Finally, we can define our main command-line options:

const mainOpts = {
  help: {
    type: 'help',
    names: ['help'],
    desc: 'Prints this help message.',
  },
  ...addOpts,
  ...subOpts,
  ...multOpts,
  ...divOpts,
} as const satisfies Options;

Trying it out

List of commands to try:

  • calc help - should print the main help message
  • calc <op> help - should print the help message of the <op> command
  • calc - should print NaN
  • calc add - should print 0
  • calc mult - should print 1
  • calc sub or calc div - should print an error message
  • calc sub 1 or calc div 1 - should print NaN
  • calc sub 1 2 3 or calc div 1 2 3 - should print an error message
  • calc add 1 sub 2 mult 3 div 4 2 - this is the aforementioned example that gives -3