寫在最前,其實真想寫一寫食譜來着,苦於烹飪能力有限,因此標題就是個謊話,哈哈^_~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!