2017年2月13日

scopt: command line parsing library in Scala

在 Java 要製作一個 command line 工具可以使用 Apache Commons cli,不過在 scala,有另一個更簡潔的 library: scopt,可以幫助我們製作 cli 程式。

libraryDependencies

根據 scopt github 的說明,我們應該在 build.sbt 中加上這樣的 libraryDependencies 宣告設定

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

但我們使用起來覺得有點問題,搜尋了 maven Group: com.github.scopt,看起來這個 library 有針對 scala 的版本提供不同的 library,因為我們是使用 scala 2.11.8,所以就將 libraryDependencies 改成以下這樣

"com.github.scopt" % "scopt_2.11" % "3.5.0",

Config

使用 scopt 之前,要先定義一個用來存放 cli parsing 結果的 case class: Config,我們是要做一個 License File 的產生工具,所以 Config 裡面存的都是 license 需要的資料。

  case class Config(mode: String = "",
                    ver: Boolean = false,
                    getmid: Boolean = false,
                    keyfile: String ="",

                    lictype:String ="",
                    product:String ="",
                    version:String ="",
                    name:String ="",
                    company:String="",
                    copies:Int=1,
                    mid:String="",
                    validfrom:String="",
                    goodthru:String=""
                   ) {
    def copy(mode: String = mode, ver: Boolean = ver,
             getmid:Boolean = getmid,
             keyfile: String = keyfile,

             lictype: String = lictype,
             product: String = product,
             version: String = version,
             name: String = name,
             company: String = company,
             copies: Int = copies,
             mid: String = mid,
             validfrom: String = validfrom,
             goodthru: String = goodthru
            ) =
      new Config(mode, ver, getmid, keyfile, lictype, product, version, name, company,
        copies, mid, validfrom, goodthru)
  }

Parser

接下來是使用 Config 產生 OptionParser,Parser 中是以第一個參數 "mode" 作為不同指令的判斷,我們提供了四個指令:key, lic, dec, --getmid, --ver,另外還有一個基本的 --help,每一個指令都有一個縮寫。

我們可以先看 help 列印出來的結果,最前面的 Usage 是這個程式的使用方式,然後有兩個基本的 --ver 及 --getmid 方法。

接下來是 key, lic, dec 這三個獨立指令的說明,每一個指令都有相關的參數,最後一行是 --help 列印 help 頁面的部分。

[info] Running license.LicenseBuilder -h
[info] License Builder 0.1
[info] Usage: license.LicenseBuilder [key|lic|dec] [options] <args>...
[info]
[info]   -v, --ver                Prints the version number.
[info]   -i, --getmid             Prints the machine id.
[info] Command: key keyfile
[info]   generate RSA key file
[info]   keyfile                  gen key files with key filename prefix
[info] Command: lic [options]
[info]   generate license file
[info]   -k, --prikeyfile <value>
[info]                            private key file prefix
[info]   -l, --lictype <value>    Evaluation/Standard/Enterprise
[info]   -p, --product <value>    product name, ex: kokome
[info]   -e, --version <value>    product version number, ex: 3.0.0
[info]   -n, --name <value>       licensed name, ex: kokome
[info]   -o, --company <value>    licensed company name, ex: maxkit
[info]   -c, --copies <value>     licensed number of users, ex: 5
[info]   -m, --mid <value>        machine id
[info]   -v, --validfrom <value>  licensed valid from date ex: 2016/01/01
[info]   -g, --goodthru <value>   licensed good thru date ex: 2016/12/31
[info] Command: dec keyfile
[info]   decode maxkit.lic
[info]   keyfile                  decode maxkit.lic with key filename prefix
[info]   -h, --help               prints this usage text

看了 help 的說明後,再去看 OptionParser 的寫法,就比較能清楚地分辨不同指令區塊的部分。

val parser = new scopt.OptionParser[Config]("license.LicenseBuilder") {
    head("License Builder", LicenseBuilder.ver)

    //activator "runMain license.LicenseBuilder -v"
    opt[Unit]("ver").abbr("v").action( (_, c) => c.copy(ver = true)).
      text("Prints the version number.")

    //activator "runMain license.LicenseBuilder -i"
    opt[Unit]("getmid").abbr("i").action( (_, c) => c.copy(getmid = true)).
      text("Prints the machine id.")

    //activator "runMain license.LicenseBuilder key maxkit"
    cmd("key").action( (x, c) => c.copy(mode = "key")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("gen key files with key filename prefix")
      ).text("  generate RSA key file")

    //activator "runMain license.LicenseBuilder lic -k maxkit -l Enterprise -p kokome -e 3.0.0 -n kokome -o maxkit -c 10 -m 1234 -v 2016/10/01 -g 2116/01/01"
    cmd("lic").action( (_, c) => c.copy(mode = "lic")).
      children(
        opt[String]('k', "prikeyfile").required().action( (x,c) => c.copy(keyfile=x) ).
          text("private key file prefix"),

        opt[String]('l', "lictype").required().action( (x,c) => c.copy(lictype=x) ).
          text("Evaluation/Standard/Enterprise"),

        opt[String]('p', "product").required().action( (x,c) => c.copy(product=x) ).
          text("product name, ex: kokome"),

        opt[String]('e', "version").required().action( (x,c) => c.copy(version=x) ).
          text("product version number, ex: 3.0.0"),

        opt[String]('n', "name").required().action( (x,c) => c.copy(name=x) ).
          text("licensed name, ex: kokome"),

        opt[String]('o', "company").required().action( (x,c) => c.copy(company=x) ).
          text("licensed company name, ex: maxkit"),

        opt[Int]('c', "copies").required().action( (x,c) => c.copy(copies=x) ).
          text("licensed number of users, ex: 5"),

        opt[String]('m', "mid").required().action( (x,c) => c.copy(mid=x) ).
          text("machine id"),

        opt[String]('v', "validfrom").required().action( (x,c) => c.copy(validfrom=x) ).
          text("licensed valid from date ex: 2016/01/01"),

        opt[String]('g', "goodthru").required().action( (x,c) => c.copy(goodthru=x) ).
          text("licensed good thru date ex: 2016/12/31")

      ).text("  generate license file")

    //activator "runMain license.LicenseBuilder dec maxkit"
    cmd("dec").action( (x, c) => c.copy(mode = "dec")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("decode maxkit.lic with key filename prefix")
      ).text("  decode maxkit.lic")

    //activator "runMain license.LicenseBuilder --help"
    help("help").abbr("h").text("prints this usage text")
  }

  parser.parse(args, Config()) match {
    case Some(config) => {
      // gen privat/pubilic key pairs
      if (config.mode == "key") LicenseBuilder.key(config.keyfile)

      // gen license file
      if (config.mode == "lic") LicenseBuilder.lic(config.keyfile, config.lictype, config.product,
        config.version, config.name, config.company, config.copies,
        config.mid, config.validfrom, config.goodthru)

      // decode license file
      if (config.mode == "dec") LicenseBuilder.dec(config.keyfile)

      // get machine if
      if (config.getmid) LicenseBuilder.getmid

      // print LicenseBuilder version
      if (config.ver) println("LicenseBuilder Version is: " + LicenseBuilder.ver)
    }
    case None => println("Please use -h for usage")
  }

完整的程式

package license

import java.io.File
import java.text.SimpleDateFormat
import java.util.Date

import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.FileUtils
import play.api.Logger
import utils.StringUtil

object LicenseBuilder extends App {
  val ver = "0.1"

  case class Config(mode: String = "",
                    ver: Boolean = false,
                    getmid: Boolean = false,
                    keyfile: String ="",

                    lictype:String ="",
                    product:String ="",
                    version:String ="",
                    name:String ="",
                    company:String="",
                    copies:Int=1,
                    mid:String="",
                    validfrom:String="",
                    goodthru:String=""
                   ) {
    def copy(mode: String = mode, ver: Boolean = ver,
             getmid:Boolean = getmid,
             keyfile: String = keyfile,

             lictype: String = lictype,
             product: String = product,
             version: String = version,
             name: String = name,
             company: String = company,
             copies: Int = copies,
             mid: String = mid,
             validfrom: String = validfrom,
             goodthru: String = goodthru
            ) =
      new Config(mode, ver, getmid, keyfile, lictype, product, version, name, company,
        copies, mid, validfrom, goodthru)
  }

  def key(keyfile: String) = {
    println(s"generate key pairs with filename prefix ${keyfile}")
  }

  def getmid() = {
    val mid = LicenseId.getLicenseId
    println(s"mid = ${mid}")
  }

  def dec(keyfile:String) = {
    println(s"decode license maxkit.lic with ${keyfile}.prikey.dat")

  }

  def lic(keyfile:String, lictype:String,
          product:String, version:String,
          name:String, company:String,
          copies:Int, mid:String,
          validfrom:String, goodthru:String) = {

    println(s"gen license with ${keyfile}.prikey.dat, lictype=${lictype}," +
      s"product=${product}, version=${version}, name=${name}, company=${company}, " +
      s"copies=${copies}, mid=${mid}, validfrom=${validfrom}, goodthru=${goodthru}")
      
  }

  val parser = new scopt.OptionParser[Config]("license.LicenseBuilder") {
    head("License Builder", LicenseBuilder.ver)

    //activator "runMain license.LicenseBuilder -v"
    opt[Unit]("ver").abbr("v").action( (_, c) => c.copy(ver = true)).
      text("Prints the version number.")

    //activator "runMain license.LicenseBuilder -i"
    opt[Unit]("getmid").abbr("i").action( (_, c) => c.copy(getmid = true)).
      text("Prints the machine id.")

    //activator "runMain license.LicenseBuilder key maxkit"
    cmd("key").action( (x, c) => c.copy(mode = "key")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("gen key files with key filename prefix")
      ).text("  generate RSA key file")

    //activator "runMain license.LicenseBuilder lic -k maxkit -l Enterprise -p kokome -e 3.0.0 -n kokome -o maxkit -c 10 -m 1234 -v 2016/10/01 -g 2116/01/01"
    cmd("lic").action( (_, c) => c.copy(mode = "lic")).
      children(
        opt[String]('k', "prikeyfile").required().action( (x,c) => c.copy(keyfile=x) ).
          text("private key file prefix"),

        opt[String]('l', "lictype").required().action( (x,c) => c.copy(lictype=x) ).
          text("Evaluation/Standard/Enterprise"),

        opt[String]('p', "product").required().action( (x,c) => c.copy(product=x) ).
          text("product name, ex: kokome"),

        opt[String]('e', "version").required().action( (x,c) => c.copy(version=x) ).
          text("product version number, ex: 3.0.0"),

        opt[String]('n', "name").required().action( (x,c) => c.copy(name=x) ).
          text("licensed name, ex: kokome"),

        opt[String]('o', "company").required().action( (x,c) => c.copy(company=x) ).
          text("licensed company name, ex: maxkit"),

        opt[Int]('c', "copies").required().action( (x,c) => c.copy(copies=x) ).
          text("licensed number of users, ex: 5"),

        opt[String]('m', "mid").required().action( (x,c) => c.copy(mid=x) ).
          text("machine id"),

        opt[String]('v', "validfrom").required().action( (x,c) => c.copy(validfrom=x) ).
          text("licensed valid from date ex: 2016/01/01"),

        opt[String]('g', "goodthru").required().action( (x,c) => c.copy(goodthru=x) ).
          text("licensed good thru date ex: 2016/12/31")

      ).text("  generate license file")

    //activator "runMain license.LicenseBuilder dec maxkit"
    cmd("dec").action( (x, c) => c.copy(mode = "dec")).
      children(
        arg[String]("keyfile").unbounded().required().action( (x, c) => c.copy(keyfile = x)).
          text("decode maxkit.lic with key filename prefix")
      ).text("  decode maxkit.lic")

    //activator "runMain license.LicenseBuilder --help"
    help("help").abbr("h").text("prints this usage text")
  }

  parser.parse(args, Config()) match {
    case Some(config) => {
      // gen privat/pubilic key pairs
      if (config.mode == "key") LicenseBuilder.key(config.keyfile)

      // gen license file
      if (config.mode == "lic") LicenseBuilder.lic(config.keyfile, config.lictype, config.product,
        config.version, config.name, config.company, config.copies,
        config.mid, config.validfrom, config.goodthru)

      // decode license file
      if (config.mode == "dec") LicenseBuilder.dec(config.keyfile)

      // get machine if
      if (config.getmid) LicenseBuilder.getmid

      // print LicenseBuilder version
      if (config.ver) println("LicenseBuilder Version is: " + LicenseBuilder.ver)
    }
    case None => println("Please use -h for usage")
  }
}

Reference

scala 命令行解析