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.
Update: I created an egghead.io lesson about using yargs middleware, feel free to check it out
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:
argv
. -------------- -------------- ---------
stdin ----> argv ----> | Middleware 1 | ----> | Middleware 2 | ---> | Command |
-------------- -------------- ---------
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:
and more...
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.
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. Middlewares can be global, thanks to aorinevo or can be specific to a command which was the part I worked on.
You can also have command-level middlewares. If you want your middlware to be called before a specific command. You can add an array of middlware as the last argument of the .command()
function.
Example:
const normalizeCredentials = (argv) => {
if (!argv.username || !argv.password) {
const credentials = JSON.parse(fs.readSync('~/.credentials'))
return credentials
}
return {}
}
var argv = require('yargs')
.usage('Usage: $0 <command> [options]')
.command('login', 'Authenticate user', (yargs) =>{
return yargs.option('username')
.option('password')
} ,(argv) => {
authenticateUser(argv.username, argv.password)
},
[normalizeCredentials]
)
.argv;
Update: this is now available in the latest stable version of yargs.
To be able to use yargs you need to have the @next
version installed.
You can install it using npm i yargs@next
.
Enjoyed the content? Receive the next one in your inbox! No spam, just content.
Khaled Garbaya is a software developer and active opensourcerer at Contentful. He speaks multiple languages and is often overheard saying "Bonjour" in HTML. You can follow him on Twitter, on Github, and on YouTube. He also runs How To Contentful as a project.