真香!原来 CLI 开发可以这么简单
CLI(命令行工具,Command Line Interface)大家都非常熟悉了,比如 create-react-app 等。我們今天介紹一個(gè) CLI 工具的開發(fā)框架,可以幫助我們快速構(gòu)建 CLI 工具。
oclif(發(fā)音為 'oh-cliff') 是一個(gè)命令行工具開發(fā)框架,功能豐富,開發(fā)方便。同時(shí) oclif 還支持通過 TypeScript 來開發(fā),對于習(xí)慣使用 TypeScript 的同學(xué)來說非常友好。
基本用法
oclif 提供兩種運(yùn)行模式,一種是單一命令模式,類似于 curl,通過各種參數(shù)使用不同的功能。另一種是多命令模式,類似于 git,可以定義子命令來實(shí)現(xiàn)不同的功能。
下面的兩個(gè)樣例分別展示了單一命令模式和多命令模式的使用方法.
$ npx oclif single mynewcli ? npm package name (mynewcli): mynewcli $ cd mynewcli $ ./bin/run hello world from ./src/index.js!單命令模式下,會(huì)在?src?目錄下生成一個(gè)?index.{ts,js}?文件,我們在這個(gè)文件里定義命令。
$ 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]COMMANDShellohelp display help for mynewcli$ ./bin/run hello hello world from ./src/hello.js!多命令模式下,會(huì)在?src?目錄下生成一個(gè)?commands?目錄,這個(gè)目錄下的每一個(gè)文件就是一個(gè)子命令。比如?./src/commands/hello.ts、./src/commands/goodbye.ts。
注意,多命令模式下命令和文件之間一個(gè)隱式的對應(yīng)關(guān)系,比如?src/commands?目錄下的文件是子命令。如果?src/commands?下是一個(gè)目錄,則目錄下的多個(gè)文件會(huì)形成一個(gè)?Topic。
加入有如下目錄結(jié)構(gòu):
package.json src/ └── commands/└── config/├── index.ts├── set.ts└── get.ts那么,命令最終的執(zhí)行形式為:?mynewcli config,mynewcli config:set?和?mynewcli config:get。
定義命令
不管是單一命令模式還是多命令模式,開發(fā)者只需要定義一個(gè)?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')} }如上,在命令運(yùn)行地時(shí)候,會(huì)自動(dòng)執(zhí)行?run?方法。
Command?還提供了很多工具方法,比如?this.log、this.warn、this.error、this.exit?等,方便在運(yùn)行過程中打印日志信息。
命令行工具通常都需要定義一些參數(shù),oclif 支持兩種參數(shù)定義形式,一種是?argument,用于定義有順序要求的參數(shù),一種是?flag,用于定義沒有順序要求的參數(shù)。
定義 argument
argument 的使用如下:
$ mycli firstArg secondArg # 參數(shù)順序不能亂我們可以這樣定義 argument 參數(shù):
import Command from '@oclif/command'export class MyCLI extends Command {static args = [{name: 'firstArg'},{name: 'secondArg'},]async run() {// 通過對象的形式獲取參數(shù)const { args } = this.parse(MyCLI)console.log(`running my command with args: ${args.firstArg}, ${args.secondArg}`)// 也可以通過數(shù)組的形式獲取參數(shù)const { argv } = this.parse(MyCLI)console.log(`running my command with args: ${argv[0]}, ${argv[1]}`)} }我們可以對 argument 參數(shù)進(jìn)行屬性定義:
static args = [{name: 'file', // 參數(shù)名稱,之后通過 argv[name] 的形式獲取參數(shù)required: false, // 是否必填description: 'output file', // 參數(shù)描述hidden: true, // 是否從命令的 help 信息中隱藏parse: input => 'output', // 參數(shù)處理函數(shù),可以改變用戶輸入的值default: 'world', // 參數(shù)默認(rèn)值options: ['a', 'b'], // 參數(shù)的可選范圍} ]定義 flag
flag 的使用形式如下:
$ mycli --force --file=./myfile我們可以這樣定義 flag 參數(shù):
import Command, {flags} from '@oclif/command'export class MyCLI extends Command {static flags = {// 可以通過 --force 或 -f 來指定參數(shù)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 參數(shù)進(jìn)行屬性定義:
static flags = {name: flags.string({char: 'n', // 參數(shù)短名稱description: 'name to print', // 參數(shù)描述hidden: false, // 是否從 help 信息中隱藏multiple: false, // 是否支持對這個(gè)參數(shù)設(shè)置多個(gè)值env: 'MY_NAME', // 默認(rèn)值使用的環(huán)境變量的名稱options: ['a', 'b'], // 可選值列表parse: input => 'output', // 對用戶輸入進(jìn)行處理default: 'world', // 默認(rèn)值,也可以是一個(gè)返回字符串的函數(shù)required: false, // 是否必填dependsOn: ['extra-flag'], // 依賴的其他 flag 參數(shù)列表exclusive: ['extra-flag'], // 不能一起使用的其他 flag 參數(shù)列表}),// 布爾值參數(shù)force: flags.boolean({char: 'f',default: true, // 默認(rèn)值,可以是一個(gè)返回布爾值的函數(shù)}), }使用生命周期鉤子
oclif 提供了一些生命周期鉤子,可以讓開發(fā)者在工具運(yùn)行的各個(gè)階段進(jìn)行一些額外操作。
我們可以這樣定義一個(gè)鉤子函數(shù):
import { Hook } from '@oclif/config'export default const hook: Hook<'init'> = async function (options) {console.log(`example init hook running before ${options.id}`) }同時(shí),還需要在?package.json?中注冊這個(gè)鉤子函數(shù):
"oclif": {"commands": "./lib/commands","hooks": {"init": "./lib/hooks/init/example"} }oclif 還支持定義多個(gè)鉤子函數(shù),多個(gè)鉤子函數(shù)會(huì)并行運(yùn)行:
"oclif": {"commands": "./lib/commands","hooks": {"init": ["./lib/hooks/init/example","./lib/hooks/init/another_hook"]} }目前支持的生命周期鉤子如下:
- init?- 在 CLI 完成初始化之后,找到對應(yīng)命令之前。
- prerun?- 在?init?完成,并找到對應(yīng)命令之后,但是在命令運(yùn)行之前。
- postrun?- 在命令運(yùn)行結(jié)束之后,并沒有錯(cuò)誤發(fā)生。
- command_not_found?- 沒有找到對應(yīng)命令,在展示錯(cuò)誤信息之前。
使用插件
oclif 官方和社區(qū)提供了很多有用的插件可以供新開發(fā)的命令行工具使用,只需要在?package.json?中聲明即可。
{"name": "mycli","version": "0.0.0",// ..."oclif": {"plugins": ["@oclif/plugin-help","@oclif/plugin-not-found"]} }可用的插件有:
- @oclif/plugin-not-found?當(dāng)未找到命令的時(shí)候提供一個(gè)友好的 "did you mean" 信息。
- @oclif/plugin-plugins?允許用戶給你的命令行工具添加插件。
- @oclif/plugin-update?自動(dòng)更新插件。
- @oclif/plugin-help?幫助信息插件。
- @oclif/plugin-warn-if-update-available?當(dāng)有可用更新時(shí),展示一個(gè)警告信息提示更新。
- @oclif/plugin-autocomplete?提供 bash/zsh 的自動(dòng)補(bǔ)全。
錯(cuò)誤處理
命令行運(yùn)行難免會(huì)出錯(cuò),oclif 提供了兩種錯(cuò)誤處理的方法。
Command.catch
每個(gè)?Command?實(shí)例都有一個(gè)?catch?方法,開發(fā)者可以在這個(gè)方法中處理錯(cuò)誤。
import {Command, flags} from '@oclif/command'export default class Hello extends Command {async catch(error) {// do something or// re-throw to be handled globallythrow error;} }bin/run?的?catch
bin/run?是每個(gè) oclif 命令行工具的入口文件,我們可以通過?bin/run?的?catch?方法抓取錯(cuò)誤,包括?Command?中重新拋出的錯(cuò)誤。
.catch(require('@oclif/errors/handle'))//或.catch((error) => {const oclifHandler = require('@oclif/errors/handle');// do any extra work with errorreturn oclifHandler(error); })其他功能
cli-ux
oclif 官方維護(hù)的?cli-ux?庫提供了許多使用的功能。
- 通過?cliux.prompt()?函數(shù)可以實(shí)現(xiàn)簡單的交互功能。如果有更復(fù)雜的交互需求,可以使用?inquirer。
- 通過?cliux.action?可以實(shí)現(xiàn)旋轉(zhuǎn) loading 效果。
- 通過?cliux.table?可以展示表格數(shù)據(jù)。
總結(jié)
以上是生活随笔為你收集整理的真香!原来 CLI 开发可以这么简单的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: lombok时运行编译无法找到get/s
- 下一篇: ECMAScript 2021(ES12