专栏首页一源一世界烹饪一道美味的 CLI
原创

烹饪一道美味的 CLI

写在最前,其实真想写一写食谱来着,苦于烹饪能力有限,所以标题就是个谎言,哈哈^_~

今天咱们就来聊一聊命令行工具(即 CLI:command-line interface,以下都会以 CLI 来代替冗长的命令行工具名词 )的开发。

阅读完本文,你会对从头到尾开发一个 CLI 有一个较全面的认识。

你也可以收藏下这篇文章,当你想开发一个 CLI 时,回来翻一翻,总会找到你想要的。

丹尼尔:花生可乐准备好了,坐等开始。

好勒,这就开始,Let's go! <( ̄︶ ̄)↗GO!


> 迈出第一步:初始化项目

创建一个空项目目录(接下来都是以 cook-cli 来作例子的,所以这里我们命名为 cook-cli),然后在该目录下敲打命令进行初始化,过程如下:

$ mkdir cook-cli
$ cd cook-cli
$ npm init --yes

通过 npm init 命令,会将该目录初始化为一个 Node.js 项目,它会在 cook-cli 目录下生成 package.json 文件。

--yes 会自动回答初始化过程中提问的所有问题,你可以试着将该参数去掉,自己一个一个问题进行回答。


> 主线打通:CLI 骨架代码

项目已初始完毕,接下来我们添加骨架代码,让 CLI 飞一会。

  • 实现者

我们创建 src/index.js 文件,它负责实现 CLI 的功能逻辑,是实际干活的。代码如下:

export function cli(args) {
    console.log('I like cooking');
}
  • 代言者

接着创建 bin/cook 文件,它是 CLI 的可执行入口文件,是 CLI 在可执行环境中的代言者。代码如下:

#!/usr/bin/env node

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

细心的你会发现这里用到了 esm 这个模块,它的作用是让我们可以在 js 源代码中直接使用 ECMAScript modules 规范加载模块,即直接使用 importexport。上面 src/index.js 的代码中能直接写 export 得益于该模块。

(请在项目根目录运行 npm i esm 来安装该模块)

  • 官宣

我们有代言者,但必须对外宣传才行。所以在 package.json 中增加 bin 的声明,对外宣布代言者的存在。如下:

{
  ...
  "bin": {
    "cook": "./bin/cook"
  },
  ...
}


> 时刻彩排:本地运行和调试

在 CLI 面世之前,本地开发调试是必不可少的,所以便捷的调试途径非常必要。

丹尼尔:开发 Web 应用,我可以通过浏览器来调试功能。那 CLI 昨弄呢?

CLI 最终是在终端运行的,所以我们要先把它注册为本地命令行。方法非常简单,在项目根目录运行以下命令即可:

$ npm link

该命令会在本地环境注册一个 cook CLI,并将其执行逻辑代码链接到你的项目目录,所以你每次修改保存后即立即生效。

试着运行以下命令:

$ cook

丹尼尔:Nice!但我还有个问题,我想要在 vscode 中设置断点来调试,这样有时候会更容易排查问题

你说得没错。方法也是很简单的,在 vscode 加入以下配置即可,路径为:调试 > 添加配置。根据实际要调试的命令参数,修改 args 的值即可。

{
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Cook",
            "program": "${workspaceFolder}/bin/cook",
            "args": ["hello"] // Fill in the parameters you want to debug
        }
    ]
}


> 意图识别:入参解析

插个小插曲:虽然你们在工作中可能经常接触到各种 CLI,但这里还是有必要对 CLI 涉及到的一些术语作简短的介绍:

  • 命令(command)和子命令(subcommand)
# cook 即为命令
$ cook

# start 即为 cook 的 子命令
$ cook start
  • 命令选项(options)
# -V 为简写模式(short flag)的选项(注意:只能一个字母,多个字母代表多个选项)
$ cook -V

# --version 为全写模式(long name)的选项
$ cook --version
  • 命令参数(argument)
# source.js 和 target.js 都为 cp 命令的参数
$ cp source.js target.js

其实,子命令也是命令的参数

Ok,从以上的介绍来看,我们要实现一个 CLI,对入参(包括 subcommand, options, argument)的解析是逃不掉的,那我们就直面它们吧。

commander:嘿,兄弟,别怕,有我呢!

是的,兄弟,有你真好。接下来我们通过使用 commander 这个模块来解析入参,过程和示例如下:

  • 模块安装
$ npm i commander
  • src/index.js 示例
......
import program from 'commander';

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

一句搞定,就是这么干脆利落。

丹尼尔:入参呢?怎么用呢?

在接下来的例子中,我们就会用到这些解析完的入参对象。所以,请先稍安勿躁。


> 不能没有你:版本和帮助

版本和帮助信息是一个 CLI 必须提供的部分,不然就显得太不专业了。我们就来看下如何实现吧。

修改 src/index.js ,代码如下:

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

通过 program.versionusage 的链式调用就搞定了,还是那么的冷酷。

试着运行以下命令:

$ cook -V
$ cook -h

> 添加大将:新增子命令

现在我们开始丰富 CLI 的功能,从增加一个子命令 start 开始。

它拥有一个参数 food 和 一个选项 --fruit,代码如下:

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

上面例子演示了如何获取解析后的入参,在 action 中你可以取到你想要的一切,你想做什么,完全由你做主。

尝试运行子命令:

$ cook start pizza -f apple

> 寻求外援:调用外部命令

有些时候,我们需要在 CLI 中去调用外部命令,如 npm 之类的。

execa:该我上场表演了。┏ (\^ω^)=☞

  • 模块安装
$ npm i execa
  • src/index.js 示例
......
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);
}

以上通过 execa 来调用外部命令 npm -v。来,打印一下 npm 的版本号吧:

$ cook npm-version

> 促进交流:提供人机交互

有些时候我们希望 CLI 能通过一问一答的方式与用户互动,用户通过输入或选择的方式来提供我们想要的信息。

就在此时,一阵大风吹过,只见 Inquirer.js 踏着七彩云飞奔而来。

  • 模块安装
$ npm i inquirer

最常见的场景是:文本输入,是否选项,复选,单选。例子如下:

  • src/index.js 示例
......
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);
}

代码浅显,直接上效果图吧:


> 减少焦虑:等待提醒

人机交互体验很重要,如果不能马上完成的工作,就需要及时反馈用户当前工作的进度,这样可以减少用户的等待焦虑感。

oralistr 肩并着肩,迈着整齐的步伐,迎面而来。

首先上场的是 ora

  • 模块安装
$ npm i ora
  • src/index.js 示例
......
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);
}

话不多说,直接上图:

listr 随后而来。

  • 模块安装
$ npm i listr
  • src/index.js 示例
......
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);
}

依然话不多说,依然直接上图:


> 加点色彩:让生活不再单调

chalk:我是文艺青年,我为艺术而活,这该非我莫属了。<( ̄ˇ ̄)/

  • 模块安装
$ npm i chalk
  • src/index.js 示例
.....
import chalk from 'chalk';


export function cli(args) {

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

有了色彩的 CLI,是不是让你心情更加愉悦:


> 门面装饰:加个边框

boxen:这个是我的拿手好戏,看我的!<(ˉ^ˉ)>

  • 模块安装
$ npm i boxen
  • src/index.js 示例
......
import boxen from 'boxen';

export function cli(args) {

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

嗯,看上去专业一些了:


> 公布成果:可以发表了

如果你是以 scope 方式发布,例如 @daniel-dx/cook-cli。那么在 package.json 中增加以下配置可以让你顺利发布(当然,如果你是 npm 的付费会员,那这个配置是可以省的)

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

临门一脚,发射:

$ npm publish

OK,已经对全世界发布了你的 CLI 了,现在你可以到 https://www.npmjs.com/ 去查询下你发布的 CLI 了。


> 温馨提醒:该升级了

update-notifier:终于到我了,我等到花儿已谢了。 X﹏X

  • 模块安装
$ npm i update-notifier
  • src/index.js 示例
......

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

为了本地调试,我们将本地的 CLI 降一个版本,把 package.jsonversion 修改为 0.0.9,然后运行 cook 查看效果:

o( ̄︶ ̄)o 完美!


以上详细地介绍了开发一个 CLI 的一些必备或常用的步骤。

当然,如果你只想快速开发一个CLI,就像一些领导经常说的:不要跟我说过程,我只要结果。那完全可以使用如 oclif 这些专为开发 CLI 而生的框架,开箱即用。

而我们作为程序员,对于解决方案的来龙去脉,前世今生的了解,还是需要为些付出些时间和精力的,这样可以让我们更踏实,走得更远。

好了,今天就聊到这了,再见我的朋友们!

差点忘了,附上示例的源码:https://github.com/daniel-dx/cook-cli

┏(^0^)┛ ByeBye!

哦,忘了说,我的最新文章会第一时间发布到公众号里,有兴趣的关注一下呗

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 机器人“美食家”的食谱:惊喜与惊吓齐飞

    今年,美食界将迎来一位特殊的作家--IBM的超级计算机明星"沃森"(Watson)。《跟沃森大厨学烹饪》(Cognitive Cooking with Chef...

    机器人网
  • 只会炒菜的机器人弱爆了,新型机器人大厨无所不能

    厨房里会自动炒菜的锅也叫机器人?那真是弱爆了。最近,英国机器人公司Moley Robotics推出了其全新的机器人厨师原型,该机器人由智能手机控制,能够根据已经...

    机器人网
  • 食物图片变菜谱:这篇CVPR论文让人人都可以学习新料理

    喜欢研究吃的人经常会在看到美味食物甚至食物图片时垂涎不已,甚至千方百计想弄明白怎么才能做出这道美食。

    磐创AI
  • MIT创业项目「智能美食餐厅」4月底登陆波士顿自由之路

    △ 纽约法餐主厨Daniel Boulud,旗下的料理餐厅Cafe Boulud为米其林一星餐厅

    量子位
  • 食物图片变菜谱:这篇CVPR论文让人人都可以学习新料理

    喜欢研究吃的人经常会在看到美味食物甚至食物图片时垂涎不已,甚至千方百计想弄明白怎么才能做出这道美食。

    机器之心
  • Facebook最新研究:逆烹饪!从食物照片倒推食谱

    通过一张简单的食物照片你能看到什么?当时和你一起吃饭的人?用餐的那个餐馆播放的爵士乐?或者是怀念那一口美味的,自己却做不出吃不到的家乡菜?

    大数据文摘
  • 做饭、洗碗一把揽,这个机器人还可以复制妈妈的“味道”!

    整理 | 麻粒儿 网址 | 51aistar.com 精进后的机器人Moley回来了。 那么,Moley是谁? 这是一款高度自动化的机器人手臂,搭配整套智能厨房...

    企鹅号小编
  • [译]比菜谱更胜一筹:计算机能让健康饮食变得更加美味可口

    大数据文摘
  • June Oven智能烤箱:帮你轻松DIY大餐的智能硬件

    镁客网
  • AI炒菜、配料、开发新口味……人类终于可以只负责吃了?

    在深圳举行的第22届中国国际高新技术成果交易会上,煲仔饭机器人、汉堡机器人、五谷豆浆机器人、棉花糖机器人、冰淇淋机器人等多款智能餐饮机器人云集,引起参展民众争相...

    用户2908108
  • 孙正义准备砸7.5亿美元投资的披萨机器人,竟然只会抹番茄酱放进烤箱?

    打住,现在的烹饪机器人可没那么美好。比如说,软银就准备投一家用机器人做披萨的公司,名叫Zume,他们的机器人是这样做披萨的:

    量子位
  • 重学 Java 设计模式:实战命令模式「模拟高档餐厅八大菜系,小二点单厨师烹饪场景」

    初学编程往往都很懵,几乎在学习的过程中会遇到各种各样的问题,哪怕别人那运行好好的代码,但你照着写完就报错。但好在你坚持住了,否则你可能看不到这篇文章。时间和成长...

    小傅哥
  • HeRM's :一个命令行食谱管理器

    烹饪让爱变得可见,不是吗?确实!烹饪也许是你的热情或爱好或职业,我相信你会维护一份烹饪日记。保持写烹饪日记是改善烹饪习惯的一种方法。有很多方法可以记录食谱。你可...

    用户8639654
  • 人工智能进军餐饮:AI调酒,越喝越有

    人类文明的发展,食物和烹饪的意义重大。从食用熟食、耕作农作物,到加入调料、丰富烹饪方式、发明冷藏等,一些列的饮食工具和手段的发明,都是为了提高生活水平。

    AI科技大本营
  • 母亲节除了扫地机器人,还有什么?

    自从五月的第二个周日被定为母亲节,在母亲节当天为辛劳一年的母亲送上礼物就成为了一个比较流行的做法。我们在淘宝指数搜索“母亲节”后可以发现,这个关键词最近一周的搜...

    机器人网
  • 前微软CTO使用Mathematica探索现代烹饪中的科学

    WolframChina
  • 理解SAP Leonardo并不难

    前面写过一篇关于Leonardo的简介《SAP Leonardo了解一下》,但只是限于理论上的说法一样,并不通俗易懂,到底什么是SAP Leonardo,今天用...

    matinal
  • 浅谈可视化设计-数据时代的美味“烹饪师”(上篇)

    还记得大学学设计的时候学院里流传了一句话:“有百分之八十的设计师都幻想着成为一名厨师。”

    HT for Web
  • MIT毕业生团队开设首个机器人厨房Spyce

    “这是创造廉价又健康食品的解决方案。我们只是想弄清楚如何以一种全新的方式烹饪,”Kale Rogers表示,他和工程学毕业生Braden Knight,Luke...

    AiTechYun

扫码关注云+社区

领取腾讯云代金券