对于大多数情况,您不需要使用外部解析器。Scala的模式匹配允许以函数式风格消费参数。例如:
object MmlAlnApp {
val usage = """
Usage: mmlaln [--min-size num] [--max-size num] filename
"""
def main(args: Array[String]) {
if (args.length == 0) println(usage)
val arglist = args.toList
type OptionMap = Map[Symbol, Any]
def nextOption(map : OptionMap, list: List[String]) : OptionMap = {
def isSwitch(s : String) = (s(0) == '-')
list match {
case Nil => map
case "--max-size" :: value :: tail =>
nextOption(map ++ Map('maxsize -> value.toInt), tail)
case "--min-size" :: value :: tail =>
nextOption(map ++ Map('minsize -> value.toInt), tail)
case string :: opt2 :: tail if isSwitch(opt2) =>
nextOption(map ++ Map('infile -> string), list.tail)
case string :: Nil => nextOption(map ++ Map('infile -> string), list.tail)
case option :: tail => println("Unknown option "+option)
exit(1)
}
}
val options = nextOption(Map(),arglist)
println(options)
}
}
例如,将打印以下内容:
Map('infile -> test/data/paml-aln1.phy, 'maxsize -> 4, 'minsize -> 2)
这个版本只接受一个输入文件。很容易进行改进(通过使用List)。
还要注意,这种方法允许连接多个命令行参数——甚至可以超过两个!
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[Unit]("verbose") action { (_, c) =>
c.copy(verbose = true) } text("verbose is a flag")
note("some notes.\n")
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")
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")
)
}
// parser.parse returns Option[C]
parser.parse(args, Config()) map { config =>
// do stuff
} getOrElse {
// arguments are bad, usage message will have been displayed
}
上述代码生成以下使用文本:scopt 3.x
Usage: scopt [update] [options] [<file>...]
-f <value> | --foo <value>
foo is an integer property
-o <file> | --out <file>
out is a required file property
--max:<libname>=<max>
maximum count for <libname>
--verbose
verbose is a flag
some notes.
--help
prints this usage text
<file>...
optional unbounded args
Command: update
update is a command.
-nk | --not-keepalive
disable keepalive
--xyz <value>
xyz is a boolean property
这是我目前在使用的。简洁而不繁琐。 (声明:我现在维护这个项目)
我意识到这个问题被提出已经有一段时间了,但是我觉得这可能会帮助一些正在搜索(像我一样)并且点击了这个页面的人。
Scallop看起来也很有前途。
特性(引自链接的github页面):
- 标志,单值和多值选项
- POSIX风格的短选项名称(-a)与分组(-abc)
- GNU风格的长选项名称(--opt)
- 属性参数(-Dkey=value,-D key1=value key2=value)
- 非字符串类型的选项和属性值(具有可扩展的转换器)
- 强大的匹配尾随参数的功能
- 子命令
还有一些示例代码(也来自于那个Github页面):
import org.rogach.scallop._;
object Conf extends ScallopConf(List("-c","3","-E","fruit=apple","7.2")) {
// all options that are applicable to builder (like description, default, etc)
// are applicable here as well
val count:ScallopOption[Int] = opt[Int]("count", descr = "count the trees", required = true)
.map(1+) // also here work all standard Option methods -
// evaluation is deferred to after option construction
val properties = props[String]('E')
// types (:ScallopOption[Double]) can be omitted, here just for clarity
val size:ScallopOption[Double] = trailArg[Double](required = false)
}
// that's it. Completely type-safe and convenient.
Conf.count() should equal (4)
Conf.properties("fruit") should equal (Some("apple"))
Conf.size.get should equal (Some(7.2))
// passing into other functions
def someInternalFunc(conf:Conf.type) {
conf.count() should equal (4)
}
someInternalFunc(Conf)
(x, c) => c.copy(xyz = x)
。 - WeiChing 林煒清我喜欢使用滑动来处理相对简单的配置参数。
var name = ""
var port = 0
var ip = ""
args.sliding(2, 2).toList.collect {
case Array("--ip", argIP: String) => ip = argIP
case Array("--port", argPort: String) => port = argPort.toInt
case Array("--name", argName: String) => name = argName
}
args.sliding(2, 2)
吗? - m01var port = 0
吗? - swdev这是我的一个工具包(尽管有点晚了)
https://github.com/backuity/clist
与 scopt
不同,它完全可变...但等等!这给了我们一个非常好的语法:
class Cat extends Command(description = "concatenate files and print on the standard output") {
// type-safety: members are typed! so showAll is a Boolean
var showAll = opt[Boolean](abbrev = "A", description = "equivalent to -vET")
var numberNonblank = opt[Boolean](abbrev = "b", description = "number nonempty output lines, overrides -n")
// files is a Seq[File]
var files = args[Seq[File]](description = "files to concat")
}
运行它的简单方法:
Cli.parse(args).withCommand(new Cat) { case cat =>
println(cat.files)
}
当然,您可以做更多的事情(多个命令、许多配置选项等),而且没有依赖关系。
最后我要提到一种独特的特点,即默认用法(经常被忽视的多个命令):
如何在没有外部依赖的情况下解析参数。很好的问题!您可能会对picocli感兴趣。
Picocli专门设计用于解决所提出的问题:它是一个单文件的命令行解析框架,因此您可以将其包含在源代码中。这使得用户可以运行基于picocli的应用程序而无需 picocli 作为外部依赖。
它通过注释字段来工作,因此您只需编写很少的代码。快速摘要:
<command> -xvfInputFile
以及<command> -x -v -f InputFile
)"1..*"
,"3..5"
使用帮助消息很容易通过注释进行自定义(无需编程)。例如:
(源代码)
我忍不住添加了另一个截屏,以展示可能的使用帮助消息类型。使用帮助是您应用程序的面孔,因此要有创意并享受乐趣!
声明:我创建了picocli。非常欢迎反馈或问题。它是用Java编写的,如果在Scala中使用时有任何问题,请告诉我,我会尝试解决。
我来自Java世界,我喜欢args4j,因为它简单易用,规范易读(多亏了注释)并且生成的输出格式漂亮。
这是我的示例代码片段:
import org.kohsuke.args4j.{CmdLineException, CmdLineParser, Option}
object CliArgs {
@Option(name = "-list", required = true,
usage = "List of Nutch Segment(s) Part(s)")
var pathsList: String = null
@Option(name = "-workdir", required = true,
usage = "Work directory.")
var workDir: String = null
@Option(name = "-master",
usage = "Spark master url")
var masterUrl: String = "local[2]"
}
//var args = "-listt in.txt -workdir out-2".split(" ")
val parser = new CmdLineParser(CliArgs)
try {
parser.parseArgument(args.toList.asJava)
} catch {
case e: CmdLineException =>
print(s"Error:${e.getMessage}\n Usage:\n")
parser.printUsage(System.out)
System.exit(1)
}
println("workDir :" + CliArgs.workDir)
println("listFile :" + CliArgs.pathsList)
println("master :" + CliArgs.masterUrl)
Error:Option "-list" is required
Usage:
-list VAL : List of Nutch Segment(s) Part(s)
-master VAL : Spark master url (default: local[2])
-workdir VAL : Work directory.
Person.scala
:import uk.co.flamingpenguin.jewel.cli.Option
trait Person {
@Option def name: String
@Option def times: Int
}
参数接口的一个示例用法Hello.scala
:
import uk.co.flamingpenguin.jewel.cli.CliFactory.parseArguments
import uk.co.flamingpenguin.jewel.cli.ArgumentValidationException
object Hello {
def main(args: Array[String]) {
try {
val person = parseArguments(classOf[Person], args:_*)
for (i <- 1 to (person times))
println("Hello " + (person name))
} catch {
case e: ArgumentValidationException => println(e getMessage)
}
}
}
将以上文件的副本保存到一个目录中,并将JewelCLI 0.6 JAR也下载到该目录。
在Linux/Mac OS X等系统上使用Bash编译并运行示例:
scalac -cp jewelcli-0.6.jar:. Person.scala Hello.scala
scala -cp jewelcli-0.6.jar:. Hello --name="John Doe" --times=3
在 Windows 命令提示符中编译并运行示例:
scalac -cp jewelcli-0.6.jar;. Person.scala Hello.scala
scala -cp jewelcli-0.6.jar;. Hello --name="John Doe" --times=3
Hello John Doe
Hello John Doe
Hello John Doe
我认为scala-optparse-applicative是Scala中功能最强大的命令行解析库。
examples
,它会起作用。 - gpampara我喜欢joslinm的slide()方法,但不喜欢可变变量。因此,这里提供一种不可变的方法:
case class AppArgs(
seed1: String,
seed2: String,
ip: String,
port: Int
)
object AppArgs {
def empty = new AppArgs("", "", "", 0)
}
val args = Array[String](
"--seed1", "akka.tcp://seed1",
"--seed2", "akka.tcp://seed2",
"--nodeip", "192.167.1.1",
"--nodeport", "2551"
)
val argsInstance = args.sliding(2, 1).toList.foldLeft(AppArgs.empty) { case (accumArgs, currArgs) => currArgs match {
case Array("--seed1", seed1) => accumArgs.copy(seed1 = seed1)
case Array("--seed2", seed2) => accumArgs.copy(seed2 = seed2)
case Array("--nodeip", ip) => accumArgs.copy(ip = ip)
case Array("--nodeport", port) => accumArgs.copy(port = port.toInt)
case unknownArg => accumArgs // Do whatever you want for this case
}
}
nextOption
不是一个好的函数名。它是一个返回Map的函数,它具有递归性质只是一种实现细节。这就像为集合编写一个max
函数并将其称为nextMax
,仅仅因为你使用了显式递归。为什么不直接称之为optionMap
呢? - itsbrucelistToOptionMap(lst:List [String])
内含函数nextOption
,并在最后一行写上return nextOption(Map(), lst)
。话虽如此,我必须承认,在我的时间里,我做过比这个答案更严重的捷径。 - tresbotexit(1)
改为sys.exit(1)
。 - tresbotcase string :: tail => { if (isSwitch(string)) { println("Unknown option: " + string) sys.exit(1) } else nextOption(map ++ Map('files -> (string :: map('files))), tail) }
还需要给Map一个默认值Nil
,即:val options = nextOption(Map().withDefaultValue(Nil), args.toList)
我不喜欢使用asInstanceOf
,因为OptionMap
的值是Any
类型。有更好的解决方案吗? - Mauro Lacy