Write in the front, actually really want to write a recipe, suffering from limited cooking ability, so the title is a lie, ha ha ^_~

Today we are going to talk about command-line interface (CLI) development.

By the end of this article, you should have a better understanding of how to develop a CLI from start to finish.

You can also bookmark this article, and when you want to develop a CLI, come back and check it out. You’ll always find what you’re looking for.

Daniel: Peanut coke is ready. Sit back and wait for it to start.

Chandler: All right, Let’s go! <(~) ~)↗[GO!]

> Step one: initialize the project

Create an empty project directory (cook-cli is the next example, so we’ll call it cook-cli) and initialize it by tapping the command in that directory as follows:

$ mkdir cook-cli
$ cd cook-cli
$ npm init --yes
Copy the code

This directory is initialized as a Node.js project with the NPM init command, which generates a package.json file in the cook-cli directory.

Add –yes will automatically answer all questions asked during initialization. You can try to remove this parameter and answer questions one by one yourself.

> Mainline Get through: CLI skeleton code

Now that the project is initialized, let’s add skeleton code and let the CLI fly for a while.

  • The implementer

We create the SRC /index.js file, which implements the CLI logic and does the actual work. The code is as follows:

export function cli(args) {
    console.log('I like cooking');
}
Copy the code
  • televsion

Next, you create the bin/cook file, which is the executable entry file to the CLI and the CLI’s proxy in the executable environment. The code is as follows:

#! /usr/bin/env node

require = require('esm') (module /*, options*/);
require('.. /src').cli(process.argv);
Copy the code

If you are careful, you will notice that the ESM module is used, which allows you to load modules directly from the JS source using the ECMAScript modules specification, i.e. import and export directly. The SRC /index.js code above can write export directly thanks to this module.

(Please run NPM I ESM in the project root directory to install the module.)

  • Officer xuan

We have surrogates, but we have to spread the word. So add a bin declaration to package.json to announce the presence of the surrogate. As follows:

{..."bin": {
    "cook": "./bin/cook"},... }Copy the code

Time rehearsal: run and debug locally

Before CLI, local development and debugging were essential, so a convenient debugging approach was necessary.

Daniel: To develop Web applications, I can debug functions through the browser. What about the CLI yesterday?

The CLI is ultimately run on a terminal, so we need to register it as a local command line first. The method is as simple as running the following command in the project root directory:

$ npm link
Copy the code

This command registers a Cook CLI in the local environment and links its execution logic to your project directory, so every change you make takes effect as soon as you save it.

Try running the following command:

$ cook
Copy the code

Daniel: Nice! But I have a problem, I want to set breakpoints in vscode to debug, so sometimes it’s easier to troubleshoot problems

You’re right. The method is also very simple, in vscode to add the following configuration, path: debug > add configuration. Modify the value of args based on 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
        }
    ]
}
Copy the code

> Intent identification: input parameter parsing

A quick interlude: While you may be exposed to all kinds of CLI in your work, it’s worth a brief introduction to some of the terms CLI refers to:

  • Commands and subcommands
# cook is the command
$ cook

# start is a subcommand of cook
$ cook start
Copy the code
  • Command options
# -v is a short flag option (note: only one letter, multiple letters represent multiple options)
$ cook -V

# --version is the full write mode (long name) option
$ cook --version
Copy the code
  • Command argument (argument)
# source.js and target.js are both arguments to the cp command
$ cp source.js target.js
Copy the code

Subcommands are also arguments to commands

If you want to implement a CLI, you will need to parse input arguments (subcommand, options, argument).

Commander: Hey, buddy, don’t be afraid. I got you.

Yeah, man, it’s good to have you. Next we parse the input parameter using the COMMANDER module in the following process and example:

  • Module is installed
$ npm i commander
Copy the code
  • SRC/index. Js sample
. import program from'commander';

export function cli(args) {
    program.parse(args);
}
Copy the code

One sentence. That’s how clean it is.

Daniel: What about the entry? How do you use it?

We will use these parsed input objects in the following examples. So please hold your horses.

> Can’t live without you: version and help

Version and help information are parts of a CLI that must be provided without being unprofessional. So let’s see how to do that.

Modify SRC /index.js as follows:

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);
}
Copy the code

A chain call to program.version and Usage is done, again cold.

Try running the following command:

$ cook -V
Copy the code

$ cook -h
Copy the code

> Add General: Add a subcommand

Now we begin to enrich the CLI by adding a subcommand, start.

It has one parameter, food, and one option, fruit, with the following code:

.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);
}
Copy the code

The above example demonstrates how to get parsed input parameters. You can get whatever you want from the action. What you want to do is up to you.

Try running a subcommand:

$ cook start pizza -f apple
Copy the code

> Call for external assistance: invoke external commands

Sometimes we need to invoke external commands, such as NPM, from the CLI.

Execa: It’s my turn to perform. ┏ (^ ^) omega = ☞

  • Module is installed
$ npm i execa
Copy the code
  • SRC/index. Js sample
. 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);
}
Copy the code

Above, the external command NPM -v is invoked through execa. Here, print the version number of NPM:

$ cook npm-version
Copy the code

> Facilitate communication: provide human-computer interaction

Sometimes we want the CLI to interact with the user in a q&A mode, where the user provides the information we want by typing or selecting.

Just then, a strong wind blew, and Inquirer. Js came flying through seven colorful clouds.

  • Module is installed
$ npm i inquirer
Copy the code

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

  • SRC/index. Js sample
. 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);
}

Copy the code

Code shallow, directly on the effect picture:

Reduce anxiety: Wait for reminders

Human-computer interaction experience is very important. If the work cannot be completed immediately, the progress of the current work needs to be timely feedback to the user, which can reduce the user’s feeling of waiting anxiety.

Ora and Listr came shoulder to shoulder, walking in neat steps.

First up is Ora

  • Module is installed
$ npm i ora
Copy the code
  • SRC/index. Js sample
. 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); }Copy the code

Without further ado, get straight to the picture above:

Listr followed.

  • Module is installed
$ npm i listr
Copy the code
  • SRC/index. Js sample
. 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); }Copy the code

Still not saying much, still going straight:

“> < p style =” max-width: 100%; clear: both; min-height: 1em

Chalk: I am a literary and artistic youth, I live for art, this should be me. < ~ ~ ~ /

  • Module is installed
$ npm i chalk
Copy the code
  • SRC/index. Js sample
. import chalk from'chalk';


export function cli(args) {

  console.log(chalk.yellow('I like cooking')); . }Copy the code

With colorful CLI, does it make you happier?

> Door decoration: add a border

Boxen: This is my thing, watch me! < (ˉ ^ ˉ) >

  • Module is installed
$ npm i boxen
Copy the code
  • SRC/index. Js sample
. import boxen from'boxen';

export function cli(args) {

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

Well, it looks a little more professional:

“> < p style =” max-width: 100%; clear: both

If you publish as scope, for example @daniel-dx/cook-cli. Add the following configuration to package.json to help you publish smoothly (although you can save this configuration if you are a paying member of NPM)

{
  "publishConfig": {
    "access": "public"}},Copy the code

3. A shot or launch:

$ npm publish
Copy the code

OK, you have published your CLI to the world, now you can go to www.npmjs.com/ to check out your published CLI.

Word-wrap: break-word! Important; “> < p style =” max-width: 100%

Update-notifier: It’s my turn at last. The man X X

  • Module is installed
$ npm i update-notifier
Copy the code
  • SRC/index. Js sample
. import updateNotifier from'update-notifier';

import pkg from '.. /package.json';

export functioncli(args) { checkVersion(); . }function checkVersion() {
  const notifier = updateNotifier({ pkg, updateCheckInterval: 0 });

  if(notifier.update) { notifier.notify(); }}Copy the code

Json to 0.0.9, and then run Cook to see the effect:

O ( ̄)  ̄)o perfect!


The above detailed some of the necessary or common steps to develop a CLI.

Of course, if you just want to develop a CLI quickly, as some leaders often say: Don’t talk to me about the process, I want the results. You can use a framework like OCLIF specifically designed for CLI development right out of the box.

As programmers, we still need to pay some time and energy to understand the ins and outs of solutions, so that we can be more practical and go further.

Well, that’s all for today, goodbye my friends!

Almost forgot, attached sample source: github.com/daniel-dx/c…

Back to old age. Get back to old age.