使用 scopt 解析命令行参数

https://github.com/scopt/scopt

简单的 scala 命令行选项解析

scopt 是一个小小的命令行选项解析库。

Sonatype

1
libraryDependencies += "com.github.scopt" %% "scopt" % "X.Y.Z"

查看上面的 Maven Central badge

使用方法

scopt 提供了两种解析方式:immutable 和 mutable。无论哪种情况,首先您需要一个表示配置的 case class:

1
2
3
4
5
import java.io.File
case class Config(foo: Int = -1, out: File = new File("."), xyz: Boolean = false,
libName: String = "", maxCount: Int = -1, verbose: Boolean = false, debug: Boolean = false,
mode: String = "", files: Seq[File] = Seq(), keepalive: Boolean = false,
jars: Seq[File] = Seq(), kwargs: Map[String,String] = Map())

在不可变的解析样式中,config 配置对象作为参数传递给 action 回调。另一方面,在可变解析样式中,你需要修改配置对象。

不可变解析

下面是一个你怎么创建 scopt.OptionParser[Config] 的例子。有关各种构建器方法的详细信息,请参阅 Scaladoc API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
val parser = new scopt.OptionParser[Config]("scopt") {
head("scopt", "3.x")

opt[Int]('f', "foo").action( (x, c) =>
c.copy(foo = x) ).text("foo is an integer property")

opt[File]('o', "out").required().valueName("<file>").
action( (x, c) => c.copy(out = x) ).
text("out is a required file property")

opt[(String, Int)]("max").action({
case ((k, v), c) => c.copy(libName = k, maxCount = v) }).
validate( x =>
if (x._2 > 0) success
else failure("Value <max> must be >0") ).
keyValueName("<libname>", "<max>").
text("maximum count for <libname>")

opt[Seq[File]]('j', "jars").valueName("<jar1>,<jar2>...").action( (x,c) =>
c.copy(jars = x) ).text("jars to include")

opt[Map[String,String]]("kwargs").valueName("k1=v1,k2=v2...").action( (x, c) =>
c.copy(kwargs = x) ).text("other arguments")

opt[Unit]("verbose").action( (_, c) =>
c.copy(verbose = true) ).text("verbose is a flag")

opt[Unit]("debug").hidden().action( (_, c) =>
c.copy(debug = true) ).text("this option is hidden in the usage text")

help("help").text("prints this usage text")

arg[File]("<file>...").unbounded().optional().action( (x, c) =>
c.copy(files = c.files :+ x) ).text("optional unbounded args")

note("some notes.".newline)

cmd("update").action( (_, c) => c.copy(mode = "update") ).
text("update is a command.").
children(
opt[Unit]("not-keepalive").abbr("nk").action( (_, c) =>
c.copy(keepalive = false) ).text("disable keepalive"),
opt[Boolean]("xyz").action( (x, c) =>
c.copy(xyz = x) ).text("xyz is a boolean property"),
opt[Unit]("debug-update").hidden().action( (_, c) =>
c.copy(debug = true) ).text("this option is hidden in the usage text"),
checkConfig( c =>
if (c.keepalive && c.xyz) failure("xyz cannot keep alive")
else success )
)
}

// parser.parse returns Option[C]
parser.parse(args, Config()) match {
case Some(config) =>
// do stuff

case None =>
// arguments are bad, error message will have been displayed
}

以上生成以下用法文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scopt 3.x
Usage: scopt [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

Options(选项)

命令行选项是使用 opt[A]('f', "foo")opt[A]("foo") 定义的, 其中 A 是任意类型, 它是 Read typeclass 的实例。

  • Unit 作为普通标记 --foo-f
  • Int, Long, Double, String, BigInt, BigDecimal, java.io.File, java.net.URIjava.net.InetAddress 接收诸如 --foo 80--foo:80 那样的值。
  • Boolean 接收 --foo true--foo:1 这样的值
  • java.util.Calendar 接收 --foo 2018-07-16 这样的值
  • scala.concurrent.duration.Duration 接收 --foo 30s 这样的值
  • (String, Int) 这样的 types 对儿接收 --foo:k=1-f k=1 那样的键值对儿
  • Seq[File] 接收 --jars foo.jar,bar.jar 这样的逗号分割的字符串值
  • Map[String, String] 接收 --kwargs key1=val1,key2=val2 这样的逗号分割的 pairs 字符串值

这可以通过在作用域中定义 Read 实例来扩展。例如,

1
2
3
4
5
6
object WeekDays extends Enumeration {
type WeekDays = Value
val Mon, Tue, Wed, Thur, Fri, Sat, Sun = Value
}
implicit val weekDaysRead: scopt.Read[WeekDays.Value] =
scopt.Read.reads(WeekDays withName _)

默认情况下,这些选项是可选的

Short options

对于普通的标记(opt[Unit]) 短的选项可以被分组为 -fb 来表示 --foo --bar

opt 只接收单个字符, 但是使用 abbr("ab"), 还可以使用字符串:

1
opt[Unit]("no-keepalive").abbr("nk").action( (x, c) => c.copy(keepalive = false) )

Help, Version, and Notes

预定义 action 有一些特殊选项,名为 help("help")version("version"),分别打印用法文本和标题文本。当定义 help("help") 时,解析器将在失败时打印出短错误消息,而不是打印整个 usage 文本。可以通过重写 showUsageOnError 来更改此行为,如下所示:

1
override def showUsageOnError = true

note("...") 用于将给定的字符串添加到 usage 文本中。

Arguments

命令行参数用 arg[A]("<file>") 定义. 它与选项类似,但它接收不含 --- 的值。默认情况下,参数接受单个值并且是必需的。

1
arg[String]("<file>...")

Occurrence

每个 opt/arg 都带有出现信息 minOccursmaxOccursminOccurs 指定 opt/arg 至少必须出现的次数,maxOccurs 指定 opt/arg 最多可能出现的次数。

可以使用 opt/arg 上的方法设置出现次数:

1
2
3
4
5
6
7
opt[String]('o', "out").required()
opt[String]('o', "out").required().withFallback(() => "default value")
opt[String]('o', "out").minOccurs(1) // same as above
arg[String]("<mode>").optional()
arg[String]("<mode>").minOccurs(0) // same as above
arg[String]("<file>...").optional().unbounded()
arg[String]("<file>...").minOccurs(0).maxOccurs(1024) // same as above

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package allinone

import com.typesafe.config.ConfigFactory
import org.apache.spark.sql.SparkSession
import scopt.OptionParser

object SparkFilesArgs extends App {
case class Params(conf: String = "application.conf") // 读取 application.conf 文件中的配置
val parser = new OptionParser[Params]("SparkFilesArgs") {

opt[String]('c', "conf")
.text("config.resource for telematics")
.action((x, c) => c.copy(conf = x))

help("help").text("prints this usage text")
}

// 解析命令行参数
parser.parse(args, Params()) match {
case Some(params) => println(params)
case _ => sys.exit(1)
}

// 本地模式运行,便于测试
val spark = SparkSession.builder()
.appName(this.getClass.getName)
.master("local[3]")
.getOrCreate()

spark.sparkContext.setLogLevel("WARN")

val df = spark.read.csv("/Users/ohmycloud/opt/apache-hive-1.2.2-bin/examples/files/csv.txt")
df.show()

ConfigFactory.invalidateCaches() // 清理配置缓存
lazy val config = ConfigFactory.load()
println(config.origin())
lazy val sparkConf = config.getConfig("spark")
lazy val sparkMaster = sparkConf.getString("master")
lazy val checkPath = sparkConf.getString("checkpoint.path")
println(sparkMaster, checkPath)
spark.stop()
}

提交方式

  • spark-submit 直接提交
1
spark-submit --class allinone.SparkFilesArgs  --driver-memory 2g    --driver-cores 2    --executor-memory 2g    --executor-cores 2    --num-executors 2 --files /Users/ohmycloud/work/cihon/resources/application.pp.env.conf  target/allinone-1.0-SNAPSHOT-shaded.jar --conf hahaha

会打印:

1
Params(hahaha)

如果把这一长串命令写在 shell 里面(spark-submit.sh):

1
spark-submit --class $1  --driver-memory 2g    --driver-cores 2    --executor-memory 2g    --executor-cores 2    --num-executors 2 --files /Users/ohmycloud/work/cihon/resources/application.pp.env.conf  target/allinone-1.0-SNAPSHOT-shaded.jar --conf $2

那么调用方式是这样:

1
spark-submit.sh  allinone.SparkFilesArgs hahaha