CLOVER🍀

That was when it all began.

Java/Groovy/Scalaで、コマンドライン引数処理

最近、ちょっと身の回りでコマンドライン引数の処理が話題に挙がりまして、ちょっとJava系でどんなのが使えるのか調べてみました。

ちなみに、もともと話が出たのはJavaでしたが、このエントリでは加えてGroovyとScalaも書きたいと思います。

実行するコマンドのお題は、以下とします。

  • 指定されたメッセージを、指定された回数表示するコマンドLoopEchoを実装する
  • 「-m」オプション(ロングオプション「--message」)で表示するメッセージを指定し、必須項目とする
  • 「-c」オプション(ロングオプション「--count」)で表示回数を指定し、任意項目とする(デフォルト値:3)

Java/args4j

このエントリを書く元になった仕事の上では(残念ながら自分が実装するわけではないので)、Commons CLIになったのですが、今回はこちらを使用したいと思います。

args4j : Java command line arguments parser
http://args4j.kohsuke.org/

Javadoc
http://args4j.kohsuke.org/args4j/apidocs/

チュートリアル
http://args4j.kohsuke.org/sample.html

サンプル
https://github.com/kohsuke/args4j/blob/master/args4j/examples/SampleMain.java

アノテーションベースで、コマンドライン引数定義を行うライブラリです。

では、使ってみます。Maven依存関係。

    <dependency>
      <groupId>args4j</groupId>
      <artifactId>args4j</artifactId>
      <version>2.0.26</version>
    </dependency>

コマンドライン引数を受け取るクラス。
src/main/java/org/littlewings/args4j/example/LoopEchoOption.java

package org.littlewings.args4j.example;

import org.kohsuke.args4j.Option;

public class LoopEchoOption {
    @Option(name = "-m", aliases = "--message", required = true, metaVar = "<message>", usage = "表示するメッセージ")
    private String message;

    @Option(name = "-c", aliases = "--count", metaVar = "<count>", usage = "表示回数")
    private int count = 3;

    public String getMessage() {
        return message;
    }

    public int getCount() {
        return count;
    }
}

アノテーションの意味は、先に想定している仕様の説明をしているのでわかる気がしますが、metaVarなどはusageで使われるので、後で見てみましょう。

メインクラス。
src/main/java/org/littlewings/args4j/example/LoopEcho.java

package org.littlewings.args4j.example;

import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;

public class LoopEcho {
    public static void main(String... args) {
        LoopEchoOption option = new LoopEchoOption();
        CmdLineParser parser = new CmdLineParser(option);

        try {
            parser.parseArgument(args);
        } catch (CmdLineException e) {
            // 引数の型がおかしかったり、必須項目が足りないと例外
            System.err.println("Got Exception: " + e.getMessage());
            parser.printUsage(System.err);
            System.exit(1);
        }

        for (int i = 0; i < option.getCount(); i++) {
            System.out.println(option.getMessage());
        }
    }
}

使い方は至ってシンプルで、先ほど作成したLoopEchoOptionクラスのインスタンスをCmdLineParserクラスのコンストラクタに設定します。あとは、コマンドライン引数を渡してparseArgumentメソッドを呼び出します。

必須項目が足りなかったり、型が不正(ここでは、countにintに変換できない値を渡すなど)だった場合は、CmdLineExceptionがスローされます。

では、実行してみましょう。maven-exec-pluginを使用することにします。

引数指定なし。

$ mvn compile exec:java \
 -Dexec.mainClass=org.littlewings.args4j.example.LoopEcho

例外がスローされ、今回のサンプルでは例外をキャッチした時にCmdLineParser#printUsageしているので、以下のようなメッセージが表示されます。

Got Exception: Option "-m (--message)" is required
 -c (--count) <count>     : 表示回数
 -m (--message) <message> : 表示するメッセージ

Optionアノテーションの、metaVarとusageがどこに反映されているかわかりましたね。

「-m」のみ指定。

$ mvn compile exec:java \
 -Dexec.mainClass=org.littlewings.args4j.example.LoopEcho \
 -Dexec.args="-m こんにちは"

結果。

こんにちは
こんにちは
こんにちは

「-m」と「-c」を指定。

$ mvn compile exec:java \
 -Dexec.mainClass=org.littlewings.args4j.example.LoopEcho \
 -Dexec.args="-m こんにちは -c 5"

結果。

こんにちは
こんにちは
こんにちは
こんにちは
こんにちは

「-c」に、不正な値を入力。

$ mvn compile exec:java \
 -Dexec.mainClass=org.littlewings.args4j.example.LoopEcho \
 -Dexec.args="-m こんにちは -c abc"

結果。

Got Exception: "abc" is not a valid value for "-c"
 -c (--count) <count>     : 表示回数
 -m (--message) <message> : 表示するメッセージ

バッチリ、引数解析エラーです。

Usageとか、ちゃんと出せるのがいいですね〜。

ロングオプション。

$ mvn compile exec:java \
 -Dexec.mainClass=org.littlewings.args4j.example.LoopEcho \
 -Dexec.args="--message こんにちは --count 5"

結果。

こんにちは
こんにちは
こんにちは
こんにちは
こんにちは

Groovy/CliBuilder

今度は、Groovy。こちらは、Groovy自体に入っているCliBuilderを使用します。Commons CLIのラッパーらしいです。

CliBuilder
http://groovy.codehaus.org/gapi/groovy/util/CliBuilder.html

コード。
loop-echo.groovy

def cli = new CliBuilder(usage: 'groovy loop-echo.groovy [options]')

cli.m(longOpt:'message', args:1, required:true, argName:'message', '表示するメッセージ')
cli.c(longOpt:'count', args:1, argName:'count', '表示回数')

def options = cli.parse(args)

if (!options) {
    // 引数解析に失敗した場合
    System.exit(1)
}

def count = !options.c ? 3 : options.c as int
(0 ..< count).each {
    println(options.m)
}

println('===========')
cli.usage()

仕様は一応同じなのですが、デフォルトはエラー時にそのまま標準出力にエラーメッセージを表示します。まあ、PrintWriterが設定できるみたいなので、そちらを差し替えればよいと思いますが。

最初に、CliBuilderのインスタンスを作成します。

def cli = new CliBuilder(usage: 'groovy loop-echo.groovy [options]')

このインスタンスに対して、引数の定義を設定していきます。

cli.m(longOpt:'message', args:1, required:true, argName:'message', '表示するメッセージ')
cli.c(longOpt:'count', args:1, argName:'count', '表示回数')

いきなり「m」とか「c」とか指定していますが、これが引数の名前として扱われます。なお、パラメータを取る場合は、argsの指定が必要です。指定しなかった場合は、パラメータなしのフラグ的なオプションみたいになります。

あとは、コマンドライン引数を渡してCliBuilder#parserを呼び出すのですが、

def options = cli.parse(args)

if (!options) {
    // 引数解析に失敗した場合
    System.exit(1)
}

この時、必須項目や型の不正などで失敗した場合は、戻り値がfalseになるのでこちらで判定します。

では、実行してみましょう。
引数指定なし。

$ groovy loop-echo.groovy

結果。

error: Missing required option: m
usage: groovy loop-echo.groovy [options]
 -c,--count <count>       表示回数
 -m,--message <message>   表示するメッセージ

このメッセージは、CliBuilderが勝手に出しています。

「-m」のみ指定。

$ groovy loop-echo.groovy -m こんにちは

結果。

こんにちは
こんにちは
こんにちは
===========
usage: groovy loop-echo.groovy [options]
 -c,--count <count>       表示回数
 -m,--message <message>   表示するメッセージ

繰り替えしますが、最後の「===========」のUsageは、自分でCliBuilder#usageを呼び出して表示しています。

「-m」と「-c」を指定。

$ groovy loop-echo.groovy -m こんにちは -c 5

結果。

こんにちは
こんにちは
こんにちは
こんにちは
こんにちは
===========
usage: groovy loop-echo.groovy [options]
 -c,--count <count>       表示回数
 -m,--message <message>   表示するメッセージ

「-c」に、不正な値を入力。

$ groovy loop-echo.groovy -c abc

結果。
「-c」に、不正な値を入力。

$ groovy loop-echo.groovy -m こんにちは -c abc

結果。

Caught: java.lang.NumberFormatException: For input string: "abc"
java.lang.NumberFormatException: For input string: "abc"
	at loop-echo.run(loop-echo.groovy:13)

これは…ここで、as intかけた時にコケましたね。

def count = !options.c ? 3 : options.c as int

CliBuilderの場合は、型制約は自分で頑張るのかな??

というか、サポートしているオプションの一覧に

type: Object (not currently used)

と書いてますし…。まあ、いいや。

ロングオプション。

$ groovy loop-echo.groovy --message こんにちは --count 5

結果。

こんにちは
こんにちは
こんにちは
こんにちは
こんにちは
===========
usage: groovy loop-echo.groovy [options]
 -c,--count <count>       表示回数
 -m,--message <message>   表示するメッセージ

こんなところですね。

Scala/scopt

最後は、Scala。こちらは、scoptというライブラリを使用します。

scopt
https://github.com/scopt/scopt

Scaladoc
http://scopt.github.io/scopt/3.1.0/api/#scopt.OptionParser

Scalaコマンドライン引数解析については、こちらにまとめがありました。

Scala: Best way to parse command-line parameters (CLI)?
http://stackoverflow.com/questions/2315912/scala-best-way-to-parse-command-line-parameters-cli

なお、scoptはscala-optionsをforkしてできたものらしいです…。

scopt 3.0
http://eed3si9n.com/ja/scopt3

で、コードの方なのですがscopt自体は、先のGitHubにサンプルも載っていますし、いろいろできそうなのですが、今回は簡単にいきます。

sbtで依存関係の定義。

resolvers += Resolver.sonatypeRepo("public")

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

例に習って、引数を受け取るためのCase Classをデフォルト値入りで用意します。
src/main/scala/org/littlewings/scopt/example/LoopEchoOption.scala

package org.littlewings.scopt.example

case class LoopEchoOption(message: String = "", count: Int = 3)

ホントに、ただのCase Classです。

続いて、パーサーの定義。
src/main/scala/org/littlewings/scopt/example/LoopEcho.scala

package org.littlewings.scopt.example

import scopt.OptionParser

object LoopEcho {
  def main(args: Array[String]): Unit = {
    val parser = new OptionParser[LoopEchoOption]("LoopEcho") {
      opt[String]('m', "message") required() valueName("<message>") action { (x, o) =>
        o.copy(message = x)
      } text("表示するメッセージ [必須]")

      opt[Int]('c', "count") valueName("<count>") action { (x, o) =>
        o.copy(count = x)
      } text("表示回数")
    }

    parser.parse(args, LoopEchoOption()) map { option =>
      // 引数解析に成功した場合
      (1 to option.count).foreach { _ => println(option.message) }
    } getOrElse {
      // 引数解析に失敗した場合
      sys.exit(1)
    }

    println("===========")
    println(parser.usage)
  }
}

OptionParserクラスのサブクラスを作成していますが、中でoptメソッドを使用してオプション定義していきます。ジェネリクスで型指定を行い、続けてオプション、ロングオプションを定義しています。

また、actionメソッドで解析した値、そしてこれまでの解析結果のクラスのインスタンス(ここではLoopEchoOptionクラスのインスタンス)が渡ってくるので、ここで値を設定します。Case Classなので、コピーを作成して返却ですね。

設定が終わったら、OptionParser#parserメソッドを呼び出し、結果を以下の様に扱います。

    parser.parse(args, LoopEchoOption()) map { option =>
      // 引数解析に成功した場合o
      (1 to option.count).foreach { _ => println(option.message) }
    } getOrElse {
      // 引数解析に失敗した場合
      sys.exit(1)
    }

mapに入った場合は、引数解析に成功したことを表すので、設定が終わったLoopEchoOptionクラスのインスタンスが渡されます。解析に失敗した場合は、getOrElseの中に失敗時の処理を書きます。今回は、即時終了。

失敗した場合のUsageは、scoptが出力するので。なので、正常時にはこちらもUsageを自分で出力するようにしました。

    println("===========")
    println(parser.usage)

では、動作確認。今回は、sbt上で動かします。

引数指定なし。

> run

結果。

Error: Missing option --message
Usage: LoopEcho [options]

  -m <message> | --message <message>
        表示するメッセージ [必須]
  -c <count> | --count <count>
        表示回数

valueNameやtextで指定した内容が、ちゃんとUsageに反映されていますね。必須なことは、自分で書きましたが…。

「-m」のみ指定。

> run -m こんにちは

結果。

こんにちは
こんにちは
こんにちは
===========
Usage: LoopEcho [options]

  -m <message> | --message <message>
        表示するメッセージ [必須]
  -c <count> | --count <count>
        表示回数

繰り替えしますが、こちらも最後の「===========」のUsageは、自分でOptionParser#usageを呼び出して表示しています。


「-m」と「-c」を指定。

> run -m こんにちは -c 5

結果。

こんにちは
こんにちは
こんにちは
こんにちは
こんにちは
===========
Usage: LoopEcho [options]

  -m <message> | --message <message>
        表示するメッセージ [必須]
  -c <count> | --count <count>
        表示回数

「-c」に、不正な値を入力。

> run -m こんにちは -c abc

結果。

Error: Option --count expects a number but was given 'abc'
Usage: LoopEcho [options]

  -m <message> | --message <message>
        表示するメッセージ [必須]
  -c <count> | --count <count>
        表示回数

ロングオプション。

> run --message こんにちは --count 5

結果。

こんにちは
こんにちは
こんにちは
こんにちは
こんにちは
===========
Usage: LoopEcho [options]

  -m <message> | --message <message>
        表示するメッセージ [必須]
  -c <count> | --count <count>
        表示回数

こんなところですね。どれも、簡単な使い方はわかりましたよっと。