在 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 命令行解析