在Scala中如何解析命令行参数?

261
26个回答

249

对于大多数情况,您不需要使用外部解析器。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)。

还要注意,这种方法允许连接多个命令行参数——甚至可以超过两个!


4
isSwitch 函数只是检查第一个字符是否为短横线 '-'。 - pjotrp
8
nextOption不是一个好的函数名。它是一个返回Map的函数,它具有递归性质只是一种实现细节。这就像为集合编写一个max函数并将其称为nextMax,仅仅因为你使用了显式递归。为什么不直接称之为optionMap呢? - itsbruce
4
@itsbruce我只是想补充/修改你的观点 - 从可读性和可维护性来看,最好定义listToOptionMap(lst:List [String])内含函数nextOption,并在最后一行写上return nextOption(Map(), lst)。话虽如此,我必须承认,在我的时间里,我做过比这个答案更严重的捷径。 - tresbot
7
在上面的代码中,"@theMadKing" 建议将 exit(1) 改为 sys.exit(1) - tresbot
4
我喜欢你的解决方案。以下是处理多个“文件”参数的修改:case 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
显示剩余5条评论

207

scopt/scopt


这是一个指向scopt/scopt的链接,它可能是一个GitHub项目或仓库的名称。
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

这是我目前在使用的。简洁而不繁琐。 (声明:我现在维护这个项目)


6
我更喜欢建造者模式的DSL,因为它可以将参数构建委托给模块。 - Daniel C. Sobral
3
注意:与所示不同,scopt 不需要那么多类型注释。 - Blaisorblade
11
如果您正在使用它来解析spark任务的参数,请注意它们无法很好地配合使用。实际上,我尝试过的所有方法都无法让spark-submit与scopt一起正常工作 :-( - jbrown
5
如果Spark使用了scopt,那很可能是问题所在——可能是jar包中的版本冲突或其他原因。相反,我使用scallop来代替Spark作业,并没有遇到任何问题。 - jbrown
17
具有讽刺意味的是,虽然这个库可以自动生成很好的 CLI 文档,但代码看起来却不比 brainf*ck 好多少。 - Coder Guy
显示剩余9条评论

64

我意识到这个问题被提出已经有一段时间了,但是我觉得这可能会帮助一些正在搜索(像我一样)并且点击了这个页面的人。

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)

6
扇贝在功能方面毫无疑问比其他所有的贝类都要强。可惜通常的 Stack Overflow(一个问答网站)“先回答者胜出”的趋势使得这个问题被推到了列表下方。 - samthebest
我同意。在这里留下评论,以防@Eugene Yokota忘记记录。查看这篇博客scallop - Pramit
1
它提到的scopt问题是“看起来不错,但无法解析需要参数列表的选项(即-a 1 2 3)。您没有办法扩展它以获取这些列表(除了分叉lib)。”但这已经不再是真的了,请参见https://github.com/scopt/scopt#options。 - Alexey Romanov
4
这比scopt更直观,更简洁。在scopt中不再需要(x, c) => c.copy(xyz = x) - WeiChing 林煒清

58

我喜欢使用滑动来处理相对简单的配置参数。

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
}

3
聪明。但是只有当每个参数也指定一个值时才有效,对吗? - Brent Faust
2
难道不应该是 args.sliding(2, 2) 吗? - m01
1
难道不应该是 var port = 0 吗? - swdev

19

命令行界面 Scala 工具包 (CLIST)

这是我的一个工具包(尽管有点晚了)

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)
}

当然,您可以做更多的事情(多个命令、许多配置选项等),而且没有依赖关系。

最后我要提到一种独特的特点,即默认用法(经常被忽视的多个命令): clist


它有验证吗? - K F
是的,它可以(请参见https://github.com/backuity/clist/blob/master/demo/src/main/scala/PuppetModuleInstaller.scala#L18以获取示例)。不过没有文件记录... 要提交请求吗? :) - Bruno Bieth
尝试过了,非常方便。之前我用过scopt,但我仍然不习惯将验证添加在每个参数的定义中,而不是一起添加。但它对我来说很有效。在不同的traits中定义不同的参数和验证,然后在不同的情况下组合它们,这真的很有帮助。在scopt中,当重复使用参数不方便时,我曾经遭受很多痛苦。谢谢回复! - K F
大多数验证是在命令行参数反序列化期间执行的(请参见Read),因此,如果您可以通过类型定义验证约束(即PasswordHex等),那么您可以利用这一点。 - Bruno Bieth

14

如何在没有外部依赖的情况下解析参数。很好的问题!您可能会对picocli感兴趣。

Picocli专门设计用于解决所提出的问题:它是一个单文件的命令行解析框架,因此您可以将其包含在源代码中。这使得用户可以运行基于picocli的应用程序而无需 picocli 作为外部依赖

它通过注释字段来工作,因此您只需编写很少的代码。快速摘要:

  • 强类型化所有内容-命令行选项以及位置参数
  • 支持POSIX聚合短选项(因此它处理<command> -xvfInputFile以及<command> -x -v -f InputFile
  • 一种数量模型,允许最小、最大和可变数量的参数,例如"1..*""3..5"
  • 流畅且紧凑的API,以最小化样板客户端代码
  • 子命令
  • 带有ANSI颜色的使用帮助

使用帮助消息很容易通过注释进行自定义(无需编程)。例如:

扩展使用帮助消息 (源代码)

我忍不住添加了另一个截屏,以展示可能的使用帮助消息类型。使用帮助是您应用程序的面孔,因此要有创意并享受乐趣!

picocli演示

声明:我创建了picocli。非常欢迎反馈或问题。它是用Java编写的,如果在Scala中使用时有任何问题,请告诉我,我会尝试解决。


3
为什么要踩这个帖子呢?这是我所知道的唯一一个专门设计来解决OP提到的问题的库:如何避免添加依赖关系。 - Remko Popma
鼓励应用程序作者将其包含在内。干得好。 - cuz
你有Scala的例子吗? - CruncherBigData
1
我已经开始为其他JVM语言创建示例:https://github.com/remkop/picocli/issues/183 欢迎反馈和贡献! - Remko Popma

13

我来自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.

13
这主要是我对同一主题的Java问题的回答的一个无耻克隆。结果发现JewelCLI对Scala很友好,因为它不需要JavaBean样式的方法来获得自动参数命名。
JewelCLI是一个Scala友好的Java库,用于解析命令行并产生清晰的代码。它使用带注释的代理接口配置来动态构建类型安全的API,以处理命令行参数。
一个示例参数接口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

在这里你可能会发现一个有趣的细节,那就是 (args : _*)。从Scala调用Java的可变参数方法需要这个。这是我从Jesse Eichar的博客Daily Scala中学到的解决方案。我强烈推荐Daily Scala :) - Alain O'Dea

11

除了 README 中的内容,它还有其他示例或文档吗? - Erik Kaplun
1
请查看测试代码中的examples,它会起作用。 - gpampara

9

我喜欢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
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接