kynthus / unixista

コマンドライン引数解析用ライブラリ

Version Matrix

Unixista

UNIXコマンド風の引数解析ができるライブラリです。

core-maven argparse-maven util-maven

core-scaladoc argparse-scaladoc util-scaladoc
core-scaladoc argparse-scaladoc util-scaladoc
core-scaladoc argparse-scaladoc util-scaladoc
core-scaladoc argparse-scaladoc util-scaladoc

目次

前提条件

本ライブラリは以下に示すバージョンのScala言語でサポートされます。

  • Scala 2.10.52.10.7
  • Scala 2.11.02.11.12
  • Scala 2.12.02.12.12
  • Scala 2.13.02.13.3

※2020年12月時点

Scala 2.10系ではマクロパラダイスコンパイラプラグインが追加で必要です。
プラグインの導入方法はリンク先をご覧ください。

Scala 2.10.4以前および2.13.4以降はサポート対象外となっています。
2.10.4以前はメソッドへ特定の型の引数が指定できず、2.10.5以降に比べて機能が大幅に制限されます。
また、2.13.4以降はサブコマンドを含むヘルプメッセージを表示する際、実行時例外が発生します。
サポート対象外となるバージョンのScalaでは、本ライブラリを使用しないでください。

導入方法

例えば、本ライブラリをSBT環境にて導入する場合はbuild.sbtへ以下の1行を追加します。

libraryDependencies += "com.github.kynthus" %% "unixista-util" % "1.0.0"

この他にも各種ビルドツールに対応した導入方法が用意されています。
詳しくはMavenリポジトリをご覧ください。

使用例

次のコードは複数の整数値を受け取り、合計値または最大値のどちらかを表示するScalaプログラムです。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object Prog {

  def main(args: Array[String]): Unit = {

    val max: Seq[Int] => Int = _.max
    val sum: Seq[Int] => Int = _.sum

    val parsed = OParser.parseBuilder
      .initial("integers"   ->> Seq.empty[Int])
      .initial("accumulate" ->> max)
      .arg("N [N...]", classOf[Int])
      .action((acc, cur) => acc.updateWith("integers")(_ :+ cur))
      .required
      .unbounded
      .text("An integer for the accumulator.")
      .opt("sum")
      .set(_.replace("accumulate", sum))
      .text("Sum the integers (default: find the max).")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val accumulate: Seq[Int] => Int = result("accumulate")
    val integers  : Seq[Int]        = result("integers")

    val answer: Int = accumulate(integers)
    println(answer)

  }

}

適切な引数を指定した場合、上記プログラムはコマンドライン引数の整数値のうち最大のものを表示します。
ただし、--sumオプションが指定されると代わりに合計値を表示します。

$ scala Prog.jar 1 2 3 4
4

$ scala Prog.jar 1 2 3 4 --sum
10

不正な引数を指定した場合はエラーメッセージを表示し、プログラムが終了します。

$ scala Prog.jar a b c
Error: Argument N [N...] expects a number but was given 'a'
Error: Argument N [N...] expects a number but was given 'b'
Error: Argument N [N...] expects a number but was given 'c'
Usage:  [options] N [N...]

  N [N...]  An integer for the accumulator.
  --sum     Sum the integers (default: find the max).

以降、一通りの使用方法を説明していきます。

パーサの作成

コマンドライン引数の解析にあたり、はじめにパーサの構築用オブジェクトを作成します。

メソッド名 機能概要
parseBuilder パーサの構築用オブジェクトを作成する
import org.kynthus.unixista._

OParser.parseBuilder // 構築用オブジェクトを作成

構築用オブジェクトに対して、引数解析に必要な情報を追加していきます。

初期値の追加

次に必要なのは、引数を解析した結果を格納する先です。
initialを使用することで、キー・値のペアを追加することができます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

val max: Seq[Int] => Int = _.max // 整数のシーケンスのなかから最大値を返す関数
val sum: Seq[Int] => Int = _.sum // 整数のシーケンスの合計値を返す関数

OParser.parseBuilder
  .initial("integers"   ->> Seq.empty[Int]) // "integers"を追加(デフォルト:空の整数シーケンス)
  .initial("accumulate" ->> max)            // "accumulate"を追加(デフォルト:最大値を返す関数)

キー"integers"に対しては空の整数値のシーケンスを、
キー"accumulate"に対しては整数値のシーケンスのなかから最大値を返す関数を初期値として設定しています。

引数の解析方法の定義

一通りの初期値を定義し終えた後は、構築用オブジェクトに対して引数の情報を与えます。
コマンドライン引数の文字列をキー・値のペアへ落とし込む方法を定義できます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

val max: Seq[Int] => Int = _.max
val sum: Seq[Int] => Int = _.sum

OParser.parseBuilder
  .initial("integers"   ->> Seq.empty[Int])
  .initial("accumulate" ->> max)
  .arg("N [N...]", classOf[Int])                              // 整数値(Int)の位置引数
  .action((acc, cur) => acc.updateWith("integers")(_ :+ cur)) // 指定されるたびに"integers"の整数シーケンスへ追加される
  .required                                                   // 少なくとも1個以上は整数値の指定が必要
  .unbounded                                                  // 指定数の上限は無制限
  .text("An integer for the accumulator.")                    // 位置引数の説明文(ヘルプ表示で使われる)
  .opt("sum")                                                 // 合計値フラグのオプション引数
  .set(_.replace("accumulate", sum))                          // "accumulate"を合計値を算出する関数へと更新する
  .text("Sum the integers (default: find the max).")          // 合計値フラグの説明文

上記の解析方法の定義より、"integers"キーは1つ以上の整数のシーケンスで、
"accumulate"キーはコマンドライン引数から--sumが指定された場合は合計値を算出する関数に、
それ以外の場合はデフォルトの最大値をわり出す関数になります。

引数の解析を実行

引数の解析は、構築用オブジェクトに対してrunを呼び出した際に行われます。
各引数を正しい型へ変換し、キー・値のペアを格納したオブジェクトを構築します。

メソッド名 機能概要
run 引数の解析を行う
import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object Prog {

  def main(args: Array[String]): Unit = {

    val max: Seq[Int] => Int = _.max
    val sum: Seq[Int] => Int = _.sum

    val parsed = OParser.parseBuilder
      .initial("integers"   ->> Seq.empty[Int])
      .initial("accumulate" ->> max)
      .arg("N [N...]", classOf[Int])
      .action((acc, cur) => acc.updateWith("integers")(_ :+ cur))
      .required
      .unbounded
      .text("An integer for the accumulator.")
      .opt("sum")
      .set(_.replace("accumulate", sum))
      .text("Sum the integers (default: find the max).")
      .args(args) // 解析するコマンドライン引数の指定
      .run        // 解析の実行(結果がparsedへ格納される)

  }

}

ほとんどの場合、argsの引数にはmainが受け取るコマンドライン引数のargs配列を使用しますが、
コマンドライン引数として扱う値を、プログラム内から直接指定することもできます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

val max: Seq[Int] => Int = _.max
val sum: Seq[Int] => Int = _.sum

val parsed = OParser.parseBuilder
  .initial("integers"   ->> Seq.empty[Int])
  .initial("accumulate" ->> max)
  .arg("N [N...]", classOf[Int])
  .action((acc, cur) => acc.updateWith("integers")(_ :+ cur))
  .required
  .unbounded
  .text("An integer for the accumulator.")
  .opt("sum")
  .set(_.replace("accumulate", sum))
  .text("Sum the integers (default: find the max).")
  .args("--sum") // 引数の直接指定
  .args(7)
  .args(1)
  .args(42)
  .run

scala> println(parsed)
Some(List(7, 1, 42) :: <function1> :: HNil)

基本機能

パーサの構築時に利用できる基本的な機能について説明します。

引数解析の初歩

引数の解析における指定値やオプションの扱い、ヘルプ文字列の表示可否を設定可能です。
それを定義するのが、以下に示すメソッドです。

メソッド名 機能概要
programName プログラム名を設定する(デフォルト:なし)
head 引数のヘルプの前にテキストを追加する(デフォルト:なし)
note 任意の場所で表示可能なテキストを追加する(デフォルト:なし)
arg 位置引数を追加する
opt オプション引数を追加する
help ヘルプ表示用のオプションを追加する
version バージョン表示用のオプションを追加する

以下の節では、各メソッドの利用方法を説明します。

programName

デフォルトでは、ヘルプメッセージ中にはオプションの指定方法のみが表示されますが、
これでは何のコマンドのオプションに関するヘルプか分かりづらいでしょう。

コマンドやJARファイルの名称をプログラム名として表示する方が、いくらか明瞭になります。
例えば、MyProgramという名前のメインクラスに次のコードがあるとします。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .initial("foo" ->> "")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("foo help")
      .help("help")
      .args(args)
      .run

  }

}

このプログラムのヘルプは、プログラム名を特に表示しません。

$ scala MyProgram.jar --help
Usage:  [options]

  --foo <value>  foo help
  --help

何のプログラムを起動したことによりヘルプが表示されているか分かりやすくするよう、
programNameへプログラム名を引数で渡します。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar") // プログラム名の指定
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("foo help")
      .help("help")
      .args(args)
      .run

  }

}
$ scala MyProgram.jar --help
Usage: MyProgram.jar [options]

  --foo <value>  foo help
  --help

head

このメソッドでは、プログラムの目的や動作に関する説明文を設定できます。
ヘルプメッセージのタイトルとして、最上部に表示されます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .initial("foo" ->> "")
      .head("A foo that bars") // 最上部に表示される
      .help("help")
      .args(args)
      .run

  }

}
$ scala MyProgram.jar --help
A foo that bars
Usage:  [options]

  --help

複数の文字列を指定する場合は、Seqで包んだ文字列のシーケンスをアンパックして渡します。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .initial("foo" ->> "")
      .head(Seq("A foo that bars", "Bars is not baz"): _*) // スペース繋ぎで複数の文字列を表示
      .help("help")
      .args(args)
      .run

  }

}
$ scala MyProgram.jar --help
A foo that bars Bars is not baz
Usage:  [options]

  --help

一般的に、タイトルへはプログラムのバージョン情報を含めることを推奨します。

note

ヘルプメッセージのなかで、任意の場所へテキストを追加したい場合もあります。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .note("After usage")         // usageの直後に表示
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("foo help")
      .note("Help message option") // --helpオプションの直前に表示
      .help("help")
      .note("Tail text")           // 末尾に表示
      .args(args)
      .run

  }

}

noteは呼び出したタイミングに応じてテキストを表示する位置を自由に調節可能です。

$ scala MyProgram.jar --help
Usage: MyProgram.jar [options]

After usage
  --foo <value>  foo help
Help message option
  --help
Tail text

arg

argはコマンドの位置引数を追加します。
位置引数は接頭語のハイフン(-)なしで指定されたものです。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

val parsed = OParser.parseBuilder
  .initial("foo" ->> "")
  .initial("bar" ->> "")
  .arg("foo", _ ("foo")) // 1個目の引数を"foo"へマッピング
  .action(_.replace("foo", _))
  .arg("bar", _ ("bar")) // 2個目の引数は"bar"へマッピング
  .action(_.replace("bar", _))
  .args(Array("fooValue", "barValue"))
  .run

val result = parsed.getOrElse(sys.exit(1))

val foo: String = result("foo")
val bar: String = result("bar")

scala> println(foo)
fooValue

scala> println(bar)
barValue

コマンドラインより指定された1個目の引数をfoo、2個目の引数をbarとして扱います。

opt

optはオプション付きの引数を追加します。
こちらは接頭語のハイフン(-)付きで指定されたものです。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

val parsed = OParser.parseBuilder
  .initial("foo" ->> "")
  .initial("bar" ->> "")
  .opt("foo", _ ("foo")) // "--foo"オプションに指定された値を"foo"へ
  .action(_.replace("foo", _))
  .opt("bar", _ ("bar")) // "--bar"オプションに指定された値は"bar"へ
  .action(_.replace("bar", _))
  .args(Array("--bar", "barValue", "--foo", "fooValue"))
  .run

val result = parsed.getOrElse(sys.exit(1))

val foo: String = result("foo")
val bar: String = result("bar")

scala> println(foo)
fooValue

scala> println(bar)
barValue

位置引数とは異なり、順番によらずオプションに指定した値をマッピング可能です。

help

慣例として、ヘルプメッセージを表示するオプションには--helpおよび短縮系として-hが多用されます。
場合によっては、これ以外の文字列をヘルプ用として使いたいこともあります。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("foo help")
      .help("myhelp") // ヘルプ表示用のオプションをmyhelpとする
      .args(args)
      .run

  }

}
$ scala MyProgram.jar --myhelp
Usage: MyProgram.jar [options]

  --foo <value>  foo help
  --myhelp

オプションがmyhelpとなったことで、--help指定ではエラー扱いとなります。

$ scala MyProgram.jar --help
Error: Unknown option --help
Try --myhelp for more information.

version

versionはその名の通り、プログラムのバージョン情報のみを表示する際に使用します。
引数に指定したオプションが含まれる場合、headで指定したメッセージのみを表示してプログラムが終了します。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .head(Seq("MyProgram", "v1.0.0"): _*)
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("foo help")
      .version("version")
      .args(args)
      .run

  }

}
$ scala MyProgram.jar --version
MyProgram v1.0.0

個々の引数に対する設定

個々のコマンドライン引数に対して詳細な設定が可能です。

メソッド名 機能概要
abbr オプションの短縮名を指定する
action オプションが指定されたときに実行する処理を定義する
text ヘルプ上での引数に対する説明文を指定する
required 必須オプション扱いとする
optional 任意オプション扱いとする
unbounded 引数の個数の上限を無制限にする
hidden 隠しオプション扱いとする
keyName ヘルプ上でキーをどのように表示するかを指定する(デフォルト:"<key>"
valueName ヘルプ上で値をどのように表示するかを指定する(デフォルト:"<value>"
keyValueName キーと値の表示方法をまとめて指定する
validate オプションに対する値チェックを行う

以下の節では、各メソッドの利用方法を説明します。

abbr

オプション名には、短縮形を定義することができます。
長いオプションは二重のハイフン(--)で始まり、短いオプションは単一のハイフン(-)で始まります。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .abbr("f") // "--foo"を"-f"でも指定可能とする
      .action(_.replace("foo", _))
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    println(s"foo = $foo")

  }

}

上記プログラムで"foo"へ値をマッピングする場合、--foo-fのどちらでも可能です。

$ scala MyProgram.jar --foo fooValue
foo = fooValue

$ scala MyProgram.jar -f fooValue
foo = fooValue

action

このメソッドはこれまでにも登場していますが、コマンドライン引数に対して実行する処理を割り当てます。
基本的にどんな処理も定義できますが、ほとんどの場合は単にキー・値のマッピングを行うだけです。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _)) // 引数に受け取った値をマッピング
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    println(s"foo = $foo")

  }

}

受け取った値を必要としない、単純なフラグ引数の場合はエイリアスメソッドのsetが利用できます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> false)
      .programName("MyProgram.jar")
      .opt("foo")
      .set(_.replace("foo", true)) // "--foo"が指定されたときtrueとなる
      .text("foo help")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: Boolean = result("foo")
    println(s"foo = $foo")

  }

}

text

引数の簡潔な説明を含むメッセージを指定します。
ヘルプを表示した際に、指定した説明文が表示されるようになります。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("foo value") // ヘルプへこの説明文が表示される
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    println(s"foo = $foo")

  }

}
$ scala MyProgram.jar --help
Usage: MyProgram.jar [options]

  --foo <value>  foo value
  --help

required

必須オプションとして扱います。デフォルトはこのモードです。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .required // 必須オプションとする
      .text("foo value")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    println(s"foo = $foo")

  }

}

上記プログラムはfooオプションが必須であるため、指定がない場合はエラー扱いとなります。

$ scala MyProgram.jar
Error: Missing option --foo
Try --help for more information.

optional

任意オプションとして扱います。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .optional // 任意オプションとする
      .text("foo value")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    println(s"foo = $foo")

  }

}

--fooを指定しなかった場合、"foo"の値はデフォルトの空文字("")のままです。

$ scala MyProgram.jar --foo fooValue
foo = fooValue

$ scala MyProgram.jar
foo =

unbounded

引数を繰り返し指定可能にします。例えば位置引数を複数指定できるようにする場合に有効です。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> Seq.empty[String])
      .programName("MyProgram.jar")
      .arg("foo", classOf[String])
      .action((hl, st) => hl.updateWith("foo")(_ :+ st))
      .unbounded // 無制限に繰り返し指定可能とする
      .text("foo value")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: Seq[String] = result("foo")
    println(s"foo = $foo")

  }

}

位置引数が順にシーケンスへ蓄積し、最終的には指定した値が全てマッピングされます。

$ scala MyProgram.jar A B C
foo = List(A, B, C)

hidden

hiddenを使用すると、ユーザからは見えない隠しオプションを定義できます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .hidden // 隠しオプション化する
      .text("foo value")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    println(s"foo = $foo")

  }

}

隠しオプションは指定自体は可能ですが、ヘルプメッセージには表示されません。

$ scala MyProgram.jar --foo fooValue
foo = fooValue

$ scala MyProgram.jar --foo fooValue
Usage: MyProgram.jar [options]

  --help

keyNameおよびvalueName

引数の種類にMapを指定した際、ヘルプへ表示されるキー・値の名称を指定します。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("mapA" ->> Map.empty[String, String])
      .initial("mapB" ->> Map.empty[String, String])
      .programName("MyProgram.jar")
      .opt("mapA", classOf[(String, String)])
      .action((hl, kv) => hl.updateWith("mapA")(_ + kv))
      .keyName("myKeyA")     // キーの表示方法を指定
      .valueName("myValueA") // 値の表示方法を指定
      .text("mapA pairs")
      .opt("mapB", classOf[(String, String)])
      .action((hl, kv) => hl.updateWith("mapB")(_ + kv))
      .keyValueName("myKeyB", "myValueB") // キーと値の表示方法をまとめて指定
      .text("mapB pairs")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val mapA: Map[String, String] = result("mapA")
    val mapB: Map[String, String] = result("mapB")

    println(s"mapA = $mapA")
    println(s"mapB = $mapB")

  }

}
$ scala MyProgram.jar --help
Usage: MyProgram.jar [options]

  --mapA:myKeyA=myValueA  mapA pairs
  --mapB:myKeyB=myValueB  mapB pairs
  --help

validate

引数に指定する値に特定の制約をかけたい場合もあります。
validateは各位置引数やオプション引数にマッピングされる値をチェックし、
条件を満たさない値をエラー扱いとすることができます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> "")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .validate { s =>
        val choices: Array[String] = Array("rock", "paper", "scissors")
        if (choices.contains(s)) { // "rock", "paper", "scissors"のいずれかであればOK
          Right(())
        } else {                   // それ以外の場合は不正値でエラー扱い
          Left(s"argument move: invalid choice: $s (choose from [rock, paper, scissors])")
        }
      }
      .text("foo value")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    println(s"foo = $foo")

  }

}

--fooオプションに渡された値がrock, paper, scissors以外の場合は不正となり、
プログラムはエラーメッセージを表示したのちに終了します。

$ scala MyProgram.jar --foo rock
foo = rock

$ scala MyProgram.jar --foo fire
Error: argument move: invalid choice: fire (choose from [rock, paper, scissors])
Try --help for more information.

キー・値の生成に関する詳細

初期値を設定するinitialには、引数として受け取ることのできる型が複数存在します。

メソッド名 機能概要
initial 初期値を格納するため、キー・値のペアを追加する

主に、以下の表へ示す型をサポートしています。

具体的な値
単一のレコード(キー・値のペア) "name" ->> "Tom"
1個目の型がWitnessTuple2 "name".witness -> "Tom"
レコードを持つshapeless.HList "name" ->> "Tom" :: "age" ->> 16 :: HNil
Unit ()
shapeless.HNil HNil
ケースクラス Student(name = "Tom", age = 16, address = "Los Angeles")
タプル ("Tom", 16, "Los Angeles")

initialへケースクラスを渡す例を示します。
値をマッピングする際は、キーの指定時にSymbolを使用してください。

import org.kynthus.unixista._

case class Student(name: String, age: Int, address: String)

val parsed = OParser.parseBuilder
  .initial(Student(name = "Tom", age = 16, address = "Los Angeles"))
  .opt("name", _ (Symbol("name")))
  .action(_.replace(Symbol("name"), _))
  .optional
  .opt("age", _ (Symbol("age")))
  .action(_.replace(Symbol("age"), _))
  .optional
  .opt("address", _ (Symbol("address")))
  .action(_.replace(Symbol("address"), _))
  .optional
  .args("--address")
  .args("San Francisco")
  .run

val result = parsed.getOrElse(sys.exit(1))

val name   : String = result(Symbol("name"))
val age    : Int    = result(Symbol("age"))
val address: String = result(Symbol("address"))

scala> println(s"$name $age $address")
Tom 16 San Francisco

Unitおよびshapeless.HNilを指定した場合は、何もフィールドを持ちません。
値のマッピングはせず、単純にコマンドライン引数の解析のみ行う場合に便利です。

コマンドライン引数の指定に関する詳細

コマンドライン引数を追加するargsの引数には、Stringの配列以外にも様々な型を受け取れます。

メソッド名 機能概要
args 解析対象のコマンドライン引数を追加する

以下は一例ですが、argsが受け取ることのできる型を表にまとめたものです。

具体的な値 コマンドライン引数への追加方法
Unit () 何も追加されない
Boolean true 文字列化した結果を追加
Char 'あ' 文字列化した結果を追加
Byte 100.asInstanceOf[Byte] 文字列化した結果を追加
Short 10000.asInstanceOf[Short] 文字列化した結果を追加
Int 10000000 文字列化した結果を追加
Long 100000000000L 文字列化した結果を追加
Float 777.777F 文字列化した結果を追加
Double 1111.1111 文字列化した結果を追加
String "Hello" 文字列をそのまま追加
配列 Array('A', 'B', 'C') 各要素を文字列化し順に追加
Javaのコレクション全般 java.util.Arrays.asList(1, 2, 3) 各要素を文字列化し順に追加
Scalaのコレクション全般 List('D', 'E', 'F') 各要素を文字列化し順に追加
Duration 5.seconds 時間の値と単位を文字列化し順に追加
ケースクラス Student("Tom", 16, "Los Angeles") フィールドの値を文字列化し順に追加
タプル ("Tom", 16, "Los Angeles") タプルの各要素を文字列化し順に追加

それぞれの型を渡した際に、どのような扱いとなるかを示したのが以下のプログラムです。

import org.kynthus.unixista._
import scala.concurrent.duration.DurationInt

case class Student(name: String, age: Int, address: String)

val parser = OParser.parseBuilder
  .args(())
  .args(true)
  .args('あ')
  .args(100.asInstanceOf[Byte])
  .args(10000.asInstanceOf[Short])
  .args(10000000)
  .args(100000000000L)
  .args(777.777F)
  .args(1111.1111)
  .args("Hello")
  .args(Array('A', 'B', 'C'))
  .args(java.util.Arrays.asList(1, 2, 3))
  .args(List('D', 'E', 'F'))
  .args(5.seconds)
  .args(Student("Tom", 16, "Los Angeles"))
  .args(("Ken", 18, "Boston"))

scala> println(parser)
Some(HNil :: Vector(true, あ, 100, 10000, 10000000, 100000000000, 777.777, 1111.1111, Hello, A, B, C, 1, 2, 3, D, E, F, 5, SECONDS, Tom, 16, Los Angeles, Ken, 18, Boston) :: HNil)

このほかにも、Javaのラッパークラスや列挙型をはじめ、
scalaz.Foldableの性質を持つ型やshapeless.HListなどをサポートします。

加えて、Scala 2.13系ではshapeless.labelled.FieldTypeも新たにサポートしました。

map・flatMapによる構築

パーサの構築には、メソッド呼び出しを繋げる以外にもいくつかの方法があります。

メソッド名 機能概要
map 構築用オブジェクトを受け取り、次に必要なメソッドを呼び出せる
flatMap 同上

map

メソッドの呼び出しはmapのなかでも行えます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object FunctionalProg {

  def main(args: Array[String]): Unit = {

    val max: Seq[Int] => Int = _.max
    val sum: Seq[Int] => Int = _.sum

    // mapによる連鎖
    val parsed = OParser.parseBuilder
      .map(_.initial("integers"   ->> Seq.empty[Int]))
      .map(_.initial("accumulate" ->> max))
      .map(_.arg("N [N...]", classOf[Int]))
      .map(_.action((acc, cur) => acc.updateWith("integers")(_ :+ cur)))
      .map(_.required)
      .map(_.unbounded)
      .map(_.text("An integer for the accumulator."))
      .map(_.opt("sum"))
      .map(_.set(_.replace("accumulate", sum)))
      .map(_.text("Sum the integers (default: find the max)."))
      .map(_.args(args))
      .run // runはmapの外側で呼び出さなくてはいけない

    val result = parsed.getOrElse(sys.exit(1))

    val accumulate: Seq[Int] => Int = result("accumulate")
    val integers  : Seq[Int]        = result("integers")

    val answer: Int = accumulate(integers)
    println(answer)

  }

}
$ scala FunctionalProg.jar 1 2 3 4
4

$ scala FunctionalProg.jar 1 2 3 4 --sum
10

なお、プログラム中のコメントにもあるとおりrunmapのなかで呼び出してはいけません。
かならず外側で呼び出すか、後述するflatMapを使用してください。

flatMap

map以外に、flatMapもサポートされています。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object FunctionalProg {

  def main(args: Array[String]): Unit = {

    val max: Seq[Int] => Int = _.max
    val sum: Seq[Int] => Int = _.sum

    // flatMapによる連鎖
    val parsed = OParser.parseBuilder
      .flatMap(_.initial("integers"   ->> Seq.empty[Int]))
      .flatMap(_.initial("accumulate" ->> max))
      .flatMap(_.arg("N [N...]", classOf[Int]))
      .flatMap(_.action((acc, cur) => acc.updateWith("integers")(_ :+ cur)))
      .flatMap(_.required)
      .flatMap(_.unbounded)
      .flatMap(_.text("An integer for the accumulator."))
      .flatMap(_.opt("sum"))
      .flatMap(_.set(_.replace("accumulate", sum)))
      .flatMap(_.text("Sum the integers (default: find the max)."))
      .flatMap(_.args(args))
      .flatMap(_.run) // flatMapは内側でrunを呼び出してもOK

    val result = parsed.getOrElse(sys.exit(1))

    val accumulate: Seq[Int] => Int = result("accumulate")
    val integers  : Seq[Int]        = result("integers")

    val answer: Int = accumulate(integers)
    println(answer)

  }

}
$ scala FunctionalProg.jar 1 2 3 4
4

$ scala FunctionalProg.jar 1 2 3 4 --sum
10

mapとは異なり、runをなかで呼び出しても問題ありません。

for式による構築

mapおよびflatMapをサポートしているため、パーサの構築にはfor式も利用できます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object FunctionalProg {

  def main(args: Array[String]): Unit = {

    val max: Seq[Int] => Int = _.max
    val sum: Seq[Int] => Int = _.sum

    // for式による構築
    val parsed = for {
      a <- OParser.parseBuilder
      b <- a.initial("integers"   ->> Seq.empty[Int])
      c <- b.initial("accumulate" ->> max)
      d <- c.arg("N [N...]", classOf[Int])
      e <- d.action((acc, cur) => acc.updateWith("integers")(_ :+ cur))
      f <- e.required
      g <- f.unbounded
      h <- g.text("An integer for the accumulator.")
      i <- h.opt("sum")
      j <- i.set(_.replace("accumulate", sum))
      k <- j.text("Sum the integers (default: find the max).")
      l <- k.args(args)
      m <- l.run // runはyield式ではなくfor式の最後に記述
    } yield m

    val result = parsed.getOrElse(sys.exit(1))

    val accumulate: Seq[Int] => Int = result("accumulate")
    val integers  : Seq[Int]        = result("integers")

    val answer: Int = accumulate(integers)
    println(answer)

  }

}
$ scala FunctionalProg.jar 1 2 3 4
4

$ scala FunctionalProg.jar 1 2 3 4 --sum
10

runfor式のなかで呼び出さなければなりません。
yield式のなかでrunを呼び出さないようにしてください。

各オプションやその詳細設定は多くの場合において階層構造となるため、
for式をネストすることで分かりやすくなるでしょう。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object FunctionalProg {

  def main(args: Array[String]): Unit = {

    val max: Seq[Int] => Int = _.max
    val sum: Seq[Int] => Int = _.sum

    // for式のネスト
    val parsed = for {
      pb <- OParser.parseBuilder
      in <- for {
        i1 <- pb.initial("integers"   ->> Seq.empty[Int])
        i2 <- i1.initial("accumulate" ->> max)
      } yield i2
      si <- for {
        on <- in.arg("N [N...]", classOf[Int])
        op <- for {
          o1 <- on.action((acc, cur) => acc.updateWith("integers")(_ :+ cur))
          o2 <- o1.required
          o3 <- o2.unbounded
          o4 <- o3.text("An integer for the accumulator.")
        } yield o4
      } yield op
      ac <- for {
        on <- si.opt("sum")
        op <- for {
          o1 <- on.set(_.replace("accumulate", sum))
          o2 <- o1.text("Sum the integers (default: find the max).")
        } yield o2
      } yield op
      ag <- ac.args(args)
      rn <- ag.run
    } yield rn

    val result = parsed.getOrElse(sys.exit(1))

    val accumulate: Seq[Int] => Int = result("accumulate")
    val integers  : Seq[Int]        = result("integers")

    val answer: Int = accumulate(integers)
    println(answer)

  }

}
$ scala FunctionalProg.jar 1 2 3 4
4

$ scala FunctionalProg.jar 1 2 3 4 --sum
10

高度な設定

単純なオプションの追加のほかにも、サブコマンドや相互排他の定義など細かな制御ができます。
各ケースに応じた使用例を以降の節で示します。

サブコマンド

コマンドのなかには、複数の機能をサブコマンドとして分割しているものも少なくありません。
例えばsvnコマンドはsvn checkout, svn update, svn commitなどのサブコマンドを持ちます。
cmdchildrenおよびparentを使用すると、各サブコマンドに応じた異なる引数解析を行えます。

メソッド名 機能概要
cmd サブコマンドの名称を指定する
children 1つ下の階層に子パーサを作成して分岐させる
parent 子パーサを親パーサへと合流させ、1つ上の階層へ戻る
import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("mode"      ->> "")
      .initial("longVal"   ->> 0L)
      .initial("doubleVal" ->> 0.0)
      .programName("MyProgram.jar")
      .help("help")
      .cmd("i64") // サブコマンド「i64」
      .set(_.replace("mode", "long-mode"))
      .children   // 「i64」用の子パーサへ分岐
      .arg("longVal", _ ("longVal"))
      .action(_.replace("longVal", _))
      .parent     // 1つ上の階層へ戻る
      .cmd("f64") // サブコマンド「f64」
      .set(_.replace("mode", "double-mode"))
      .children   // 「f64」用の子パーサへ分岐
      .arg("doubleVal", _ ("doubleVal"))
      .action(_.replace("doubleVal", _))
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    result("mode") match {
      case "long-mode"   => println(s"longVal   = ${result("longVal"  )}")
      case "double-mode" => println(s"doubleVal = ${result("doubleVal")}")
    }

  }

}

上記プログラムは、サブコマンドとしてi64またはf64のどちらかを受け取り、
i64の場合はLong型の整数値を、f64の場合はDouble型の実数値を表示するものです。

$ scala MyProgram.jar --help
Usage: MyProgram.jar [i64|f64] [options] <args>...

  --help
Command: i64 longVal

  longVal
Command: f64 doubleVal

  doubleVal

$ scala MyProgram.jar i64 777
longVal   = 777

$ scala MyProgram.jar f64 111.111
doubleVal = 111.111

$ scala MyProgram.jar s64 ABC
Error: Unknown argument 's64'
Error: Unknown argument 'ABC'
Try --help for more information.

解析後の値チェック

個々のオプションに対してではなく、複数のオプションをまたいで値の整合性をチェックしたい場合があります。
例えば相互排他的な2つのオプションが、どちらも指定されていた場合にエラーとして扱うようなケースです。
checkConfigを使用すると、全てのマッピングされた値同士での比較ができます。

メソッド名 機能概要
checkConfig マッピングされた値の整合性をチェックする
import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo" ->> false)
      .initial("bar" ->> true)
      .programName("MyProgram.jar")
      .opt("foo")
      .set(_.replace("foo", true))
      .opt("bar")
      .set(_.replace("bar", false))
      .checkConfig { hl =>
        if (hl("foo") && !hl("bar")) { // "--foo"と"--bar"が両方とも指定されている場合はエラー
          Left("argument --bar: not allowed with argument --foo")
        } else {
          Right(())
        }
      }
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: Boolean = result("foo")
    val bar: Boolean = result("bar")

    println(s"foo = $foo")
    println(s"bar = $bar")

  }

}

上記プログラムは、--fooおよび--barが両方とも指定されたときのみエラーとなります。

$ scala MyProgram.jar --foo
foo = true
bar = true

$ scala MyProgram.jar --bar
foo = false
bar = false

$ scala MyProgram.jar --foo --bar
Error: argument --bar: not allowed with argument --foo
Usage: MyProgram.jar [options]

  --foo
  --bar

パーサの動作設定

引数解析の際に表示されるメッセージの出力先や、不正な引数の扱いとった、
パーサの全体的な動作を設定できるメソッドも提供しています。

メソッド名 機能概要
renderingMode ヘルプ表示する際のフォーマットを指定する
errorOnUnknownArgument 定義されていない引数をエラー扱いとするか指定する
showUsageOnError エラー発生時にコマンドの使用方法を表示するか指定する
displayToOut ヘルプやバージョン表示時のメッセージ出力先を定義する
displayToErr エラー発生時のコマンド使用方法の出力先を定義する
reportError エラーメッセージを出力する方法を定義する
reportWarning 警告メッセージを出力する方法を定義する
terminate ヘルプおよびバージョン表示後に行う処理を定義する

renderingMode

ヘルプを表示する際のフォーマットを指定することができます。

  • RenderingMode.OneColumn
    各オプションを単一列で表示します。説明文は行を改めて字下げされます。
import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .renderingMode(RenderingMode.OneColumn) // 単一列フォーマット
      .initial("foo" ->> "")
      .initial("bar" ->> "")
      .arg("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("--foo string.")
      .opt("bar", _ ("bar"))
      .action(_.replace("bar", _))
      .text("--bar string.")
      .help("help")
      .args("--help")
      .run

  }

}
$ scala MyProgram.jar --help
Usage:  [options] foo

  foo
        --foo string.
  --bar <value>
        --bar string.
  --help
  • RenderingMode.TwoColumns
    各オプションの説明文は行を改めず続けて表示されます。デフォルトはこのモードです。
import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .renderingMode(RenderingMode.TwoColumns) // 二重列フォーマット
      .initial("foo" ->> "")
      .initial("bar" ->> "")
      .arg("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("--foo string.")
      .opt("bar", _ ("bar"))
      .action(_.replace("bar", _))
      .text("--bar string.")
      .help("help")
      .args("--help")
      .run

  }

}
$ scala MyProgram.jar --help
Usage:  [options] foo

  foo            --foo string.
  --bar <value>  --bar string.
  --help

errorOnUnknownArgument

ときにはコマンドライン引数の一部分だけを解析し、定義されていない引数は無視したいケースもあります。
errorOnUnknownArgumentで不正な引数を検出した際にエラー扱いとするかを指定可能です。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

val myArgs: String = "--foo fooValue --bar barValue --baz bazValue"

val parsed = OParser.parseBuilder
  .errorOnUnknownArgument(isError = false) // 不正な引数(--baz)は無視する
  .initial("foo" ->> "")
  .initial("bar" ->> "")
  .arg("foo", _ ("foo"))
  .action(_.replace("foo", _))
  .text("--foo string.")
  .opt("bar", _ ("bar"))
  .action(_.replace("bar", _))
  .text("--bar string.")
  .help("help")
  .args(myArgs.split(' '))
  .run

scala> println(parsed) // "fooValue"および"barValue"のみ格納される
Some(fooValue :: barValue :: HNil)

なお、わざわざ論理値のtruefalseを指定するよりも、
以下の2つのエイリアスメソッドを使用することを推奨します。

メソッド名 相当
errorOnUnknownArgument(引数なし) errorOnUnknownArgument(true)
permitOnUnknownArgument errorOnUnknownArgument(false)

showUsageOnError

エラー発生時のヘルプメッセージの表示有無を指定します。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .showUsageOnError(Option(true)) // エラー発生時にヘルプを表示する
      .initial("foo" ->> "")
      .initial("bar" ->> "")
      .arg("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("--foo string.")
      .opt("bar", _ ("bar"))
      .action(_.replace("bar", _))
      .text("--bar string.")
      .help("help")
      .args(args)
      .run

  }

}
$ scala MyProgram.jar --baz
Error: Unknown option --baz
Error: Missing argument foo
Usage:  [options] foo

  foo            --foo string.
  --bar <value>  --bar string.
  --help

わざわざ引数を指定せずとも、以下の2つのエイリアスメソッドで設定可能です。

メソッド名 相当
showUsageOnError(引数なし) showUsageOnError(Option(true))
hideUsageOnError errorOnUnknownArgument(Option(false))

display系およびreport系メソッド

これらのメソッドは、ヘルプ文字列やコマンド使用方法のメッセージをどう出力するかを定義します。
たとえばヘルプメッセージをファイルへ出力することも可能です。

import java.io.{BufferedWriter, FileWriter}

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    OParser.parseBuilder
      .displayToOut(new BufferedWriter(new FileWriter("/root/helplog")).append) // 「/root/helplog」へ出力
      .initial("foo" ->> "")
      .initial("bar" ->> "")
      .arg("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("--foo string.")
      .opt("bar", _ ("bar"))
      .action(_.replace("bar", _))
      .text("--bar string.")
      .help("help")
      .args(args)
      .run

  }

}

terminate

ヘルプおよびバージョン表示用のオプションを指定した際、デフォルトではsys.exitでプログラムが終了します。
terminateを定義すると、例えばヘルプ表示後もプログラムを続行するよう動作を変更できます。

import org.kynthus.unixista._
import shapeless.syntax.singleton.mkSingletonOps

object MyProgram {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .terminate(_ => println("That's all for help.")) // ヘルプ表示時も終了せず続行
      .initial("foo" ->> "fooDefault")
      .initial("bar" ->> "barDefault")
      .programName("MyProgram.jar")
      .opt("foo", _ ("foo"))
      .action(_.replace("foo", _))
      .text("--foo string.")
      .opt("bar", _ ("bar"))
      .action(_.replace("bar", _))
      .text("--bar string.")
      .help("help")
      .args(args)
      .run

    val result = parsed.getOrElse(sys.exit(1))

    val foo: String = result("foo")
    val bar: String = result("bar")

    println(s"foo = $foo")
    println(s"bar = $bar")

  }

}

上記プログラムは--help指定で呼び出しても途中で終了せず、処理を続行します。
コマンドから--fooおよび--barが指定された場合、値をマッピングし取得できます。

$ scala MyProgram.jar --foo fooValue --bar barValue --help
Usage:  [options]

  --foo <value>  --foo string.
  --bar <value>  --bar string.
  --help
That's all for help.
foo = fooValue
bar = barValue

解析結果のケースクラス化

引数を解析した結果は通常、キー・値のペアとしてマッピングされますが、
場合によっては、これらをケースクラスのメンバとして扱いたいこともあります。
runProductを使用すると結果をケースクラスへ変換して取得できます。

メソッド名 機能概要
runProduct 引数の解析を行うとともに、結果のキー・値のペアをケースクラス化する
import org.kynthus.unixista._

case class CalcIntegers(
  integers  : Seq[Int], 
  accumulate: Seq[Int] => Int
)

object ProgProduct {

  def main(args: Array[String]): Unit = {

    val sum: Seq[Int] => Int = _.sum

    val parsed: APOption[CalcIntegers] = OParser.parseBuilder
      .initial(CalcIntegers(Seq.empty, _.max))
      .arg("N [N...]", classOf[Int])
      .action((acc, cur) => acc.updateWith(Symbol("integers"))(_ :+ cur))
      .required
      .unbounded
      .text("An integer for the accumulator.")
      .opt("sum")
      .set(_.replace(Symbol("accumulate"), sum))
      .text("Sum the integers (default: find the max).")
      .args(args)
      .runProduct // 解析結果をCalcIntegerとして取得

    val result: CalcIntegers = parsed.getOrElse(sys.exit(1))

    // 結果はそれぞれフィールドへ格納される
    val accumulate: Seq[Int] => Int = result.accumulate
    val integers  : Seq[Int]        = result.integers

    val answer: Int = accumulate(integers)
    println(answer)

  }

}

これにより、定義済みのケースクラスを解析結果の格納先として活用することが可能です。

構築用オブジェクトが使用する型の切り替え

構築用オブジェクトは内部で使用する型がいくつか存在します。

  • 構築用オブジェクトをラップする型:Option
  • コマンドライン引数の要素の型:String
  • コマンドライン引数の並びを格納する型:Vector

「構築用オブジェクトをラップする型(Option)」と「コマンドライン引数の並びを格納する型(Vector)」は、
ユーザが自由に選択することが可能です。
※「コマンドライン引数の要素の型(String)」はユーザ定義型を利用することで変更可能です。

以下の節では、型の変更方法に関する例を示します。

コマンドライン引数の格納方法の変更

デフォルトでは、コマンドライン引数の並びの格納にはVectorが使用されます。

import org.kynthus.unixista._

val parser = OParser.parseBuilder
  .args("--foo")
  .args("fooValue")
  .args("--bar")
  .args("barValue")
  .args("--help")

scala> println(parser)
Some(HNil :: Vector(--foo, fooValue, --bar, barValue, --help) :: HNil)

引数の並びを格納する型を変更するには、以下のようにインポート宣言で対象の型を選択してください。

// Listを引数の並びを格納する型として選択
import org.kynthus.unixista.core.instance.ArgumentCategoryInstances.ListArgumentCategory

デフォルトで用意されているのは、以下の14種類です。

使用する型 インポート対象
Vector VectorArgumentCategory
List ListArgumentCategory
Stream StreamArgumentCategory
Option OptionArgumentCategory
scalaz.IList IListArgumentCategory
scalaz.IndSeq IndSeqArgumentCategory
scalaz.Dequeue DequeueArgumentCategory
scalaz.DList DListArgumentCategory
scalaz.NonEmptyList NonEmptyListArgumentCategory
scalaz.CorecursiveList CorecursiveListArgumentCategory
scalaz.EphemeralStream EphemeralStreamArgumentCategory
scalaz.Id.Id IdArgumentCategory
scalaz.Maybe MaybeArgumentCategory
scalaz.LazyOption LazyOptionArgumentCategory

変更する場合はorg.kynthus.unixista.util._をインポート宣言として記述します。
以下は引数の並びを格納する型をStreamへ変更した例です。

import org.kynthus.unixista.util._
import org.kynthus.unixista.core.instance.ArgumentCategoryInstances.StreamArgumentCategory // Streamへ変更する

val parser = OParser.parseBuilder
  .args("--foo")
  .args("fooValue")
  .args("--bar")
  .args("barValue")
  .args("--help")

scala> println(parser) // コマンドライン引数がStreamとなる
Some(HNil :: Stream(--foo, ?) :: HNil)

コマンドライン引数をラップする特殊な型

ラップする型にscalaz.NonEmptyListscalaz.Id.Idを指定した場合は引数に制約がかかります。
それぞれの制約は以下の通りです。

  • scalaz.NonEmptyList
    argsの引数へ渡せるのはかならず1つ以上の要素を持つ型です。
    Intなどの値型やStringをはじめ、フィールドを1つ以上持つケースクラスなどが該当します。
    ArraySeqなど空となりうる型を指定することはできません。

  • scalaz.Id.Id
    argsの引数へ渡せるのはかならず単一の要素のみを持つ型です。
    IntStringのほか、フィールドを1つだけ持つケースクラスがこれに該当します。

また、Optionscalaz.Maybeおよびscalaz.LazyOptionは値を1つしか持てない都合上、
最初に指定された引数しか保持することができません。
用途は稀ですが、上書き不可能な唯一のコマンドライン引数を表現したいときに便利です。

構築用オブジェクトをラップする型の変更

構築用オブジェクトをラップするデフォルトの型はOptionです。

import org.kynthus.unixista._

scala> println(OParser.parseBuilder)
Some(HNil :: HNil)

こちらも組み込みで以下の3種類がサポートされており、変更することが可能です。

使用する型 インポート対象
Option APOptionRunCategory
scalaz.Maybe APMaybeRunCategory
scalaz.LazyOption APLazyOptionRunCategory
import org.kynthus.unixista.util._
import org.kynthus.unixista.argparse.instance.RunCategoryInstances.APMaybeRunCategory

scala> println(OParser.parseBuilder) // scalaz.Maybeでラップする
Just(HNil :: HNil)

型切り替え時の注意点

型を切り替える際はorg.kynthus.unixista._をインポート宣言として記述してはいけません。

import org.kynthus.unixista._
import org.kynthus.unixista.argparse.instance.RunCategoryInstances.APMaybeRunCategory

scala> println(OParser.parseBuilder) // scalaz.Maybeを指定してもOptionのまま
Some(HNil :: HNil)

Scala 2.11系以降であればコンパイルエラーとして検出できます。

import org.kynthus.unixista._
import org.kynthus.unixista.argparse.instance.RunCategoryInstances.APMaybeRunCategory

scala> println(OParser.parseBuilder) // Scala 2.11以降ではコンパイルエラー
<console>:12: error: could not find implicit value for parameter base: scalaz.@@[org.kynthus.unixista.core.concept.Builder.Aux[org.kynthus.unixista.OParser.type,Output],scopt.OParser.type]
              println(OParser.parseBuilder)
                              ^

ユーザ定義型の使用

パーサの構築用オブジェクトが内部に使用する型や、メソッドの引数へ渡すことのできる型は、
ユーザが独自に定義することもできます。

構築用オブジェクトでユーザ定義型を使用する

コマンドライン引数の要素の型、並びを格納する型、および構築用オブジェクトをラップする型に対し、
ユーザ定義型を使用する場合は以下のように型クラスインスタンスを定義します。

import org.kynthus.unixista.util._
import org.kynthus.unixista.core.concept._
import scalaz._

/**
 * --- コマンドライン引数の要素型の定義 ---
 * コマンドライン引数の要素としてSymbolをサポートする
 */
implicit val MySymbolArgumentElement:
  ResultElement[Symbol] @@ Argument.type =
  Tag(new ResultElement[Symbol] {})

/**
 * --- コマンドライン引数の要素型の定義 ---
 * 要素型をSymbolへ変換するためのコンバータの定義
 */
implicit def MyToSymbolArgumentConverter[Source]:
Converter.Aux[Source, Symbol] @@ Argument.type = Tag {

  new Converter[Source] {

    override type Result = Symbol

    override def apply(derived: => Source): super.Result = Symbol(derived.toString)

  }

}

/**
 * --- コマンドライン引数の要素型の定義 ---
 * SymbolからStringへ戻すための逆コンバータの定義
 */
implicit val MySymbolToStringConverter:
  Converter.Aux[Symbol, String] @@ Argument.type = Tag {

  new Converter[Symbol] {

    override type Result = String

    override def apply(derived: => Symbol): super.Result = derived.name

  }

}

/**
 * --- コマンドライン引数の並びを格納する型の定義 ---
 * コマンドライン引数の並びを格納する型としてNonEmptyListをサポート
 */
implicit val MyNonEmptyListArgumentCategory:
  ResultCategory[NonEmptyList] @@ Argument.type =
  Tag(new ResultCategory[NonEmptyList] {})

/**
 * --- 構築用オブジェクトをラップする型の定義 ---
 * 構築用オブジェクトをラップする型としてIListをサポート
 */
implicit val MyIListRunCategory:
  ResultCategory[IList] @@ Run.type =
  Tag(new ResultCategory[IList] {})

/**
 * --- 構築用オブジェクトをラップする型の定義 ---
 * OptionからIListへ変換方法を定義
 */
implicit val MyOptionToIListNaturalTransformation:
  Option ~> IList = new NaturalTransformation[Option, IList] {

  import scalaz.std.option.optionInstance
  import scalaz.syntax.foldable.ToFoldableOps

  override def apply[Element](option: Option[Element]): IList[Element] = option.toIList

}

val parser = OParser.parseBuilder
  .args("--foo")
  .args("fooValue")
  .args("--bar")
  .args("barValue")
  .args("--help")

scala> println(parser) // それぞれユーザ定義型へ変更される
[HNil :: NonEmpty['--foo,'fooValue,'--bar,'barValue,'--help] :: HNil]

コマンドライン引数の要素型をSymbol、並びを格納する型をscalaz.NonEmptyList
構築用オブジェクト自体をラップする型をscalaz.IListへ変更しています。

ユーザ定義型は自由度は高いですが、引数の解析にあたってデータ構造のパフォーマンス等に問題がなければ、
デフォルトで用意されているもので十分です。

コマンドライン引数および初期値へ指定する型のユーザ定義

argsinitialへ指定できる型は多岐にわたりますが、さらに独自の型も受け取れるようにもできます。
以下のプログラムはargsの引数へ指定できる型としてjava.net.Socketを追加する例です。

import org.kynthus.unixista._
import org.kynthus.unixista.core.concept._
import java.net.{ServerSocket, Socket}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.language.higherKinds
import scalaz._
import shapeless._

/**
 * argsの引数としてjava.net.Socketをサポート(ホスト名とポート番号を引数として順に追加する)
 */
implicit def SocketArgument[
  ConvertedElement,
  ConvertedCategory[_]
](
   implicit
   argumentElement: ResultElement[ConvertedElement] @@ Argument.type,
   argumentCategory: ResultCategory[ConvertedCategory] @@ Argument.type,
   lazyPairArgument: Lazy[
     Argument.Aux[
       (String, Int),
       ConvertedCategory[ConvertedElement]
     ]
   ]
 ): Argument.Aux[
  Socket,
  ConvertedCategory[ConvertedElement]
] = new Argument[Socket] {

  private[this] implicit val pairArgument: Argument.Aux[
    (String, Int),
    ConvertedCategory[ConvertedElement]
  ] = lazily.apply

  override type Result = ConvertedCategory[ConvertedElement]

  override def apply(derived: => Socket):
  super.Result = (derived.getInetAddress.getHostName, derived.getPort).argument

}

val future: Future[ServerSocket] = Future {
  val s: ServerSocket = new ServerSocket(12345)
  s.setReuseAddress(true)
  s.accept()
  s
}
Thread.sleep(1000L)

val parser = OParser.parseBuilder.args(new Socket("localhost", 12345)) // java.net.Socketをargsの引数に指定

scala> println(parser) // ホスト名とポート番号がコマンドライン引数に格納される
Some(HNil :: Vector(localhost, 12345) :: HNil)

続いて、java.net.Socketinitialがサポートする型として追加する例を以下に示します。

import org.kynthus.unixista._
import org.kynthus.unixista.core.concept._
import java.net.{ServerSocket, Socket}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.language.higherKinds
import shapeless._
import shapeless.labelled._
import shapeless.syntax.singleton.mkSingletonOps

val hostRecord: Witness = "host".witness
val portRecord: Witness = "port".witness

/**
 * initialの引数としてjava.net.Socketをサポート(ホスト名とポート番号を持つ)
 */
implicit val SocketField: Field.Aux[
  Socket,
  FieldType[
    hostRecord.T,
    String
  ] :: FieldType[
    portRecord.T,
    Int
  ] :: HNil
] = new Field[Socket] {

  override type Result = FieldType[
    hostRecord.T,
    String
  ] :: FieldType[
    portRecord.T,
    Int
  ] :: HNil

  override def apply(derived: => Socket): super.Result = {

    val r1: FieldType[
      hostRecord.T,
      String
    ] = field(derived.getInetAddress.getHostName)

    val r2: FieldType[
      portRecord.T,
      Int
    ] = field(derived.getPort)

    r1 :: r2 :: HNil

  }

}

val future: Future[ServerSocket] = Future {
  val s: ServerSocket = new ServerSocket(12345)
  s.setReuseAddress(true)
  s.accept()
  s
}
Thread.sleep(1000L)

val parser = OParser.parseBuilder.initial(new Socket("localhost", 12345)) // java.net.Socketをinitialの引数に指定

scala> println(parser) // ホスト名とポート番号の初期値が追加される
Some(HNil :: List(Queue()) :: (localhost :: 12345 :: HNil) :: HNil)

総ざらい

これまでの機能を活用し、より複雑なコマンドライン引数の解析を行う例を以下に示します。

import org.kynthus.unixista._
import java.io.File
import shapeless.syntax.singleton.mkSingletonOps

object FullExample {

  def main(args: Array[String]): Unit = {

    val parsed = OParser.parseBuilder
      .initial("foo"       ->> -1)
      .initial("out"       ->> new File("."))
      .initial("xyz"       ->> false)
      .initial("libName"   ->> "")
      .initial("maxCount"  ->> -1)
      .initial("verbose"   ->> false)
      .initial("debug"     ->> false)
      .initial("mode"      ->> "")
      .initial("files"     ->> Seq.empty[File])
      .initial("keepalive" ->> false)
      .initial("jars"      ->> Seq.empty[File])
      .initial("kwargs"    ->> Map.empty[String, String])
      .programName("unixista")
      .head(Seq("unixista", "1.x"): _*)
      .opt("foo", _ ("foo"))
      .abbr("f")
      .action(_.replace("foo", _))
      .text("foo is an integer property")
      .opt("out", _ ("out"))
      .abbr("o")
      .required
      .valueName("<file>")
      .action(_.replace("out", _))
      .text("out is a required file property")
      .opt("max", classOf[(String, Int)])
      .action { case (hl, (ln, mc)) => hl.replace("libName", ln).replace("maxCount", mc) }
      .validate { case (_, mc) => if (mc <= 0) Left("Value <max> must be >0") else Right(()) }
      .keyValueName("<libname>", "<max>")
      .text("maximum count for <libname>")
      .opt("jars", _ ("jars"))
      .abbr("j")
      .valueName("<jar1>,<jar2>...")
      .action(_.replace("jars", _))
      .text("jars to include")
      .opt("kwargs", _ ("kwargs"))
      .valueName("k1=v1,k2=v2...")
      .action(_.replace("kwargs", _))
      .text("other arguments")
      .opt("verbose")
      .set(_.replace("verbose", true))
      .text("verbose is a flag")
      .opt("debug")
      .hidden
      .set(_.replace("debug", true))
      .text("this option is hidden in the usage text")
      .help("help")
      .text("prints this usage text")
      .arg("<file>...", classOf[File])
      .unbounded
      .optional
      .action((hl, fl) => hl.updateWith("files")(_ :+ fl))
      .text("optional unbounded args")
      .note("some notes." + sys.props("line.separator"))
      .cmd("update")
      .set(_.replace("mode", "update"))
      .text("update is a command.")
      .children
      .opt("not-keepalive")
      .abbr("nk")
      .set(_.replace("keepalive", false))
      .text("disable keepalive")
      .opt("xyz", _ ("xyz"))
      .action(_.replace("xyz", _))
      .text("xyz is a boolean property")
      .opt("debug-update")
      .hidden
      .set(_.replace("debug", true))
      .text("this option is hidden in the usage text")
      .checkConfig(hl => if (hl("keepalive") && hl("xyz")) Left("xyz cannot keep alive") else Right(()))
      .args(args)
      .run

    val result = parsed.getOrElse {
      // Arguments are bad, Error message will have been displayed.
      sys.exit(1)
    }

    // Do something.

  }

}

上記プログラムへ--helpオプションを付与して実行した結果が以下です。

$ scala FullExample.jar --help
unixista 1.x
Usage: unixista [update] [options] [<file>...]

  -f, --foo <value>        foo is an integer property
  -o, --out <file>         out is a required file property
  --max:<libname>=<max>    maximum count for <libname>
  -j, --jars <jar1>,<jar2>...
                           jars to include
  --kwargs k1=v1,k2=v2...  other arguments
  --verbose                verbose is a flag
  --help                   prints this usage text
  <file>...                optional unbounded args
some notes.

Command: update [options]
update is a command.
  -nk, --not-keepalive     disable keepalive
  --xyz <value>            xyz is a boolean property

注意事項

  • IDEでの構文エラー
    本ライブラリをIntelliJ IDEAで利用する際、メソッドの呼び出し箇所が構文エラーとなることがあります。
    コンパイルおよび実行は問題なく可能なケースもありますので、念のためコンパイル確認を行ってください。

  • 多数のメソッド呼び出し時のスタック不足
    メソッドの呼び出し回数が多くなると、コンパイル時にスタックサイズ超過でエラーとなる場合があります。
    その際は、以下のように-J-Xssオプションでコンパイル時のスタックサイズを増やしてください。

$ scalac -J-Xss32m FullExample.scala

ライセンス

License: MIT