The pile of one-off command line scripts I’ve written for my current weekend projecting finally reached the critical mass where they needed some organizing, which led me to discover the yargs project. Yargs provides mature argument and option parsing for command line programs (both --foo
and -b
style options) and features for defining individual sub-commands (think programs like git sub-command
). It’s also been around for six plus years and is still receiving updates.
I’m coming up on the one year anniversary of NodeJS being my primary programming language, and I’m starting to get a better feel for how folks organize their programs and libraries. I’ve noticed the ongoing challenges/evolution of single threaded asynchronous programming means there’s a few distinct styles out there
- When All we had were Callbacks
- Let’s all Write our Own Promises
- Let’s use Native Promises
- Let’s use Promises with
async/await
Yargs appears to fall mostly in the When All we had was Callbacks camp. The rest of this post is a few places where the choices of the yargs engineers clashed with my own expectations.
First — here’s their Hello World example.
require('yargs')
.command('serve [port]', 'start the server', (yargs) => {
yargs
.positional('port', {
describe: 'port to bind on',
default: 5000
})
}, (argv) => {
if (argv.verbose) console.info(`start server on :${argv.port}`)
serve(argv.port)
})
.option('verbose', {
alias: 'v',
type: 'boolean',
description: 'Run with verbose logging'
})
.argv
Although there’s nothing visible in this code that says “async”, its APIs are designed around passing in anonymous functions. This specific example is also made a little hard to follow since it uses the “arrow function” declaration syntax.
// using the `function` keyword to define a function
const myFunction = function() { //function code here}
// using the arrow function syntax instead
const myFunctionAlso = () => { //function code here}
// arrow functions may also look like this -- the value
// return by the single line is the value returned by
// the function
const myFunctionAlsoUgh = () => /* single line of function code here/*;
I’m still not a fan of this particular style of function declaration, but that’s another post for another time. If we write that hello world code in a different way —
require('yargs')
.command(
'serve [port]',
'start the server',
functionThatWillDefineCommandArguments,
functionThatWillDoTheWorkOfTheCommand
)
.option('verbose', {
alias: 'v',
type: 'boolean',
description: 'Run with verbose logging'
})
.argv
the API becomes a bit clearer. To setup a command you call the command
method of the object returned by the yargs
module. The command
method accepts four arguments — one is the command’s name with its position arguments, the second is a description, the third is a function you can use to configure things about your command’s positional arguments, and the fourth is a command that does the actual work.
It may look like the option
method is setting up a --verbose/-v
flag for the command, but these options appear to be global for every command. The call to command
does not return a command object, it returns the same object instance returned by the call to require('yargs')
. Yargs does not appear to let you define options that are specific to a command. Instead, the same options be available for all your commands.
The final bit I was confused by was the .argv
at the end. This appears to be a simple property access that has no reason to be there. However, without it, yargs doesn’t work. After digging into it a bit, is discovered that argv
is setup as a javascript getter, which means accessing this property actually invokes a method. This explains how a simple looking property access actually invokes the functionality we expect from yargs.