一步一步带你打造自己的CLI命令工具

231 阅读4分钟

引言

什么是CLI呢?它是英语(command-line interface 缩写CLI),即命令行界面。命令行界面是是在**图形用户界面(GUI)**得到普及之前使用最为广泛的的用户界面。

我们现在电脑端的操作使用,更多用户习惯于图形界面,因为它易于使用,所见即所得,而命令行界面需要用户熟记一大堆的命令才能进行操作使用;但是在熟记命令的前提下,使用命令行界面往往要较使用图形用户界面的操作速度要快。GUI侧重于易用而CLI则侧重于效率。

本篇文章,我们基于node来实现一个简单的命令行工具:命令行输入歌曲名字,打开默认浏览器查找。

1.初始化项目
  • 新建工程目录 mkdir cli-demo && cd cli-demo
  • 初始化 npm init
  • 打开vscode编辑器 code .
  • 根目录新建bin文件夹并新建index.js
  • 修改package.json,删除无用main,script等字段
# package.json添加字段
"bin": {
    "ktb": "./bin/index.js"
}

说明:ktb即为我们cli工具的名称

  • 修改bin/index.js
#!/usr/bin/env node

console.log('hello ktb cli')

说明

  • 文件头部**#!/usr/bin/env node**用来向系统指明这个文件的解释器是node
  • 测试

1.通过node终端运行程序,node ./bin/index.js

2.好奇的同学肯定会问,现我们使用的Vue-Cli,create-react-app这些都有一个特定的命令,而我们使用上面的方式来执行是否太难看了,在上面的package.json文件中,我们已经添加了bin字段,并且声明了一个关键字:ktb和对应的执行文件。在我们使用第三方的命令工具之前,我们都先要安装到全局,然而我们目前还没有发布到npm,自然无法安装,这时候我们可以使用npm link这个命令,本地安装。

  1. 终端执行:ktb
// 终端输出
hello ktb cli
2.添加命令

2.1 查看版本信息

在我们使用第三方命令工具的时候,我们都可以通过参数-V或者--version来查看当前的版本号。这里我们会首先遇到几个问题:

  • 如何获取 -V这个参数?
  • 如何获取package.json的版本号?

在node程序中,我们可以通过使用process.argv获取到命令参数。

#!/usr/bin/env node

console.log(process.argv)

终端输入 ktb -v -h -l,控制台会输出:

[
  '/Users/kongtuanbing/.nvm/versions/node/v16.13.0/bin/node',
  '/Users/kongtuanbing/.nvm/versions/node/v16.13.0/bin/ktb',
  '-v',
  '-h',
  '-l'
]

不过在此我们并不采用process.argv的方式来获取,比较麻烦,我们使用一个第三方专门处理命令行交互的包:commander

npm install commander

如何获取package.json的版本号呢?

由于我在package.json添加了"type": "module"的字段来声明node采用es6模块化方案,所以不能直接require('package.json')来读取json文件。

我们在此使用fs模块fs.readFileSync('package.json')

完整代码:

// ./bin/index.js
#!/usr/bin/env node

import fs from 'fs';
import {program} from 'commander';
import { concatUrl } from '../utils/index.js';

// 读取package.json
const pak = JSON.parse(fs.readFileSync(concatUrl('./package.json'), 'utf-8'));
// ktb -V
program.version(pak.version).usage('<command> [options]')

program.parse(process.argv)
// ./utils/index.js
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';

export { openBrowser } from './open.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export function getRootPath(){
  return resolve(__dirname, '../')
}

export function concatUrl(...args){
  let result = getRootPath();
  const startUrl = getRootPath();
  args.forEach(item => {
    result = resolve(result, item)
  })
  return result;
}

此时我们可以输入:

  • ktb -V 或 --version 查看版本号
  • ktb -h 或 --help和使用说明

2.2 美化终端输出

我们使用create-react-app 工具时候,我们可以在终端输入:create-react-app -h

WechatIMG304.jpeg 我们可以看到,命令行输出的关键提示进行了颜色区分,增加交互式阅读体验。

在此,我们使用一个成熟的node包chalk

npm install chalk

用法:console.log(chalk.blue('Hello world!'));详细用法可参考chalk文档

修改./bin/index.js

#!/usr/bin/env node

import fs from 'fs';
import {program} from 'commander';
import chalk from 'chalk';
import { concatUrl } from '../utils/index.js';

// welcome
console.log(chalk.green('welcome to use ktb-cli'))

// 读取package.json
const pak = JSON.parse(fs.readFileSync(concatUrl('./package.json'), 'utf-8'));
// ktb -V
program.version(pak.version).usage('<command> [options]')
// ktb -h
program.on('--help', () => {
  console.log()
  console.log(
    `Run ${chalk.cyan(
      `ktb <command> --help`
    )} for detailed usage of given command.`
  )
  console.log()
})

program.parse(process.argv)

2.3 添加sing命令

这里我们添加一个命令,实现输入歌曲名称,查找歌曲并自动打开浏览器显示结果。

ktb sing 梦的翅膀已经受了伤

先介绍commander的用法:

# npm install commander

import {program} from 'commander';

  • program.command(name,arg),第一个参数为命令名称,第二个参数为命令参数。
  • 参数有3种表示方式:
    • <> 表示必须按
    • [] 表示可选参数
    • 点号表示可变长参数,如果使用,只能是最后一个参数
// 添加命令--new
program
  .command('sing <word>')
  .description('generate a new image use the input word') // 命令描述
  .option('-a --auto', 'auto play music') // 布尔选项
  .option('-t --time <time>', 'music time limit min', 1000) // 默认值选项
  .action((word, options) => {
    console.log(word)
    console.log(options)
  })

完整文件参考

#!/usr/bin/env node

import fs from 'fs';
import {program} from 'commander';
import chalk from 'chalk';
import { concatUrl, openBrowser } from '../utils/index.js';

// welcome
console.log(chalk.green('welcome to use ktb-cli'))

// 读取package.json
const pak = JSON.parse(fs.readFileSync(concatUrl('./package.json'), 'utf-8'));
// ktb -V
program.version(pak.version).usage('<command> [options]')
// 添加命令--new
program
  .command('sing <word>')
  .description('generate a new image use the input word') // 命令描述
  .option('-a --auto', 'auto play music') // 布尔选项
  .option('-t --time <time>', 'music time limit min', 1000) // 默认值选项
  .action((word, options) => {
    console.log(word)
    console.log(options)
    openBrowser(word)
  })

// ktb -h
program.on('--help', () => {
  console.log()
  console.log(
    `Run ${chalk.cyan(
      `ktb <command> --help`
    )} for detailed usage of given command.`
  )
  console.log()
})

program.parse(process.argv)
// util
import open from 'open';

export function openBrowser(search = '自由飞翔'){
  const rootPath = 'https://music.hitushen.cn/';
  const openUrl = new URL(rootPath);
  openUrl.search = `?name=${encodeURIComponent(search)}&type=netease`;
  switch(process.platform) {
    case 'darwin':
      open(openUrl.href)
      break
    case 'win32':
    open(openUrl.href)
      break
    default:
    open(openUrl.href)
  }
}

2.4 升级命令,添加问答操作

在我们使用vue-cli初始化一个工程的时候,我们都会看到有一系列的问答操作,来处理用户的需求,根据用户不同的问答结果反馈其要的结果

这里我们需要用到一个node成熟的交互模块,inquirer,具体使用,我们可以参考官方说明文档

我们在./bin/index.js 添加命令


// 添加命令--create
program
  .command('create <word>')
  .description('This is project name')
  .action(async(action) => {
      const action = await createAction();
      console.log(action)
  })
// createAction
import inquirer from 'inquirer';
export function createAction () {
  return inquirer.prompt([
    {
      type: 'input',
      message: 'please input music name',
      default: '自由飞翔',
      name: 'projectName'
    },
    {
      type: 'list',
      message: 'please select a player',
      default: 0,
      name: 'style',
      choices: [
        {
          value: 0,
          name: 'netease'
        },
        {
          value: 1,
          name: 'qq'
        },
        {
          value: 2,
          name: 'kugou'
        }
      ]
    },
    {
      type: 'confirm',
      message: 'Auto player?',
      default: false,
      name: 'auto'
    },
    {
      type: 'checkbox',
      name: 'allSelect',
      message: 'please select condition',
      default: 0,
      choices: [
        {
          value: 1,
          name: 'babel'
        },
        {
          value: 2,
          name: 'typescript'
        },
        {
          value: 3,
          name: 'postcss'
        }
      ]
    }
  ]).then(answers => answers)
}
结束

上面只是初步带领大家熟悉cli工具的基本实现。希望能够帮助到有需要学习了解的同学。