Using Express.js a lot, I was always a big fan of the middleware approach when handling routes.
When I started building CLI tools I noticed that there is a lot of similarity between a server-side program and a command line tool.
Think of the command that a user types as the route or URL. For example cli-tool project new
in a server environment will be the following url example.com/project/new
.
A Request
object in the cli world can be the stdin
and the Response
as the stdout
.
A while ago I introduced the middleware concept to yargs, the main framework I was using to build clis.
You can check the pull request if you want to checkout the code.
What is a middleware?
A middleware is a function that has access to the incoming data in our case will be the argv
. It is usually executed before a yargs command.
Middleware functions can perform the following tasks:
- Execute any code.
- Make changes to the
argv
. - End the request-response cycle.
-------------- -------------- ---------
stdin ----> argv ----> | Middleware 1 | ----> | Middleware 2 | ---> | Command |
-------------- -------------- ---------
What is yargs?
Yargs helps you build interactive command line tools, by parsing arguments and generating an elegant user interface.
It's an amazing library that remove all the pain of parsing the command line args also it provides more features like:
- commands and (grouped) options.
- A dynamically generated help menu based on your arguments.
- bash-completion shortcuts for commands and options.
and more...
A simple Node.js command line tool with yargs
Let's create a simple command line program that authenticate the user saves the state to a file called .credentials
to be used in the next commands.
const argv = require('yargs')
const fs = require ('fs')
argv
.usage('Usage: $0 <command> [options]')
.command('login', 'Authenticate user', (yargs) => {
// login command options
return yargs.option('username')
.option('password')
},
({username, password}) => {
// super secure login, don't try this at home
if (username === 'admin' && password === 'password') {
console.log('Successfully loggedin')
fs.writeFileSync('~/.credentials', JSON.stringify({isLoggedIn: true, token:'very-very-very-secret'}))
} else {
console.log('Please provide a valid username and password')
}
}
)
.command('secret', 'Authenticate user', (yargs) => {
return yargs.option('token')
},
({token}) => {
if( !token ) {
const data = JSON.parse(fs.readFile('~/.credentials'))
token = data.token
}
if (token === 'very-very-very-secret') {
console.log('the secret word is `Eierschalensollbruchstellenverursacher`') // <-- that's a real german word btw.
}
}
)
.command('change-secret', 'Authenticate user', (yargs) => {
return yargs.option('token')
},
({token, secret}) => {
if( !token ) {
const data = JSON.parse(fs.readFile('~/.credentials'))
token = data.token
}
if (token === 'very-very-very-secret') {
console.log(`the new secret word is ${secret}`)
}
}
)
.argv;
The very first problem in the code is that you have a lot of duplicate code whenever you want to check if the user authenticated.
One more problem can popup is when more then one person is working on this. Adding another "secret" command feature will require someone to care about authentication, which is not ideal. What about an authentication function that gets called before every command and attach the token to your args.
Adding yargs middleware
const argv = require('yargs')
const fs = require ('fs')
cosnt normalizeCredentials = (argv) => {
if( !argv.token ) {
const data = JSON.parse(fs.readFile('~/.credentials'))
token = data.token
}
return {token} // this will be added to the args
}
const isAuthenticated = (argv) => {
if (token !== 'very-very-very-secret') {
throw new Error ('please login using the command mytool login command')
}
return {}
}
argv
.usage('Usage: $0 <command> [options]')
.command('login', 'Authenticate user', (yargs) => {
// login command options
return yargs.option('username')
.option('password')
},
({username, password}) => {
// super secure login, don't try this at home
if (username === 'admin' && password === 'password') {
console.log('Successfully loggedin')
fs.writeFileSync('~/.credentials', JSON.stringify({isLoggedIn: true, token:'very-very-very-secret'}))
} else {
console.log('Please provide a valid username and password')
}
}
)
.command('secret', 'Authenticate user', (yargs) => {
return yargs.option('token')
},
(argv) => {
console.log('the secret word is `Eierschalensollbruchstellenverursacher`') // <-- that's a real german word btw.
}
)
.command('change-secret', 'Authenticate user', (yargs) => {
return yargs.option('token')
},
(argv) => {
console.log(`the new secret word is ${secret}`)
}
)
.middleware(normalizeCredentials, isAuthenticated)
.argv;
With these two small changes we now have cleaner commands code. This willl help you a lot when maintaining the code especially when you change the authentication code for example.Commands can be global, thanks to aorinevo or can be specific to a command which was the part I worked on.
Can I use yargs middleware now?
To be able to use yargs you need to have the @next
version installed.You can install it using npm i yargs@next
.
Top comments (0)