Subcommands
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 to0
if no number)sub
- subtracts two numbers (defaults toNaN
if less than two numbers)mult
- multiplies multiple numbers (defaults to1
if no number)div
- divides two numbers (defaults toNaN
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 an array option that receives (unlimited) positional arguments:
const multiOpts = {
numbers: {
type: 'array',
preferredName: 'numbers',
synopsis: 'The numbers to operate on.',
default: [],
parse: Number,
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 set 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. We parse the parameters as numbers.
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:
add
const addOpts = {
add: {
type: 'command',
names: ['add'],
synopsis: 'A command that adds multiple numbers.',
options: (): Options => ({ ...multiOpts, ...mainOpts }),
parse(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 a nested options callback to provide the option definitions for each subcommand. 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 parsing callback, we perform a typecast to access the values parsed for the subcommand. This is necessary because the library does not know the concrete type of our option values when it calls the callback (and the callback type cannot be defined in terms of a generic type parameter), so it must pass an opaque reference to them.
From the values received in the callback, we select the one 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, except if, in the case of sub
and mult
, two arguments are provided before another recursive call (then the result of that call is ignored).
In this case, it is necessary to specify the return type of both callbacks. 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'],
synopsis: '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 messagecalc <op> help
- should print the help message of the<op>
commandcalc
- should printNaN
calc add
- should print0
calc mult
- should print1
calc sub
orcalc div
- should print an error messagecalc sub 1
orcalc div 1
- should printNaN
calc sub 1 2 3
orcalc div 1 2 3
- should print an error messagecalc add 1 sub 2 mult 3 div 4 2
- this is the aforementioned example that gives -3