Unixista
UNIXコマンド風の引数解析ができるライブラリです。
目次
前提条件
本ライブラリは以下に示すバージョンのScala言語でサポートされます。
Scala 2.10.5
~2.10.7
Scala 2.11.0
~2.11.12
Scala 2.12.0
~2.12.12
Scala 2.13.0
~2.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個目の型がWitness のTuple2 |
"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
なお、プログラム中のコメントにもあるとおりrun
をmap
のなかで呼び出してはいけません。
かならず外側で呼び出すか、後述する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
run
はfor
式のなかで呼び出さなければなりません。
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
などのサブコマンドを持ちます。
cmd
やchildren
および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)
なお、わざわざ論理値のtrue
やfalse
を指定するよりも、
以下の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.NonEmptyList
とscalaz.Id.Id
を指定した場合は引数に制約がかかります。
それぞれの制約は以下の通りです。
-
scalaz.NonEmptyList
args
の引数へ渡せるのはかならず1つ以上の要素を持つ型です。
Int
などの値型やString
をはじめ、フィールドを1つ以上持つケースクラスなどが該当します。
Array
やSeq
など空となりうる型を指定することはできません。 -
scalaz.Id.Id
args
の引数へ渡せるのはかならず単一の要素のみを持つ型です。
Int
やString
のほか、フィールドを1つだけ持つケースクラスがこれに該当します。
また、Option
やscalaz.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
へ変更しています。
ユーザ定義型は自由度は高いですが、引数の解析にあたってデータ構造のパフォーマンス等に問題がなければ、
デフォルトで用意されているもので十分です。
コマンドライン引数および初期値へ指定する型のユーザ定義
args
やinitial
へ指定できる型は多岐にわたりますが、さらに独自の型も受け取れるようにもできます。
以下のプログラムは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.Socket
をinitial
がサポートする型として追加する例を以下に示します。
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