Cooking a delicious CLI
Daniel.xiao
Posted on October 31, 2019
Written at the begining, I really want to write a recipe, and suffer from limited cooking ability, so the title is a lie, forgive me ^_~
Today, let's talk about the development of the command-line interface (abbreviated as CLI, the following will replace the lengthy command-line interface nouns with CLI).
After reading this article, you will have a more comprehensive understanding of developing a CLI from beginning to end.
You can also bookmark this article. When you want to develop a CLI, come back and you will always find what you want.
Daniel: Cola and potato chips are ready, waiting for you to start
All right. Let's go! <( ̄︶ ̄)↗[GO!]]
> Take the first step: Initialize the project
Create an empty project directory (the following is an example of cook-cli
, so here we name it cook-cli
), then type the command at the directory path to initialize, the process is as follows:
$ mkdir cook-cli
$ cd cook-cli
$ npm init --yes
The npm init
command will initialize the directory to a Node.js
project, which will generate a package.json
file in the cook-cli
directory.
Adding --yes
will automatically answer all the questions that were asked during the initialization process. You can try to remove the parameter and answer them yourself.
> Through main line: CLI skeleton codes
The project is initially complete, then we add the skeleton codes and let the CLI fly for a while.
- Implementer
We create the src/index.js
file, which is responsible for implementing the functional logic of the CLI. code show as below:
export function cli(args) {
console.log('I like cooking');
}
- Spokesperson
Then create the bin/cook
file, which is the executable entry file for the CLI and the spokesperson for the CLI in the executable environment. code show as below:
#!/usr/bin/env node
require = require('esm')(module /*, options*/);
require('../src').cli(process.argv);
Careful, you will find that the esm
module is used here. Its function is to let us use the ECMAScript modules
specification to load modules directly in the JS source code, ie use import
and export
directly. The code in src/index.js
above can directly write export
thanks to this module.
(Run npm i esm
in the project root path to install the module)
- External publicity
We have spokesperson, but we must be publicized. So add a bin
statement to package.json
to announce the existence of the spokesperson. as follows:
{
...
"bin": {
"cook": "./bin/cook"
},
...
}
> Frequent rehearsal: Local development and debugging
Local development and debugging is essential before the CLI is available, so a convenient debugging way is necessary.
Daniel: Developing web applications, I can debug features through a browser. What did the CLI get?
The CLI is running on the terminal, so we have to register it as a local command line. The way is very simple, run the following command in the project root path:
$ npm link
This command will register a cook
CLI in the local environment and link its execution logic codes to your project directory, so it will take effect as soon as you update the code.
Try running the following command:
$ cook
Daniel: Nice! But I still have a problem, I want to set a breakpoint in vscode to debug, which sometimes makes it easier to troubleshoot the problem.
You are right. That is also very simple.
Add the following configuration to vscode. The path is: Debug > Add Configuration
. Modify the value of args
according to the actual command parameters to be debugged.
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Cook",
"program": "${workspaceFolder}/bin/cook",
"args": ["hello"] // Fill in the parameters you want to debug
}
]
}
> Intent recognition: parameters analysis
Insert an episode: although you may use various CLIs at work, it is necessary to give a brief introduction to some of the terms that CLI refers to:
- Command and Subcommand
# cook is a command
$ cook
# start is the subcommand of cook
$ cook start
- Options
# -V is an option for short flag mode (note: only one letter, multiple letters means multiple options)
$ cook -V
# --version is the option for long name mode
$ cook --version
- Parameters
# source.js and target.js are both parameters of the cp command
$ cp source.js target.js
In fact, subcommands are also parameters of the command
Ok, from the above introduction, we know if we want to implement a CLI, the analysis of the input parameters (including subcommand, options, argument) can not escape, then we will face them.
commander: Hey, brother, don't be afraid. I am here!
Yes, brother, it’s good to see you. Next, we will use the commander
module to parse the parameters. The process and example are as follows:
- Module installation
npm i commander
- src/index.js example
......
import program from 'commander';
export function cli(args) {
program.parse(args);
}
Just one sentence to get it, so cool.
Daniel: What about the input parameters? How to use it?
In the next example, we will use these parsed input parameters. So please don't worry about it now.
> Can't live without you: version and help
The version and help information is a part of the CLI that must be provided, otherwise it is too unprofessional. Let's see how to achieve it.
Modify src/index.js
with the following code:
import program from 'commander';
import pkg from '../package.json';
export function cli(args) {
program.version(pkg.version, '-V, --version').usage('<command> [options]');
program.parse(args);
}
It's done by chained calls to program.version
and usage
, and it's still cool.
Try running the following command:
$ cook -V
$ cook -h
> Add a general: Add a subcommand
Now let's start to enrich the functionality of the CLI, starting with adding a subcommand start
.
It has a parameter food
and an option --fruit
, the code is as follows:
......
export function cli(args) {
.....
program
.command('start <food>')
.option('-f, --fruit <name>', 'Fruit to be added')
.description('Start cooking food')
.action(function(food, option) {
console.log(`run start command`);
console.log(`argument: ${food}`);
console.log(`option: fruit = ${option.fruit}`);
});
program.parse(args);
}
The above example demonstrates how to get the parsed input parameters. In action
you can get everything you want. What you want to do is up to you.
Try running the subcommand:
$ cook start pizza -f apple
> Seeking foreign aid: Calling external commands
Sometimes we need to call external commands in the CLI, such as npm
.
execa: I am going to perform. ┏ (^ω^)=☞
- Module installation
$ npm i execa
- src/index.js example
......
import execa from 'execa';
export function cli(args) {
.....
program
.command('npm-version')
.description('Display npm version')
.action(async function() {
const { stdout } = await execa('npm -v');
console.log('Npm version:', stdout);
});
program.parse(args);
}
The above external command is called by execa
is npm -v
. Let's print the version of npm
:
$ cook npm-version
> Promoting communication: providing human interaction
Sometimes we want the CLI to interact with the user in a question-and-answer way, and the user can provide the information we want by inputting or selecting.
At thie moment, a strong wind blew.
Inquirer.js
ran on the colorful clouds.
- Module installation
$ npm i inquirer
The most common scenarios are: text input, boolean option, radio, check. Examples are as follows:
- src/index.js example
......
import inquirer from 'inquirer';
export function cli(args) {
......
program
.command('ask')
.description('Ask some questions')
.action(async function(option) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'What is your name?'
},
{
type: 'confirm',
name: 'isAdult',
message: 'Are you over 18 years old?'
},
{
type: 'checkbox',
name: 'favoriteFrameworks',
choices: ['Vue', 'React', 'Angular'],
message: 'What are you favorite frameworks?'
},
{
type: 'list',
name: 'favoriteLanguage',
choices: ['Chinese', 'English', 'Japanese'],
message: 'What is you favorite language?'
}
]);
console.log('your answers:', answers);
});
program.parse(args);
}
The code is simple, let's directly see the result:
> Reduce anxiety: display hints in processing
The human interaction experience is very important. If you can't complete the work immediately, you need to feedback the progress of the user's current work in time, which can reduce the user's waiting anxiety.
ora
andlistr
shoulder to shoulder, marching neatly, oncoming.
The first thing is ora
.
- Module installation
$ npm i ora
- src/index.js example
......
import ora from 'ora';
export function cli(args) {
......
program
.command('wait')
.description('Wait 5 secords')
.action(async function(option) {
const spinner = ora('Waiting 5 seconds').start();
let count = 5;
await new Promise(resolve => {
let interval = setInterval(() => {
if (count <= 0) {
clearInterval(interval);
spinner.stop();
resolve();
} else {
count--;
spinner.text = `Waiting ${count} seconds`;
}
}, 1000);
});
});
program.parse(args);
}
Here is the result:
listr
followed.
- Module installation
$ npm i listr
- src/index.js example
......
import Listr from 'listr';
export function cli(args) {
......
program
.command('steps')
.description('some steps')
.action(async function(option) {
const tasks = new Listr([
{
title: 'Run step 1',
task: () =>
new Promise(resolve => {
setTimeout(() => resolve('1 Done'), 1000);
})
},
{
title: 'Run step 2',
task: () =>
new Promise((resolve) => {
setTimeout(() => resolve('2 Done'), 1000);
})
},
{
title: 'Run step 3',
task: () =>
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Oh, my god')), 1000);
})
}
]);
await tasks.run().catch(err => {
console.error(err);
});
});
program.parse(args);
}
Still directly to see the result:
> Colorful: Make life no longer monotonous
chalk
: I am a literary youth, I live for art, It’s me. <( ̄ˇ ̄)//
- Module installation
$ npm i chalk
- src/index.js example
.....
import chalk from 'chalk';
export function cli(args) {
console.log(chalk.yellow('I like cooking'));
.....
}
With the color of the CLI, is it to make you feel more happy?
> Decoration door: Add a border
boxen
: This is my masterpiece, look at me! <(ˉ^ˉ)>
- Module installation
$ npm i boxen
- src/index.js example
......
import boxen from 'boxen';
export function cli(args) {
console.log(boxen(chalk.yellow('I like cooking'), { padding: 1 }));
......
}
Well, it looks professional:
> Announcement: Publish to everyone
If you publish in scope
mode, for example @daniel-dx/cook-cli
. Then add the following configuration to package.json
to allow you to publish it smoothly (of course, if you are a paid member of npm, then this configuration can be ignore)
{
"publishConfig": {
"access": "public"
},
}
Go go go:
$ npm publish
OK, you have already released your CLI to the world, now you can go to https://www.npmjs.com/ to check your CLI.
> Sweet reminder: You should upgrade now
update-notifier: I finally got to play. I have waited until the flowers have been thanked. X_X
- Module installation
$ npm i update-notifier
- src/index.js example
......
import updateNotifier from 'update-notifier';
import pkg from '../package.json';
export function cli(args) {
checkVersion();
......
}
function checkVersion() {
const notifier = updateNotifier({ pkg, updateCheckInterval: 0 });
if (notifier.update) {
notifier.notify();
}
}
For local debugging, we will reduce the local CLI version, change the version
of package.json
to 0.0.9
, and then run cook
to see the effect:
o( ̄︶ ̄)o Perfect!
The above details some of the necessary or common steps to develop a CLI.
Of course, if you just want to develop a CLI quickly, you can consider to use frameworks such as oclif
that are created for the development of the CLI, out of the box.
As a programmer, we need to pay some time and energy for the ins and outs of the solution, the understanding of past and present, so that we can be more practical and go further.
Ok, that's all.
Here is the sample source code: https://github.com/daniel-dx/cook-cli
┏(^0^)┛ goodbye my friends! ByeBye...
Posted on October 31, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.