HHVM/Hack : hh-clilibでCLIアプリケーション開発

[HHVM/Hack : Building your own Micro Framework](https://qiita.com/ytake/items/ebaad85e50c84d3c303a)では、
Webアプリケーション開発に重きを置いた内容をお届けしました。
実際の開発ではWebアプリケーション以外に
CLIアプリケーションを開発しなければならないことも多くあります。
HackでCLIアプリケーションを開発する場合はどうしたらいいのでしょうか。

# hh-clilib
そんな方にオススメなのが[facebook/hh-clilib](https://github.com/hhvm/hh-clilib) です。
これはFacebookから提供されているHack製のCLI系のライブラリは、
全てこれをベースに開発されていますので、十分商用などにも利用できます。

PHPでCLIといえばSymfonyの[The Console Component](https://symfony.com/doc/current/components/console.html)が定番ですが、
hh-clilibは、symfony/consoleとはまた違ったHack専用の実装になっています。
今回は利用法を紹介します。

## Hello World
まずは定番のHello Worldを出力してみましょう。
CLIアプリケーションの基底となるクラスは`Facebook\CLILib\CLIBase`クラスです。
これを継承してアプリケーションクラスを実装します。
アプリケーション実行時に起動するのは`mainAsync`メソッドになりますので、
Overrideして実装します。 
*HackでOverrideするときはメソッドに`<<__Override>>`と書きます
コンソールへの出力は、getStdoutメソッドから出力に渡します。
このメソッドのインスタンスは、
以前のエントリで紹介した`HH\Lib\Experimental\IO\WriteHandle`になりますので、
使い方は同じです。

```hack:src/Command/Application.php
<?hh // strict

namespace Acme\Sample\Command;

use type Facebook\CLILib\CLIBase;
use namespace Facebook\CLILib\CLIOptions;

class Application extends CLIBase {

  <<__Override>>
  public async function mainAsync(): Awaitable<int> {
    $stdout = $this->getStdout();
    await $stdout->writeAsync("Hello, world!");
    await $stdout->flushAsync();
    return 0;
  }

  <<__Override>>
  protected function getSupportedOptions(): vec<CLIOptions\CLIOption> {
    return vec[];
  }
}
```

アプリケーションクラスができたら、エントリポイントを用意します。
ここではファイル名を`boot`として作成します。

```hack:boot
#!/usr/bin/env hhvm
<?hh // strict

require_once(__DIR__.'/vendor/hh_autoload.hh');

use type Acme\Sample\Command\Application;

<<__Entrypoint>>
function main(): void {
  Application::runAsync();
}
```

ここで下記のコマンドで実行してみましょう。

```bash
$ hhvm ./boot
Hello, world!
```

無事に出力されたら完了です。
非常に簡単に利用することができます。
続いて引数などを利用してみましょう。

## オプションにflagを使ってみよう
オプションを利用する場合は、getSupportedOptionsメソッドをOverrideします。
CLIOptionsをvec配列げ返却すれば如何様にもできます。
先ほどのhello worldに追加してみましょう。

```hack
<?hh // strict

namespace Acme\Sample\Command;

use type Facebook\CLILib\CLIBase;
use namespace Facebook\CLILib\CLIOptions;

class Application extends CLIBase {

  private int $increment = 1;

  <<__Override>>
  public async function mainAsync(): Awaitable<int> {
    $stdout = $this->getStdout();
    await $stdout->writeAsync("Hello, world!" . $this->increment);
    await $stdout->flushAsync();
    return 0;
  }

  <<__Override>>
  protected function getSupportedOptions(): vec<CLIOptions\CLIOption> {
    return vec[
	    CLIOptions\flag(
        () ==> { 
          $this->increment++;
        },
        "Increment value",
        '--increment',
        '-i',
      ),
    ];
  }
}
```

実行すると下記のようになります。

```bash
$ hhvm ./boot
Hello, world!1

$ hhvm ./boot -i
Hello, world!1
```

## オプションにEnumを使う
オプションにEnumを使いたい、というケースは多いかもしれません。
hh-clilibではEnumでオプションを作ることもできます。
例えば下記の様なEnumを扱う場合はFacebook\CLILib\CLIOptions\with_required_enum関数を
getSupportedOptionsメソッドのvecで返却する様に記述します。

```hack
enum OutputFormat: string {
  MARKDOWN = 'markdown';
  HTML = 'html';
}
```

追加すると下記の通りになります。

```hack
  <<__Override>>
  protected function getSupportedOptions(): vec<CLIOptions\CLIOption> {
    return vec[
        CLIOptions\flag(
        () ==> { $this->increment++; },
        "Increment value",
        '--increment',
        '-i',
      ),
      CLIOptions\with_required_enum(
        OutputFormat::class,
        $f ==> { $this->format = $f; },
        Str\format(
          "Desired output format (%s). Default: %s",
          Str\join(OutputFormat::getValues(), '|'),
          (string) $this->format,
        ),
        '--format',
        '-f',
      ),
    ];
  }
```

with_required_enumの第一引数は、Enumを文字列で渡します。
EnumはGenericsなどで以下の様に記述されていますので、
Enumのクラス名以外は利用することができません。Hackならではと云えます。

```hack
type enumname<T> = classname<\HH\BuiltinEnum<T>>;
```

実行時は下記の様にEnumに記載されたオプションを使わなければなりません。

```bash
$ hhvm ./boot -f html
```

オプションにEnumを利用するもの以外に、オプションの値に文字列を指定する関数も用意されています。
*CLIOptions\with_required_string関数

## 引数必須
ここまで`Facebook\CLILib\CLIBase`クラスを継承した例を紹介しましたが、
CLIアプリケーションで引数を必須したい場合は`Facebook\CLILib\CLIWithRequiredArguments`クラスを継承する必要があります。
このクラス自身は`Facebook\CLILib\CLIBase`クラスを継承したものですので、
大きな差はありません。

引数必須にする場合は、
getHelpTextForRequiredArgumentsメソッドを用意しなければなりません。
このメソッドは引数必須指定ではありますが、
CLI実行時に`-h` を与えて実行すると現れるヘルプ画面に、
getHelpTextForRequiredArgumentsメソッドで返却されるvecの中身が表示されます。
下記の通りです。

```hack
class Application extends CLIWithRequiredArguments {

  private int $increment = 1;
  private OutputFormat $format = OutputFormat::HTML;

  public static function getHelpTextForRequiredArguments(): vec<string> {
    return vec['hello!'];
  }
  // 省略
}
```

実行時のイメージは下記のものです。

```bash
$ hhvm ./boot -h
Usage: ./boot [OPTIONS] hello! [hello! ...]

Options:
  -i, --increment
	Increment value
  -f VALUE, --format=VALUE
	Desired output format (markdown|html). Default: html
  -h, --help
	display this text and exit
```

ヘルプに表示されるだけなので`hello!`を引数に与える必要はありませんが、
引数を指定していないことでCLI実行時にエラーが発生する様になります。

```bash
$ hhvm ./boot 
hello! must be specified.
```

引数指定のクラスを継承した場合は、引数へのアクセスは`$this->getArguments()`で行えます。
これと[Hack Factory Pattern Explained](https://qiita.com/ytake/items/f9c827019961700b5b2f)で紹介した例と、
[Hackで作る簡単サービスロケーター / Generics](https://qiita.com/ytake/items/2d779552e55c8bc61920)
を組み合わせることで簡単なCLIアプリケーションフレームワークを作ることもできます。
雛形ライクに用意する場合は次の様にするといいかもしれません。

### AbstractCommandクラス
このクラスを継承することで、様々な振る舞いを提供できる様にします。
インターフェースでも構いませんが、ここではコンスタラクタに制約を設けたいため抽象クラスを利用します。

```hack
<?hh // strict

namespace Acme\Sample\Command;

use namespace HH\Lib\Experimental\IO;

<<__ConsistentConstruct>>
abstract class AbstractCommand {

  public function __construct(
    protected IO\WriteHandle $write
  ) {}

  abstract public function runAsync(): Awaitable<void>;
}
```

### PhpCommandクラス
この例ではこのクラスしか作りませんが、
引数に合わせて実行内容が変わるサンプルのクラスです
AbstractCommandクラスを継承します。

```hack
<?hh // strict

namespace Acme\Sample\Command;

use namespace HH\Lib\Experimental\IO;

class PhpCommand extends AbstractCommand {

  public async function runAsync(): Awaitable<void> {
    $write = $this->write;
    await $write->writeAsync('php');
    await $write->flushAsync();
  }
}
```

### CLIアプリケーションクラス
次のEnumを用意します。

```hack
enum OutputFormat: string {
  PHP = 'php';
}
```

ここで指定した値以外は利用できない様にするためのものです。
クラスのプロパティとして、下記のものも記述します。

```hack
  private ImmMap<OutputFormat, classname<AbstractCommand>> $m = ImmMap{
    OutputFormat::PHP => PhpCommand::class,
  };
```

phpで取得したクラスは、AbstractCommandクラスを継承している必要があり、
かつコンストラクタでHH\Lib\Experimental\IO\WriteHandleクラスを指定しなければならない、
という制約があるもののみを利用できる様に制限する、ということになります。
これを利用することで、mainAsyncメソッドの内部でファクトリとして実行させることができる様になります。

```hack
  public static function getHelpTextForRequiredArguments(): vec<string> {
    return vec['command'];
  }

  <<__Override>>
  public async function mainAsync(): Awaitable<int> {
    $command = C\first($this->getArguments());
    if(OutputFormat::isValid($command)) {
      $commandClass = $this->m->at(OutputFormat::assert($command));
      await new $commandClass($this->getStdout())|> $$->runAsync();
      return 0;
    }
    $this->getStdout()->rawWriteBlocking('lol');
    return 1;
  }
```

下記の部分が、制限付きのコンストラクタをもつクラスのインスタンス生成を行なっている部分ですが、
上記の記述方法でなければインスタンス生成方法が保証できないためTypecheckerに怒られるパターンとなってしまいますので、
こういったパターンを取り入れたい場合は、PHPと同じ様に記述するだけでは実行ができませんので、
注意しておきましょう。

```hack
await new $commandClass($this->getStdout())|> $$->runAsync();
```

こうすると、下記のコマンド以外では実行できない様に実装することができます。

```hack
$ hhvm ./boot php
php
```

違う引数を与えると、Enumに記述されていないため `lol`が返却される、ということになります。
アプリケーションクラス全体のコードは下記の通りです。

```hack
<?hh // strict

namespace Acme\Sample\Command;

use namespace HH\Lib\C;
use type Facebook\CLILib\CLIBase;
use type Facebook\CLILib\CLIWithRequiredArguments;
use namespace Facebook\CLILib\CLIOptions;

enum OutputFormat: string {
  PHP = 'php';
}

class Application extends CLIWithRequiredArguments {

  private ImmMap<OutputFormat, classname<AbstractCommand>> $m = ImmMap{
    OutputFormat::PHP => PhpCommand::class,
  };

  public static function getHelpTextForRequiredArguments(): vec<string> {
    return vec['command'];
  }

  <<__Override>>
  public async function mainAsync(): Awaitable<int> {
    $command = C\first($this->getArguments());
    if(OutputFormat::isValid($command)) {
      $commandClass = $this->m->at(OutputFormat::assert($command));
      await new $commandClass($this->getStdout())|> $$->runAsync();
      return 0;
    }
    $this->getStdout()->rawWriteBlocking('lol');
    return 1;
  }

  <<__Override>>
  protected function getSupportedOptions(): vec<CLIOptions\CLIOption> {
    return vec[];
  }
}
```

hh-clilibは今回紹介したもの以外に、REPLを実装することもできますので
是非実装してみてください。