dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Node.js製CLIフレームワークoclifを試す

はじめに

Heroku が oclif という CLI フレームワークオープンソースとして公開したという記事を読みました。

Heroku CLISalesforce DX のベースにもなっているらしい。
どんなもんか触ってみます。

(oclif は (The) Open CLI Framework の略のようです。読み方がわからない。。。)


oclif の特徴

手を動かす前に、どういった特徴があるのか公式ドキュメントに目を通してみます。
Features · oclif: The Open CLI Framework

  • Super Speed
    • コマンド実行時のオーバーヘッド(?)がほとんどなく、また依存パッケージもほとんどない
    • 実行されるコマンドだけ require されるので、たくさんのコマンドからなる巨大 CLI でも単一コマンドの CLI と速度が変わらない
  • 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 とは lscurl のように、コマンド自体は1つで引数やオプションを取るものです。
Multi-command とは githeroku のように、後にサブコマンドが続くものです。


試してみる

今回は、自分が過去に 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',
}),

のように optionsdefault を利用しました。便利。

# -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 のエンジニアブログにも記事があった。