Cooking a delicious CLI
Tags: node, javascript, cli, commmand
Published: 2019-10-31 11:57:32

![](https://camo.githubusercontent.com/2b53c0a61a44f86ab2aa8163233af407f5decb2b/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33312f313665316638313565643766616164633f773d3132383026683d38343726663d6a70656726733d353631313437)

> 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:

```shell
$ 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:

```javascript
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:

```shell
#!/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:

```json
{
  ...
  "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:

```shell
$ 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:

```shell
$ cook
```

![](https://camo.githubusercontent.com/4383a5f48ac5a5d4fbc40991eb56927e230b95f4/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316266653461663532326566633f773d34373826683d353026663d67696626733d38383338)

> 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.

```json
{
    "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

```shell
# cook is a command
$ cook

# start is the subcommand of cook
$ cook start
```

- Options

```shell
# -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

```shell
# 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

```shell
npm i commander
```

- src/index.js example

```javascript
......
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:

```javascript
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:

```shell
$ cook -V
```

![](https://camo.githubusercontent.com/35dba21017e90037ff35358c1b6a6a37dddfd4e1/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316330356465656138633563333f773d33343626683d363326663d67696626733d37373436)

```shell
$ cook -h
```

![](https://camo.githubusercontent.com/29721ee168d3acdd491addd74a295aaeb73446c5/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316330386630313537643839383f773d35333226683d31323526663d67696626733d3130363136)

----

## > 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:

```javascript
......
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:

```shell
$ cook start pizza -f apple
```

![](https://camo.githubusercontent.com/dfb88e64eb49f2ca6bb46efdc9f8fda39bcbb404/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316337616264643166643333343f773d34323626683d393526663d67696626733d3233383737)

----

## > 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

```shell
$ npm i execa
```

- src/index.js example

```javascript
......
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`:

```shell
$ cook npm-version
```

![](https://camo.githubusercontent.com/2767c254af5ec55b51a593f2ad01483ca8946c9d/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316338363338343234326265343f773d34313826683d353326663d67696626733d3134363133)

----

## > 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

```shell
$ npm i inquirer
```

The most common scenarios are: text input, boolean option, radio, check. Examples are as follows:

- src/index.js example

```javascript
......
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:

![](https://camo.githubusercontent.com/bcf53bd6b3c942de69b584959377018934907808/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316361316437633062346135633f773d37303826683d31393126663d67696626733d3333343638)

----

## > 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` and `listr` shoulder to shoulder, marching neatly, oncoming.

The first thing is `ora`.

- Module installation

```shell
$ npm i ora
```

- src/index.js example

```javascript
......
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:

![](https://camo.githubusercontent.com/d5b2bbf3a416c83eba62bebecb4a89ceed7dd95f/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316361646332623037643431333f773d33333526683d363226663d67696626733d3334363739)

`listr` followed.

- Module installation

```shell
$ npm i listr
```

- src/index.js example

```javascript
......
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:

![](https://camo.githubusercontent.com/914f7269ef1aaa371aa21044ccea6a0875d0b849/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316362343133663933656134613f773d33353126683d31303826663d67696626733d3232313339)

----

## > Colorful: Make life no longer monotonous

> `chalk`: I am a literary youth, I live for art, It’s me. <(￣ˇ￣)//

- Module installation

```shell
$ npm i chalk
```

- src/index.js example

```javascript
.....
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?

![](https://camo.githubusercontent.com/9aa90ad9f701fe31bc20e5d426ab35e7ab47daec/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316362373738393633383233363f773d31333926683d343726663d706e6726733d31363333)

----

## > Decoration door: Add a border

> `boxen`: This is my masterpiece, look at me! <(ˉ^ˉ)>

- Module installation

```shell
$ npm i boxen
```

- src/index.js example

```javascript
......
import boxen from 'boxen';

export function cli(args) {

  console.log(boxen(chalk.yellow('I like cooking'), { padding: 1 }));
  
  ......
}  
```

Well, it looks professional:

![](https://camo.githubusercontent.com/404c8eaa03dc1963743568ee60c5e78eebd3ddd9/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316362616239326233343534363f773d31393826683d31313626663d706e6726733d32353530)

----

## > 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)

```json
{
  "publishConfig": {
    "access": "public"
  },
}
```

Go go go:

```shell
$ 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

```shell
$ npm i update-notifier
```

- src/index.js example

```javascript
......

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:

![](https://camo.githubusercontent.com/78d8bdafdf30d0c492743c1add8974f107be8eb0/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f31302f33302f313665316362633961313961383230643f773d34313326683d32363226663d706e6726733d39303639)

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...