解析命令行参数的最佳方法?

Eug*_*ota 233 command-line scala command-line-parsing

在Scala中解析命令行参数的最佳方法是什么?我个人更喜欢轻量级的东西,不需要外部罐子.

有关:

pjo*_*trp 222

对于大多数情况,您不需要外部解析器.Scala的模式匹配允许以功能样式消费args.例如:

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)
  }
}
Run Code Online (Sandbox Code Playgroud)

将打印,例如:

Map('infile -> test/data/paml-aln1.phy, 'maxsize -> 4, 'minsize -> 2)
Run Code Online (Sandbox Code Playgroud)

这个版本只需要一个infile.易于改进(通过使用List).

另请注意,此方法允许连接多个命令行参数 - 甚至超过两个!

  • `nextOption`不是函数的好名字.它是一个返回映射的函数 - 它是递归的这一事实是一个实现细节.这就像为集合编写一个`max`函数并将其称为`nextMax`只是因为你用显式递归编写它.为什么不把它称为`optionMap`? (6认同)
  • 在`exit(1)`上面的代码中的@theMadKing可能需要是`sys.exit(1)` (5认同)
  • isSwitch只是检查第一个字符是短划线' - ' (4认同)
  • @itsbruce我只想添加/修改你的观点 - 从可读性/可维护性来定义`listToOptionMap(lst:List [String])`并使用在其中定义的函数`nextOption`进行最合适的"最后一行说`return nextOption(Map(),lst)`.也就是说,我必须承认,在我的时间里,我做出了比这个答案中更快捷的捷径. (4认同)
  • 我喜欢你的解决方案.这是处理多个`file`参数的修改:`case string :: tail => {if(isSwitch(string)){println("Unknown option:"+ string)sys.exit(1)} else nextOption(map + + Map('files - >(string :: map('files).asInstanceOf [List [String]])),tail)`.地图还需要一个默认值"Nil",即`val options = nextOption(Map()withDefaultValue Nil,args.toList)`.我不喜欢的是不得不求助于`asInstanceOf`,因为`OptionMap`值的类型是`Any`.有更好的解决方案吗? (3认同)
  • 这是一个处理标志的附加项,即没有关联值的命令行参数:'case“ --verbose” :: tail => nextOption(map ++ Map('verbose-> true),tail)`。稍后,我们可以使用val verbose = options.contains('verbose)`来获取是否存在该标志的布尔值。 (2认同)

Eug*_*ota 194

scopt/scopt

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
}
Run Code Online (Sandbox Code Playgroud)

以上生成以下用法文本:

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
Run Code Online (Sandbox Code Playgroud)

这就是我目前使用的.干净的使用,没有太多的行李.(免责声明:我现在维护这个项目)

  • 具有讽刺意味的是,虽然这个库自动生成了良好的CLI文档,但代码看起来比brainf*ck好一些. (12认同)
  • 如果你使用它来解析一个火花作业的args,请注意它们不能很好地协同工作.字面上没有我试过的东西可以得到火花提交与scopt :-( (9认同)
  • 我更喜欢构建器模式DSL,因为它可以将参数构造委托给模块. (6认同)
  • @BirdJaguarIV如果spark使用scopt可能是问题 - jar中的冲突版本或其他东西.我使用扇贝与火花工作,而没有任何问题. (4认同)
  • 注意:与显示不同,scopt不需要那么多类型的注释. (3认同)
  • @jbrown我发现有趣的是,github中用于scopt使用的顶级搜索结果之一是在[Spark](https://github.com/apache/spark/blob/master/examples/src/main/scala/org/ apache / spark / examples / mllib / SparseNaiveBayes.scala)。但是我对这两个库都没有经验,也不知道两者之间是多么不兼容。 (2认同)
  • @jbrown 不确定这是否仍然与您相关,但我刚刚尝试使用我的 Spark (2.0.0) 应用程序进行 scopt,它对我来说效果很好。 (2认同)

rin*_*ius 55

我意识到这个问题是在前一段时间被问过的,但我认为这可能对一些正在谷歌上搜索的人(比如我)有所帮助,并且点击此页面.

扇贝看起来也很有希望.

功能(从链接的github页面引用):

  • 标志,单值和多值选项
  • POSIX样式的短选项名称(-a),带分组(-abc)
  • GNU样式的长选项名称(--opt)
  • 属性参数(-Dkey = value,-D key1 = value key2 = value)
  • 非字符串类型的选项和属性值(带可扩展转换器)
  • 在尾随args上强大匹配
  • 子命令

还有一些示例代码(也来自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)
Run Code Online (Sandbox Code Playgroud)

  • Scallop在功能方面将其余的手放下.耻辱通常的SO趋势"第一次回答胜利"已经推动了这个列表:( (4认同)
  • 这比 scopt 更直观,更少样板。在 scopt 中不再有 `(x, c) =&gt; c.copy(xyz = x) ` (3认同)

jos*_*inm 34

我喜欢滑动参数以获得相对简单的配置.

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
}
Run Code Online (Sandbox Code Playgroud)

  • 聪明.只有在每个arg也指定一个值时才有效,对吧? (2认同)
  • 它应该不是`args.sliding(2,2)`? (2认同)

Bru*_*eth 16

命令行界面Scala Toolkit(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")
}
Run Code Online (Sandbox Code Playgroud)

还有一个简单的方法来运行它:

Cli.parse(args).withCommand(new Cat) { case cat =>
    println(cat.files)
}
Run Code Online (Sandbox Code Playgroud)

你当然可以做更多的事情(多命令,许多配置选项......)并且没有依赖性.

我将完成一个与众不同的功能,默认用法(对于多个命令经常被忽略): CLIST


Ala*_*Dea 13

这在很大程度上是对我对同一主题的Java问题的回答的无耻克隆.事实证明,JewelCLI是Scala友好的,因为它不需要JavaBean样式方法来获得自动参数命名.

JewelCLI是一个Scala友好的Java库,用于命令行解析,产生干净的代码.它使用配置注释的Proxied Interfaces为命令行参数动态构建类型安全的API.

一个示例参数接口Person.scala:

import uk.co.flamingpenguin.jewel.cli.Option

trait Person {
  @Option def name: String
  @Option def times: Int
}
Run Code Online (Sandbox Code Playgroud)

参数接口的示例用法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)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

将上述文件的副本保存到单个目录中,并将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
Run Code Online (Sandbox Code Playgroud)

在Windows命令提示符中编译并运行该示例:

scalac -cp jewelcli-0.6.jar;. Person.scala Hello.scala
scala -cp jewelcli-0.6.jar;. Hello --name="John Doe" --times=3
Run Code Online (Sandbox Code Playgroud)

运行该示例应该产生以下输出:

Hello John Doe
Hello John Doe
Hello John Doe
Run Code Online (Sandbox Code Playgroud)


Rem*_*pma 11

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

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

它通过注释字段来工作,因此您编写的代码非常少.快速摘要:

  • 强力输入所有内容 - 命令行选项以及位置参数
  • 对于POSIX支持集群短选项(因此处理<command> -xvfInputFile以及<command> -x -v -f InputFile)
  • 允许最小,最大和可变数量参数的arity模型,例如"1..*","3..5"
  • 流畅而紧凑的API,可最大限度地减少样板客户端代码
  • 子命令
  • 使用ANSI颜色的帮助

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

扩展使用帮助消息(来源)

我无法抗拒添加一个截图,以显示可能的用法帮助消息.使用帮助是您的应用程序的面貌,所以要有创意,玩得开心!

picocli演示

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

  • 为什么投反对票?这是我所知道的唯一一个*专门*旨在解决 OP 中提到的问题的库:如何避免添加依赖项。 (3认同)

Ken*_*ida 10

斯卡拉-optparse,应用性

我认为scala-optparse-applicative是Scala中功能最强大的命令行解析器库.

https://github.com/bmjames/scala-optparse-applicative


Tha*_*wda 10

我来自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]"

}
Run Code Online (Sandbox Code Playgroud)

解析

//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)
Run Code Online (Sandbox Code Playgroud)

在无效的参数上

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.
Run Code Online (Sandbox Code Playgroud)


Ced*_*ust 8

还有JCommander(免责声明:我创建了它):

object Main {
  object Args {
    @Parameter(
      names = Array("-f", "--file"),
      description = "File to load. Can be specified multiple times.")
    var file: java.util.List[String] = null
  }

  def main(args: Array[String]): Unit = {
    new JCommander(Args, args.toArray: _*)
    for (filename <- Args.file) {
      val f = new File(filename)
      printf("file: %s\n", f.getName)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 我喜欢这一个.那些'纯粹的scala'解析器缺乏干净的语法 (2认同)

hag*_*ggy 6

我喜欢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
  }
}
Run Code Online (Sandbox Code Playgroud)