写在最前,其实真想写一写食谱来着,苦于烹饪能力有限,因此标题就是个谎话,哈哈^_~javascript
今天我们就来聊一聊命令行工具(即 CLI:command-line interface,如下都会以 CLI 来代替冗长的命令行工具名词 )的开发。java
阅读完本文,你会对从头至尾开发一个 CLI 有一个较全面的认识。node
你也能够收藏下这篇文章,当你想开发一个 CLI 时,回来翻一翻,总会找到你想要的。git
丹尼尔:花生可乐准备好了,坐等开始。程序员
好勒,这就开始,Let's go! <( ̄︶ ̄)↗[GO!]github
建立一个空项目目录(接下来都是以 cook-cli
来做例子的,因此这里咱们命名为 cook-cli
),而后在该目录下敲打命令进行初始化,过程以下:shell
$ mkdir cook-cli
$ cd cook-cli
$ npm init --yes
复制代码
经过 npm init
命令,会将该目录初始化为一个 Node.js
项目,它会在 cook-cli
目录下生成 package.json
文件。npm
加 --yes
会自动回答初始化过程当中提问的全部问题,你能够试着将该参数去掉,本身一个一个问题进行回答。json
项目已初始完毕,接下来咱们添加骨架代码,让 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
规范加载模块,即直接使用 import
和 export
。上面 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 涉及到的一些术语做简短的介绍:
# cook 即为命令
$ cook
# start 即为 cook 的 子命令
$ cook start
复制代码
# -V 为简写模式(short flag)的选项(注意:只能一个字母,多个字母表明多个选项)
$ cook -V
# --version 为全写模式(long name)的选项
$ cook --version
复制代码
# source.js 和 target.js 都为 cp 命令的参数
$ cp source.js target.js
复制代码
其实,子命令也是命令的参数
Ok,从以上的介绍来看,咱们要实现一个 CLI,对入参(包括 subcommand, options, argument)的解析是逃不掉的,那咱们就直面它们吧。
commander:嘿,兄弟,别怕,有我呢!
是的,兄弟,有你真好。接下来咱们经过使用 commander
这个模块来解析入参,过程和示例以下:
$ npm i commander
复制代码
......
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.version
和 usage
的链式调用就搞定了,仍是那么的冷酷。
试着运行如下命令:
$ 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
复制代码
......
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
复制代码
最多见的场景是:文本输入,是否选项,复选,单选。例子以下:
......
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);
}
复制代码
代码浅显,直接上效果图吧:
人机交互体验很重要,若是不能立刻完成的工做,就须要及时反馈用户当前工做的进度,这样能够减小用户的等待焦虑感。
ora
和listr
肩并着肩,迈着整齐的步伐,迎面而来。
首先上场的是 ora
$ npm i ora
复制代码
......
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
复制代码
......
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
复制代码
.....
import chalk from 'chalk';
export function cli(args) {
console.log(chalk.yellow('I like cooking'));
.....
}
复制代码
有了色彩的 CLI,是否是让你心情更加愉悦:
boxen
:这个是个人拿手好戏,看个人!<(ˉ^ˉ)>
$ npm i boxen
复制代码
......
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 了,如今你能够到 www.npmjs.com/ 去查询下你发布的 CLI 了。
update-notifier:终于到我了,我等到花儿已谢了。 X﹏X
$ npm i update-notifier
复制代码
......
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.json
的 version
修改成 0.0.9
,而后运行 cook
查看效果:
o( ̄︶ ̄)o 完美!
以上详细地介绍了开发一个 CLI 的一些必备或经常使用的步骤。
固然,若是你只想快速开发一个CLI,就像一些领导常常说的:不要跟我说过程,我只要结果。那彻底可使用如 oclif
这些专为开发 CLI 而生的框架,开箱即用。
而咱们做为程序员,对于解决方案的前因后果,前世此生的了解,仍是须要为些付出些时间和精力的,这样可让咱们更踏实,走得更远。
好了,今天就聊到这了,再见个人朋友们!
差点忘了,附上示例的源码:github.com/daniel-dx/c…
┏(^0^)┛ ByeBye!