“Subcommands” in Yargs

September 15, 2020

I was working on some CLI code this week written in yargs. I struggled a bit with their docs because I wasn’t familiar with their terms and didn’t know what to Google.

I figured it’d be useful to others to write a post detailing the different ways of working with yargs and the official terminology.

The basic case

In a lot of the docs you’ll see a very specific example for working with yargs commands.

yargs.command(
'cmd',
'description goes here',
function () {
// we'll talk about this more
}
)

The code I was working with was a bit difference so I found this example confusing. Let’s break it down.

command takes four arguments. The first is the command key itself, e.g. get, config, etc. The second is the description of the command. The third can be a function or an object and it’s called a builder. The final argument is the optional handler function.

Making it easier

I quickly realized that part of my confusion was due to the fact that the docs examples were structured differently than the codebase I was working in. In this case, I think my starting point was helpful.

yargs.command(
command: 'cmd',
describe: 'description goes here',
builder: yargs => {
// we'll talk about this more
},
handler: argv => {
// we'll talk about this too
}
)

Having those keys helped me identify what was what and it took me a bit to realize the mapping to the docs examples. Most of this was based on the differences between the builder and handler functions.

Builder function

The builder function is in charge of providing additional information about your command. It’s where all the data is stored that users see when they type --help.

The most common examples I found for builder functions used the keyword option.

yargs.command(
command: 'cmd',
describe: 'description goes here',
builder: yargs => {
yargs.option(`flag`, {
type: `string`,
describe: `What this flag does`.
})
},
handler: argv => {
// we'll talk about this too
}
)

There are a number of configurations available for options, they’re very powerful. However, what are they? If I wanted to write a test for this code, how would I call it?

I’d call it like this.

project cmd --flag value

This is a pretty typical pattern for CLIs. “Flag” arguments. And the handler can access the value of those flags.

yargs.command(
command: 'cmd',
describe: 'description goes here',
builder: yargs => {
yargs.option(`flag`, {
type: `string`,
describe: `What this flag does`.
})
},
handler: ({flag}) => {
if (flag) console.log("something")
}
)

So the builder defines what is available for use on a given command and the handler “handles” it. This is great, but it wasn’t what I wanted to do.

Positional arguments

Instead of a flag command the requirements for my code were a bit different. I had two scenarios to support.

The first, was a command without any arguments.

project cmd

In this scenario, I wanted to print out the data. Alternatively, I wanted a user to be able to set the data using the same parent command.

project cmd set key value

Why was this such a struggle? Well, I spent a while searching for the term subcommand. In reality, I wanted to be looking at the docs for positional arguments.

yargs.command(
command: 'cmd [sub] [key] [value]',
describe: 'description goes here',
builder: yargs => {
yargs.positional(`sub`, {
type: `string`,
describe: `What this argument is`.
})
yargs.positional(`key`, {
type: `string`,
describe: `What this argument is`.
})
yargs.positional(`value`, {
type: `string`,
describe: `What this argument is`.
})
},
handler: ({sub, key, value}) => {
if (sub) {
console.log("something")
return
}
console.log("if not sub, do this")
}
)

There are different types of positional arguments, but the square brackets indicate arguments that are optional. Meaning, the command does not require them to work.

Not quite done

That code was mostly what I wanted, but it was missing a key ingredient. When a user passed --help they wouldn’t see the same type of options print out that the flag scenario gave them.

No matter, positional arguments take an array called choices!

yargs.positional(`sub`, {
choices: [`set`],
type: `string`,
describe: `What this argument is`.
})

My requirements only called for a single “subcommand” but this structure means I could add more in the future without changing much.

Yargs!

In the process of meeting these requirements I scratched my head quite a bit. Figuring out the right terminology and how to map the existing code to the examples was a challenge. Hopefully this will help someone else in the future!

Let’s be honest, that future person will be me 😂.


A blog by Laurie Barth.
Teacher of all things tech.