はじめに
Heroku が oclif という CLI フレームワークをオープンソースとして公開したという記事を読みました。
Heroku CLI や Salesforce DX のベースにもなっているらしい。
どんなもんか触ってみます。
(oclif は (The) Open CLI Framework の略のようです。読み方がわからない。。。)
oclif の特徴
手を動かす前に、どういった特徴があるのか公式ドキュメントに目を通してみます。
Features · oclif: The Open CLI Framework
- Super Speed
- CLI Generator
- コマンド一発で scaffold が生成できる generator がある
- Testing Helpers
- テストが書きやすい。 stdout/stderr を簡単にモックできる
- generator がテストの scaffold も自動生成する
- Auto-documentation
--help
オプションで表示するヘルプテキストが自動生成される- ↑は CLI が publish されるときに README にも自動的に記載される(※最後で軽く触れる)
- (未確認)Plugins
- (未確認)Hooks
- コマンド実行時などのライフサイクルイベントや独自にカスタムイベントを定義し、そこにフックする処理を書ける
- TypeScript
- TypeScript と JavaScript 両方をサポート
- oclif 自体も TypeScript で書かれている
- Coming soon: man pages, Autocomplete
テストしやすく、ドキュメントが自動生成されるのはいいですね。
Single-command と Multi-command
oclif で作成できる CLI には大きく分けて 2 種類あります。
Single-command とは ls
や curl
のように、コマンド自体は1つで引数やオプションを取るものです。
Multi-command とは git
や heroku
のように、後にサブコマンドが続くものです。
試してみる
今回は、自分が過去に Node.js で作った parse-salesforce-object という CLI に oclif を導入してみます。
(Salesforce 開発で使うメタデータファイル(XML)をパースしてよしなに表示してくれるという、ごくごく一部の方にしか需要がないやつです)
この CLI は先ほどの分類で言うと Single-command です。
generator で CLI のひな形(scaffold)作成
Quickstart を参考に、generator を使って必要なファイルを生成します。
$ npx oclif single [コマンド名]
を実行すると途中で色々聞かれるので適宜入力します。
$ npx oclif single parse-salesforce-object npx: 293個のパッケージを17.261秒でインストールしました。 _-----_ ╭──────────────────────────╮ | | │ Time to build a │ |--(o)--| │ single-command CLI with │ `---------´ │ oclif! Version: 1.7.9 │ ( _´U`_ ) ╰──────────────────────────╯ /___A___\ / | ~ | __'.___.'__ ´ ` |° ´ Y ` ? npm package name parse-salesforce-object ? command bin name the CLI will export parse-salesforce-object ? description ? author Shingo Yamazaki @zaki-yama ? version 0.0.5 ? license MIT ? node version supported >=8.0.0 ? github owner of repository (https://github.com/OWNER/repo) zaki-yama ? github name of repository (https://github.com/owner/REPO) parse-salesforce-object ? optional components to include ❯◉ yarn (npm alternative) ◉ mocha (testing framework) ◉ typescript (static typing for javascript) ◉ tslint (static analysis tool for typescript) ◯ semantic-release (automated version management)
最後のオプションで yarn, mocha, TypeScript を使うかどうかは好みです。
インストールが完了すると、 [コマンド名]
でディレクトリが作成され、その下に必要なファイルが揃っています。
npm install
(or yarn
)も実行されているため、必要なパッケージもインストール済みです。
$ tree -I node_modules parse-salesforce-object parse-salesforce-object ├── README.md ├── appveyor.yml ├── bin │ ├── run │ └── run.cmd ├── package.json ├── src │ └── index.ts ├── test │ ├── helpers │ │ └── init.js │ ├── index.test.ts │ ├── mocha.opts │ └── tsconfig.json ├── tsconfig.json ├── tslint.json └── yarn.lock
bin/run
を実行するとコマンドを実行できます。
$ cd parse-salesforce-object
$ ./bin/run
hello world from /Users/yamazaki/workspace/nodejs/parse-salesforce-object/src/index.ts!
コマンドファイルの構成
src/index.ts がコマンド本体です。中身を見てみます。
((1) ~ (3) は便宜的にこちらで番号を振りました)
import {Command, flags} from '@oclif/command' class ParseSalesforceObject extends Command { // (3) static description = 'describe the command here' // (3) static examples = [ `$ parse-salesforce-object hello world from ./src/parse-salesforce-object.ts! `, ] // (2) static flags = { // add --version flag to show CLI version version: flags.version({char: 'v'}), // add --help flag to show CLI version help: flags.help({char: 'h'}), // flag with a value (-n, --name=VALUE) name: flags.string({char: 'n', description: 'name to print'}), force: flags.boolean({char: 'f'}), } // (2) static args = [{name: 'file'}] // (1) async run() { const {args, flags} = this.parse(ParseSalesforceObject) const name = flags.name || 'world' this.log(`hello ${name} from ${__filename}!`) if (args.file && flags.force) { this.log(`you input --force and --file: ${args.file}`) } } } export = ParseSalesforceObject
- (1) コマンドの実処理は
run()
メソッドに記述します - (2) 引数やオプション(flags)はこのように
static
変数として定義します。この後ここをカスタマイズしてみます - (3) description, examples も同様に
static
変数として定義すると、ヘルプテキストに反映されます
最後の (3) について、実際にコマンドを --help
オプションつきで実行すると
$ ./bin/run --help describe the command here USAGE $ parse-salesforce-object [FILE] OPTIONS -f, --force -h, --help show CLI help -n, --name=name name to print -v, --version show CLI version EXAMPLE $ parse-salesforce-object hello world from ./src/parse-salesforce-object.ts!
のように、description および examples に記述した文字列がヘルプの先頭と EXAMPLE セクションに記載されているのがわかります。
引数を処理する
さて、ここからひな形をベースに元の CLI としての機能を実装していきます。
まずは引数の処理から。
参考:Command Arguments · oclif: The Open CLI Framework
元の CLI では
const argv = require('minimist')(process.argv.slice(2)); const filePath = argv._[0]; ... if (!filePath) { console.log(chalk.red('ERROR: You must specify a path to .object file.')); process.exit(1); } fs.readFile(filePath, (err, data) => { ... });
のように、とあるファイルへのパスを必須の引数として受け取るようになっていました。
またその処理のために minimist というライブラリを使っていました。
oclif だと以下のように書けます。
class ParseSalesforceObject extends Command { ... static args = [{ name: 'path', description: 'path to .object file', required: true, }] async run() { const {args, flags} = this.parse(ParseSalesforceObject) fs.readFile(args.path, (err, data) => { ... }) }
引数には { name: 'foo' }
という形で名前を付けておくことができ、run()
メソッド内で(パース後に) args.foo
でアクセスできます。
その他のオプションは https://oclif.io/docs/args.html を参照するといいです。
必須かどうかもオプションで指定できるようになったので判定処理が不要になりました。
# 引数なしで実行するとエラーになる $ ./bin/run › Error: Missing 1 required arg: › path path to .object file › See more help with --help
フラグ(オプション)を処理する
続いて、いくつかのフラグを受け取れるようにします。
フラグとは -f foo
や --file=foo
のようなものを指します。
参考:Command Flags · oclif: The Open CLI Framework
フラグは version と help 以外は
static flags = { force: flags.boolean({char: 'f'}), file: flags.string(), }
のように、
- 引数を受け取るもの:
flags.string()
- 引数を受け取らず、boolean として使うもの:
flags.boolean()
の 2 種類あります。
また両者に共通して、{char: 'f'}
のように char
オプションを指定すると短縮形も扱えるようになります。
その他のオプションは https://oclif.io/docs/flags.html を参照します。
元の CLI には、-f xxx
または --format=xxx
オプションで出力フォーマットを指定でき、その選択肢は markdown, csv, soql
のいずれかとなっていたので
format: flags.string({ char: 'f', description: 'output format', options: ['markdown', 'csv', 'soql'], default: 'markdown', }),
のように options
と default
を利用しました。便利。
# -f で許可されているフォーマット以外を指定するとエラー
$ ./bin/run objects/Expense__c.object -f foo
› Error: Expected --format=foo to be one of: markdown, csv, soql
› See more help with --help
ヘルプを出力してみる
引数やオプションを一通り定義した後で、 --help
によりヘルプを表示してみます。
$ ./bin/run --help USAGE $ parse-salesforce-object PATH ARGUMENTS PATH path to .object file OPTIONS -f, --format=markdown|csv|soql [default: markdown] output format -h, --help show CLI help -n, --namespace=namespace namespace prefix (for SOQL format) -v, --version show CLI version EXAMPLE $ parse-salesforce-object src/objects/Expense__c.object | label | fullName | type | required | | ----------- | ------------- | -------- | -------- | | Amount | Amount__c | Number | false | | Client | Client__c | Text | false | | Date | Date__c | DateTime | false | | Reimbursed? | Reimbursed__c | Checkbox | null |
ARGUMENTS
および OPTIONS
のセクションのところが、定義した引数・フラグの内容から自動生成されました。
GitHub
今回 oclif での置き換えを試した CLI のリポジトリはここにあります。
PR は https://github.com/zaki-yama/parse-salesforce-object/pull/6
まとめと TODO
今回は oclif の導入手順と基本構成についてなんとなくわかったという程度ですが、個人的には
npx oclif single/multi foo
で scaffold から始められるのは楽- 引数やオプションからヘルプ自動生成は便利
といった点が、フレームワークというだけあって良いなと思いました。
また将来的に Autocomplete もサポートしてくれるのは期待したい。
テストを書くところや Plugins、Hooks については試せてないので、今後の TODO ということで。
おまけ:Auto-documentation について
Features#Auto-documentation には
This information is also automatically placed in the README whenever the npm package of the CLI is published. See the multi-command CLI example
と記載がありますが、自動生成したヘルプを README に埋め込む方法はドキュメントに記載がありませんでした。
リンクされてるリポジトリを見ると oclif-dev
という CLI を使ってる っぽいので、これかな...?
あわせて読みたい
Salesforce のエンジニアブログにも記事があった。