真香!原來 CLI 開發可以這麼簡單
- 2021 年 9 月 17 日
- 筆記
CLI(命令行工具,Command Line Interface)大家都非常熟悉了,比如 create-react-app 等。我們今天介紹一個 CLI 工具的開發框架,可以幫助我們快速構建 CLI 工具。
oclif(發音為 ‘oh-cliff’) 是一個命令行工具開發框架,功能豐富,開發方便。同時 oclif 還支援通過 TypeScript 來開發,對於習慣使用 TypeScript 的同學來說非常友好。
基本用法
oclif 提供兩種運行模式,一種是單一命令模式,類似於 curl,通過各種參數使用不同的功能。另一種是多命令模式,類似於 git,可以定義子命令來實現不同的功能。
下面的兩個樣例分別展示了單一命令模式和多命令模式的使用方法.
$ npx oclif single mynewcli
? npm package name (mynewcli): mynewcli
$ cd mynewcli
$ ./bin/run
hello world from ./src/index.js!
單命令模式下,會在 src
目錄下生成一個 index.{ts,js}
文件,我們在這個文件里定義命令。
$ npx oclif multi mynewcli
? npm package name (mynewcli): mynewcli
$ cd mynewcli
$ ./bin/run --version
mynewcli/0.0.0 darwin-x64 node-v9.5.0
$ ./bin/run --help
USAGE
$ mynewcli [COMMAND]
COMMANDS
hello
help display help for mynewcli
$ ./bin/run hello
hello world from ./src/hello.js!
多命令模式下,會在 src
目錄下生成一個 commands
目錄,這個目錄下的每一個文件就是一個子命令。比如 ./src/commands/hello.ts
、./src/commands/goodbye.ts
。
注意,多命令模式下命令和文件之間一個隱式的對應關係,比如 src/commands
目錄下的文件是子命令。如果 src/commands
下是一個目錄,則目錄下的多個文件會形成一個 Topic
。
加入有如下目錄結構:
package.json
src/
└── commands/
└── config/
├── index.ts
├── set.ts
└── get.ts
那麼,命令最終的執行形式為: mynewcli config
,mynewcli config:set
和 mynewcli config:get
。
定義命令
不管是單一命令模式還是多命令模式,開發者只需要定義一個 class
繼承 Command
類即可。
import Command from '@oclif/command'
export class MyCommand extends Command {
static description = 'description of this example command'
async run() {
console.log('running my command')
}
}
如上,在命令運行地時候,會自動執行 run
方法。
Command
還提供了很多工具方法,比如 this.log
、this.warn
、this.error
、this.exit
等,方便在運行過程中列印日誌資訊。
命令行工具通常都需要定義一些參數,oclif 支援兩種參數定義形式,一種是 argument
,用於定義有順序要求的參數,一種是 flag
,用於定義沒有順序要求的參數。
定義 argument
argument 的使用如下:
$ mycli firstArg secondArg # 參數順序不能亂
我們可以這樣定義 argument 參數:
import Command from '@oclif/command'
export class MyCLI extends Command {
static args = [
{name: 'firstArg'},
{name: 'secondArg'},
]
async run() {
// 通過對象的形式獲取參數
const { args } = this.parse(MyCLI)
console.log(`running my command with args: ${args.firstArg}, ${args.secondArg}`)
// 也可以通過數組的形式獲取參數
const { argv } = this.parse(MyCLI)
console.log(`running my command with args: ${argv[0]}, ${argv[1]}`)
}
}
我們可以對 argument 參數進行屬性定義:
static args = [
{
name: 'file', // 參數名稱,之後通過 argv[name] 的形式獲取參數
required: false, // 是否必填
description: 'output file', // 參數描述
hidden: true, // 是否從命令的 help 資訊中隱藏
parse: input => 'output', // 參數處理函數,可以改變用戶輸入的值
default: 'world', // 參數默認值
options: ['a', 'b'], // 參數的可選範圍
}
]
定義 flag
flag 的使用形式如下:
$ mycli --force --file=./myfile
我們可以這樣定義 flag 參數:
import Command, {flags} from '@oclif/command'
export class MyCLI extends Command {
static flags = {
// 可以通過 --force 或 -f 來指定參數
force: flags.boolean({char: 'f'}),
file: flags.string(),
}
async run() {
const {flags} = this.parse(MyCLI)
if (flags.force) console.log('--force is set')
if (flags.file) console.log(`--file is: ${flags.file}`)
}
}
我們可以對 flag 參數進行屬性定義:
static flags = {
name: flags.string({
char: 'n', // 參數短名稱
description: 'name to print', // 參數描述
hidden: false, // 是否從 help 資訊中隱藏
multiple: false, // 是否支援對這個參數設置多個值
env: 'MY_NAME', // 默認值使用的環境變數的名稱
options: ['a', 'b'], // 可選值列表
parse: input => 'output', // 對用戶輸入進行處理
default: 'world', // 默認值,也可以是一個返回字元串的函數
required: false, // 是否必填
dependsOn: ['extra-flag'], // 依賴的其他 flag 參數列表
exclusive: ['extra-flag'], // 不能一起使用的其他 flag 參數列表
}),
// 布爾值參數
force: flags.boolean({
char: 'f',
default: true, // 默認值,可以是一個返回布爾值的函數
}),
}
使用生命周期鉤子
oclif 提供了一些生命周期鉤子,可以讓開發者在工具運行的各個階段進行一些額外操作。
我們可以這樣定義一個鉤子函數:
import { Hook } from '@oclif/config'
export default const hook: Hook<'init'> = async function (options) {
console.log(`example init hook running before ${options.id}`)
}
同時,還需要在 package.json
中註冊這個鉤子函數:
"oclif": {
"commands": "./lib/commands",
"hooks": {
"init": "./lib/hooks/init/example"
}
}
oclif 還支援定義多個鉤子函數,多個鉤子函數會並行運行:
"oclif": {
"commands": "./lib/commands",
"hooks": {
"init": [
"./lib/hooks/init/example",
"./lib/hooks/init/another_hook"
]
}
}
目前支援的生命周期鉤子如下:
init
– 在 CLI 完成初始化之後,找到對應命令之前。prerun
– 在init
完成,並找到對應命令之後,但是在命令運行之前。postrun
– 在命令運行結束之後,並沒有錯誤發生。command_not_found
– 沒有找到對應命令,在展示錯誤資訊之前。
使用插件
oclif 官方和社區提供了很多有用的插件可以供新開發的命令行工具使用,只需要在 package.json
中聲明即可。
{
"name": "mycli",
"version": "0.0.0",
// ...
"oclif": {
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-not-found"
]
}
}
可用的插件有:
- @oclif/plugin-not-found 當未找到命令的時候提供一個友好的 “did you mean” 資訊。
- @oclif/plugin-plugins 允許用戶給你的命令行工具添加插件。
- @oclif/plugin-update 自動更新插件。
- @oclif/plugin-help 幫助資訊插件。
- @oclif/plugin-warn-if-update-available 當有可用更新時,展示一個警告資訊提示更新。
- @oclif/plugin-autocomplete 提供 bash/zsh 的自動補全。
錯誤處理
命令行運行難免會出錯,oclif 提供了兩種錯誤處理的方法。
Command.catch
每個 Command
實例都有一個 catch
方法,開發者可以在這個方法中處理錯誤。
import {Command, flags} from '@oclif/command'
export default class Hello extends Command {
async catch(error) {
// do something or
// re-throw to be handled globally
throw error;
}
}
bin/run
的 catch
bin/run
是每個 oclif 命令行工具的入口文件,我們可以通過 bin/run
的 catch
方法抓取錯誤,包括 Command
中重新拋出的錯誤。
.catch(require('@oclif/errors/handle'))
//或
.catch((error) => {
const oclifHandler = require('@oclif/errors/handle');
// do any extra work with error
return oclifHandler(error);
})
其他功能
cli-ux
oclif 官方維護的 cli-ux 庫提供了許多使用的功能。
- 通過
cliux.prompt()
函數可以實現簡單的交互功能。如果有更複雜的交互需求,可以使用 inquirer。 - 通過
cliux.action
可以實現旋轉 loading 效果。 - 通過
cliux.table
可以展示表格數據。
node-notifier
通過 node-notifier 可以實現跨平台的通知資訊展示。
常見面試知識點、技術解決方案、教程,都可以掃碼關注公眾號「眾里千尋」獲取,或者來這裡 //everfind.github.io 。