DEV Community

Daniel.xiao
Daniel.xiao

Posted on

Cooking a delicious CLI

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
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode

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"
  },
  ...
}

Enter fullscreen mode Exit fullscreen mode

> 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

> 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • Parameters
# source.js and target.js are both parameters of the cp command
$ cp source.js target.js
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • src/index.js example
......
import program from 'commander';

export function cli(args) {
    program.parse(args);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

It's done by chained calls to program.version and usage, and it's still cool.

Try running the following command:

$ cook -V
Enter fullscreen mode Exit fullscreen mode


$ cook -h
Enter fullscreen mode Exit fullscreen mode


> 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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode


> 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
Enter fullscreen mode Exit fullscreen mode
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

The above external command is called by execa is npm -v. Let's print the version of npm:

$ cook npm-version
Enter fullscreen mode Exit fullscreen mode


> 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
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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

The first thing is ora.

  • Module installation
$ npm i ora
Enter fullscreen mode Exit fullscreen mode
  • 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);
}

Enter fullscreen mode Exit fullscreen mode

Here is the result:

listr followed.

  • Module installation
$ npm i listr
Enter fullscreen mode Exit fullscreen mode
  • 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);
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • src/index.js example
.....
import chalk from 'chalk';


export function cli(args) {

  console.log(chalk.yellow('I like cooking'));

  .....

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • src/index.js example
......
import boxen from 'boxen';

export function cli(args) {

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

  ......
}  
Enter fullscreen mode Exit fullscreen mode

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"
  },
}
Enter fullscreen mode Exit fullscreen mode

Go go go:

$ npm publish
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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();
  }
}

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)