In the first part, we overviewed some basic characteristics of Node and npm, which help to bootstrap a program without any effort. We also implemented primary commands, like showing help and version messages. In this part, we are going to explore the main libraries used in Node for constructing a CLI application. And it makes sense to start with a short description of an example, that we are going to implement.
We are aiming to build a quiz program, which consumes a file with JSON formatted questions and options. After it’s configured, a user might want to answer the test’s questions. The quiz program should save the state, so the user can get back to left questions any time he wants. And when the test is finished, the application should print a result. We try to hold our implementation simple and small and keep in mind, that everything happens in command line surroundings.
Let’s start with initializing the package.json file, as all Node projects do.
Now we are ready to begin playing with code.
Our first tool, that we are going to explore is Commander.js. It is relatively small — around 1300 lines of documented code, and it doesn’t use any 3rd party dependency, so no left-pad incident can happen with it. However, Commander.js is a really powerful instrument to start with. It provides a set of useful built-in features, such as:
- A generic parsing arguments functionality, which helps to not worry about different flag formats and types.
- Commands declaration to organize code into logically structured chunks.
- Automated help messages based on declared commands and options eliminate the developer’s routine work.
To use the library, add the commander package and export it to the source code.
npm install commander
The main actor in Commander.js application is the program object, exported as a default from the package. In fact, it is an instance of the provided Command class. The package exports a few concepts:
- new Command() is the program main object to declare any commands on top of it.
- Command and Option classes are mostly used internally to instantiate sub actions and define arguments.
When calling the program’s parse(process.argv) method, Commander runs an appropriate operation regarding on provided arguments.
This code would become a full CLI application as soon as you install it globally with npm install — global or npm link command. The first line in the code is the shebang, which we discussed in the first article. With this code, you are now able to run quizme keyword inside the terminal. The — help or -h flags show an automatically generated description and available options. To show a help message explicitly when no arguments passed use help() and outputHelp() methods. Be aware, that the help() call does the process exit, so no further operation can be executed. Here is the output of the code above:
Remember, we’ve discussed a user-friendly way to show the version. By default, Commander.js shows a version when a command is called with -V and — version options, but it is possible to override option name with the second argument. To add a version action simply call the method version(version, ‘-v, — version’) on the program object.
There are a few ways to declare commands with Commander.js. Each command has a command() method, by calling which a new command is added.
In this case, description() and action() define how the command will be executed and which text a user will see.
The other way of declaring commands is to use sub-commands, by calling command() method with a name and a description, without specifying any action. That will add Git-like isolated command, so the Commander.js will look for files starting with quizme- prefix and the command name afterward. That approach nicely divides a declaration from an implementation, and each command can be written in a separate file without interfering to each other. Also, it can be executed and packaged as a single bundle. A new process will be spawned for each sub-command.
We use the first approach for the sake of simplicity. We define commands in the main file and execute actions from their declarations. We create a commands folder and put configure.js, start.js as we plan to create these two commands for the user who is filling the test. By specifying an index.js file inside the folder we simplify exporting dependencies from the main module.
With Commander.js syntax, we can define required and optional arguments for a given command.
In this version, we read a local file for filling in a quiz’ questions and options. Let’s keep a syntax simple, and declare text in a JSON format. An array will contain questions and a list of possible answers.
We store a configuration and a quiz state inside the package.json file itself. The commands/configure.js contains an implementation for saving the configuration.
And now, the configure command will read a provided local file path and store the configuration for further usage. For the future, it is also possible to add an ability to download external quiz data, for example from a github page.
Pass the Test
Now, when we configured questions, we can start working on taking the test experience.
First, let’s prepare the playground and define the start command.
Now we are ready to begin to throw questions and read the user’s input. There are plenty of npm packages, facilitating this functionality.
prompt — a package mostly dedicated to the user input. With the simple and clear syntax, it allows not only request data, but also describe it’s validation with JSON-Schema declarations. It allows to store and manipulate input with streams and output messages.
With prompt the start implementation could look like that:
We stick with promises, as it simplifies code perception. In the given code we had to promisify an API a little. Unfortunately, the prompt package is not doing what we wanted for the questionary. The user is suggested to type text, while we are looking more for selecting a choice.
The other option could be to use the cli-ux — a powerful tool by Heroku. It contains a set of nice I/O utilities, such as prompt, but also formatters and some useful actions, like opening a link in a browser, showing a spinner, or drawing a tree in the console. We’ll get into it in the next article when we are going to talk about CLI frameworks.
Inquirer.js — the most popular and rich library among others to request and read input in different ways. Like, in the way we need it selecting one from many options.
Here is how the code looks like with the Inquirer.js
It supports promises from the box, but also it’s a CLI select box, which nicely fits our tasks.
In fact, internally Inquirer.js uses Observable pattern to consume and manage values from the user’s input.
Save the State
We want to save the user’s answers history so that he can stop at any time he wants and later start with the question he finished with. We use the when() method of Inquirer question definitions and update the state:
Now, every time when the user answers a question, we update the questionary state. We are storing the questions configuration and the answering state in the CLI’s package.json. This approach has a number of drawbacks. This is a no standard way to save user’s configuration and information. Also, from the semantic point of view, extending module definition with unrelated data doesn’t sound the right thing to do. It is possible to use a database for that purpose, but we don’t’ want to increase the application complexity. The recommended approach might be to store user-specific data in the generic folder on a local machine.
Many open source projects are referencing to the XDG Base Directory Specification to specify directories where to store application configurations and data files.
In the future of quizme CLI, we can update the configure command, as it’s the only place which is connected to the real data storage. We’ll use XDG_CONFIG_HOME environment variable to define a directory for configuration files, and XDG_DATA_HOME for state files. Default values would be ~/.config/quizme, and ~/.local/share/quizme respectively.
Show the Result
When the last question is done, we show the “Thank you” message and output the user’s answers
In this article, we’ve overviewed different helpers, utilities, and libraries which help to build a CLI in Node. We’ve built a small quiz program for the terminal, try it yourself!
In the next part, we’ll construct a Command Line Interface application on the oclif framework.