airframe-launcherとsbt-packでScalaでCLIツールを作る

私は普段主にScalaを使っているので、ちょっと手の込んだ処理が必要だったりJava/Scalaライブラリを使ったツールが必要な場合にScalaで書けると便利だなと思うことがあります。

AirframeはScala用のDIコンテナを中心とした様々な機能を提供するライブラリ群ですが、そのうちの1つとしてairframe-launcherというCLIツール作成用のモジュールがあり、コマンドラインオプションを簡単に扱うことができます。また、sbt-packというsbtプラグインを使うと作成したアプリケーションを実行可能なコマンドとしてパッケージング、インストールできます。これらを組み合わせてScalaCLIツールを作る方法を紹介したいと思います。

wvlet.org

github.com

まずはbuild.sbtに以下の記述を追加し、airframe-launcherを依存関係に追加します。

libraryDependencies ++= Seq(
  "org.wvlet.airframe"  %% "airframe-launcher" % "19.11.1"
)

手始めにメッセージを表示するだけの簡単なCLIツールを作成してみます。コマンドラインオプションは@optionアノテーション、サブコマンドは@commandアノテーションで指定します。実行時にコマンドラインオプションでサブコマンドが指定されていない場合、isDefault = trueが指定されたコマンドが実行されます。

package com.github.takezoe

import wvlet.airframe.launcher.{Launcher, command, option}

class Hello(@option(prefix = "-n,--name", description = "your name")
            name: String,
            @option(prefix = "-h,--help", description = "show help message", isHelp = true)
            displayHelp: Boolean) {

  @command(isDefault = true)
  def default(): Unit = {
    println(s"Hello ${name}!")
  }
}

object Main extends App {
  Launcher.execute[Hello](args)
}

これをsbt-packでパッケージングします。plugins.sbtに以下の記述を追加します。

addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.11")

build.sbtにパッケージングの設定を追加する必要があります。PackPluginを有効にし、コマンドと起動クラスのマッピングを行ないます。

enablePlugins(PackPlugin)
packMain := Map("hello" -> "com.github.takezoe.Main")

sbt packtarget/packディレクトリに必要なファイル一式が生成されます。また、sbt packInstall$HOME/local/binディレクトリに実行可能なコマンドがインストールされるのでこのディレクトリにPATHを通しておけば普通のコマンドと同じように使用できます。

インストールしたコマンドを実行してみます。

$ hello -n Naoki
Hello Naoki!

続いてScalaコードを修正してサブコマンドを追加してみます。Helloクラスに以下のメソッドを追加します。サブコマンド固有のオプションも定義できます。

@command(description = "display a message repeatedly")
def repeat(@option(prefix = "-c,--count", description = "repeat count")
           count: Int): Unit = {
  for(_ <- 0 to count){
    println(s"Hello ${name}!")
  }
}

以下のように実行します。

$ hello repeat -n Naoki -c 3
Hello Naoki!
Hello Naoki!
Hello Naoki!

airframe-launcherにはヘルプを自動生成する機能もあります。isHelp = trueが指定されたオプションを指定して実行すると以下のようなヘルプが表示されます。

$ hello --help
usage: hello [options] <command name>

[options]
 -n, --name:[NAME]  your name
 -h, --help         show help message

[commands]
 repeat         display a message repeatedly

JVMの起動速度という問題はあるものの、CLIツールでもそこそこ大きなものであれば既存のJVM資産が活用できタイプセーフかつ記述能力の高いScalaで書くメリットもあるかと思います。こういった用途にもScalaの活用の幅が広がればと思います。