1. 序章

このチュートリアルでは、Scalaでコマンドライン引数を解析するさまざまな方法を紹介します。

このトピックは少し些細なことのように思えるかもしれませんが、誰かが自分で解析を実装するべきか、それとも外部ライブラリを使用するべきか疑問に思うことがよくあります。

2. パターンマッチング

パターンマッチングは、単純なシナリオでScalaの引数を解析する便利な方法のようです。

val usage = """
  Usage: patternmatching [--arg1 num] [--arg2 num] filename
"""
def main(args: Array[String]) {
  if (args.length == 0) println(usage)

  def nextArg(map: Map[String, Any], list: List[String]): Map[String, Any] = {
    list match {
      case Nil => map
      case "--arg1" :: value :: tail =>
        nextArg(map ++ Map("arg1" -> value.toInt), tail)
      case "--arg2" :: value :: tail =>
        nextArg(map ++ Map("arg2" -> value.toInt), tail)
      case string :: Nil =>
        nextArg(map ++ Map("filename" -> string), list.tail)
      case unknown :: _ =>
        println("Unknown option " + unknown)
        exit(1)
    }
  }
  val options = nextArg(Map(), args.toList)
  println(options)
}

マイナス面として、ここで重要なのは、このソリューションは柔軟ではなく、新しい引数ごとに、実装を変更してを解析する必要があるということです。

3. リストスライディング

引数配列を解析する別の方法は、slideing関数を使用することです。 基本的に、この実装は配列をタプルに分割し、パターンマッチングソリューションと同じ方法でタプルを処理します。

コードを見てみましょう:

val usage = """
  Usage: sliding [--arg1 num] [--arg2 num] [--filename filename]
"""

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

  if (args.isEmpty || args.length % 2 != 0) {
    println(usage)
    exit(1)
  }

  val argMap = Map.newBuilder[String, Any]
  args.sliding(2, 2).toList.collect {
    case Array("--arg1", arg1: String) => argMap.+=("arg1" -> arg1)
    case Array("--arg2", arg2: String) => argMap.+=("arg2" -> arg2)
    case Array("--filename", filename: String) =>
      argMap.+=("filename" -> filename)
  }
  println(argMap.result())
}

パターンマッチングソリューションと同様に、このソリューションは一般的ではなく、すべての新しい引数を個別に処理する必要があります。

4. Scopt

Scopt は、より複雑で動的なシナリオに使用できるコマンドラインオプション解析ライブラリです。 カスタムソリューションとは異なり、Scoptは、デフォルト値、略語、リスト解析、ヘルプ印刷、オプション検証などのオプションを提供します。 特に、Scoptは機能的DSLとオブジェクト指向DSLの両方を提供します。

Scoptを使用するには、最初に依存関係を追加する必要があります。

libraryDependencies += "com.github.scopt" %% "scopt" % "4.0.1"

次のスニペットは、機能的なDSLを使用するいくつかの機能を示しています。

case class Config(
  argA: Int = -1,
  argB: Int = -1,
  debug: Boolean = false,
  string: String = "",
  list: Seq[String] = Seq(),
  map: Map[String, String] = Map(),
  command1: String = "",
  cmdArg: Int = 0
)

val builder = OParser.builder[Config]
val argParser = {
  import builder._
  OParser.sequence(
    programName("myprog"),
    head("myprog", "0.1"),
    opt[Int]('a', "argA")
      .required()
      .action((a, c) => c.copy(argA = a))
      .text("required integer"),
    opt[Int]('b', "argB")
      .action((b, c) => c.copy(argB = b))
      .validate(b => {
        if (b >= 10) {
          success
        } else {
          failure("just cause")
        }
      })
      .text("optional integer"),
    opt[Boolean]('d', "debug")
      .action((d, c) => c.copy(debug = d))
      .text("optional boolean"),
    opt[String]('s', "string")
      .action((s, c) => c.copy(string = s))
      .text("optional string"),
    opt[Seq[String]]('l', "list")
      .valueName("<v1>,<v2>")
      .action((l, c) => c.copy(list = l))
      .text("string list"),
    opt[Map[String, String]]('m', "map")
      .valueName("<k1>=<v1>,<k2>=<v2>")
      .action((m, c) => c.copy(map = m))
      .text("optional map"),
    cmd("command1")
      .action((_, c) => c.copy(command1 = "command1"))
      .children(
        opt[Int]("cmdArg")
          .action((cmdArg, c) => c.copy(cmdArg = cmdArg))
          .text("command argument")
      ),
    checkConfig(c => {
      if (c.argA < c.argB) {
        success
      } else {
        failure("just cause")
      }
    })
  )

}

def main(args: Array[String]): Unit = {
  OParser.parse(argParser, args, Config()) match {
    case Some(config) =>
      println(OParser.usage(argParser))
      // do stuff with config
    case _ =>
      exit(1)
  }
}

OParser.usage関数の出力を見てみましょう。

myprog 0.1
Usage: myprog [command1] [options]

  -a, --argA <value>       required integer
  -b, --argB <value>       optional integer
  -d, --debug <value>      optional boolean
  -s, --string <value>     optional string
  -l, --list <v1>,<v2>     string list
  -m, --map <k1>=<v1>,<k2>=<v2>
                           optional map
Command: command1 [options]

  --cmdArg <value>         command argument

5. ホタテ貝

Scallop は、やはり別の解析ライブラリです。 ScallopはScoptがサポートする多くの機能をサポートしていますが、には機能的なDSLがありません。

Scallopに必要な依存関係は次のとおりです。

libraryDependencies += "org.rogach" %% "scallop" % "4.1.0"

Scallop解析の基本的な例を見てみましょう。

class Conf(arguments: Seq[String]) extends ScallopConf(arguments) {
  val high = opt[Int](required = true)
  val low = opt[Int]()
  val name = trailArg[String]()
  verify()
}

def main(args: Array[String]): Unit = {
  val conf = new Conf(args)
  println("high is: " + conf.high())
  println("low is: " + conf.low())
  println("name is: " + conf.name())
}

さらに、 Scallopは、末尾の引数の強力なパターンマッチングをサポートします。 例えば:

object ScallopPatternMatching {

  class Conf(args: Seq[String])
    extends ScallopConf(args) {
    val propsMap = props[String]('P')
    val firstString = trailArg[String]()
    val firstList = trailArg[List[Int]]()
    val secondString = trailArg[String]()
    val secondList = trailArg[List[Double]]()
    verify()
  }

  def main(args: Array[String]): Unit = {
    val conf = new Conf(args)
    println("propsMap.key1 is: " + conf.propsMap("key1"))
    println("propsMap.key2 is: " + conf.propsMap("key2"))
    println("propsMap.key3 is: " + conf.propsMap("key3"))
    println("firstString is: " + conf.firstString())
    println("firstList is: " + conf.firstList())
    println("secondString is: " + conf.secondString())
    println("secondList is: " + conf.secondList())
  }
}

上記のスニペットは、次の引数を解析します。

scala MyProg.scala -Pkey1=value1 key2=value2 key3=value3 first 1 2 3 second 4 5 6

6. クリスト

Clistはさらに別の解析ライブラリです。 構文と機能は、末尾の引数の強力なマッチングを除いて、Scallopが提供するものと非常に似ています。

Clistの依存関係をbuild.sbtファイルに追加しましょう。

libraryDependencies += "org.backuity.clist" %% "clist-core"   % "3.5.1"
libraryDependencies += "org.backuity.clist" %% "clist-macros" % "3.5.1" % "provided"

次の例では、必須、オプション、およびリスト引数を解析します。

class Config extends Command("") {
  var apples = arg[Int](description = "apples count")
  var oranges = opt[Option[Int]](description = "oranges count")
  var debug = opt[Boolean](description = "debug flag", abbrev = "d")
  var list = opt[Option[Seq[String]]](description = "a list of strings")
}

object Clist {
  def main(args: Array[String]): Unit = {
    Cli.parse(args).withCommand(new Config) { config =>
      println(config.description)
    }
  }
}

印刷された詳細な説明を見てみましょう。

Usage

  [options] <apples>

Options

   -d, --debug : debug flag
   --list      : a list of strings
   --oranges   : oranges count

Arguments

   <apples> : apples count

7. Args4j

以前のライブラリとは異なり、 Args4j は、機能またはオブジェクト指向のDSLの代わりに注釈を使用します。 名前が示すように、Args4jはJavaライブラリであるため、アノテーションを使用します。

Args4jの依存関係を含めましょう。

libraryDependencies += "args4j" % "args4j" % "2.33"

以下に、オプションの引数と必須の引数を解析する方法を示します。

object Args {
  @Option(name = "-bananas", required = true, usage = "bananas count")
  var bananas: Int = -1

  @Option(name = "-apples", usage = "apples count")
  var apples: Int = -1

  @Option(name = "-filename", usage = "file name")
  var filename: String = null

}

object Args4J {

  def main(args: Array[String]): Unit = {
    val parser = new CmdLineParser(Args)
    try {
      parser.parseArgument(JavaConverters.asJavaCollection(args))
    } catch {
      case e: CmdLineException =>
        print(s"Error:${e.getMessage}\n Usage:\n")
        parser.printUsage(System.out)
        exit(1)
    }
    println(Args.apples)
    println(Args.bananas)
    println(Args.filename)
  }

}

8. 結論

この記事では、Scalaでコマンドライン引数を解析するさまざまな方法を示しました。

もちろん、すべてのソリューションには長所と短所があります。 この記事が、次にコマンドライン引数を解析する必要があるときに役立つことを願っています。

いつものように、上記の例のコードはGitHubから入手できます。