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

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

相关:

155853 次浏览

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
}

上面生成了以下用法文本:

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

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

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

这个版本只需要一个文件。很容易改进(通过使用列表)。

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

这在很大程度上是我对同一主题的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
}

参数interface 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/etc / 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

我刚刚在scalac的scala.tools.cmd包中找到了一个广泛的命令行解析库。

看到http://www.assembla.com/code/scala-eclipse-toolchain/git/nodes/src/compiler/scala/tools/cmd?rev=f59940622e32384b1e08939effd24e924a8ba8db

另一个库:scarg

下面是一个容易使用的Scala命令行解析器。它自动格式化帮助文本,并将开关参数转换为所需的类型。短POSIX和长GNU样式开关都受支持。支持带必选参数、可选参数和多值参数的开关。您甚至可以为特定开关指定可接受值的有限列表。为了方便,可以在命令行上缩写较长的交换机名称。类似于Ruby标准库中的选项解析器。

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

我知道这个问题是很久以前问过的,但我认为它可能会帮助一些人,谁是谷歌周围(像我),并点击这个页面。

扇贝看起来也很有希望。

特点(引用自链接的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)

我已经尝试概括@pjotrp的解决方案,通过在必要的位置键符号的列表,一个映射的标志->键符号和默认选项:

def parseOptions(args: List[String], required: List[Symbol], optional: Map[String, Symbol], options: Map[Symbol, String]): Map[Symbol, String] = {
args match {
// Empty list
case Nil => options


// Keyword arguments
case key :: value :: tail if optional.get(key) != None =>
parseOptions(tail, required, optional, options ++ Map(optional(key) -> value))


// Positional arguments
case value :: tail if required != Nil =>
parseOptions(tail, required.tail, optional, options ++ Map(required.head -> value))


// Exit if an unknown argument is received
case _ =>
printf("unknown argument(s): %s\n", args.mkString(", "))
sys.exit(1)
}
}


def main(sysargs Array[String]) {
// Required positional arguments by key in options
val required = List('arg1, 'arg2)


// Optional arguments by flag which map to a key in options
val optional = Map("--flag1" -> 'flag1, "--flag2" -> 'flag2)


// Default options that are passed in
var defaultOptions = Map()


// Parse options based on the command line args
val options = parseOptions(sysargs.toList, required, optional, defaultOptions)
}

我喜欢这段代码的干净外观…从这里的讨论中收集到: http://www.scala-lang.org/old/node/4380 < / p >

object ArgParser {
val usage = """
Usage: parser [-v] [-f file] [-s sopt] ...
Where: -v   Run verbosely
-f F Set input file to F
-s S Set Show option to S
"""


var filename: String = ""
var showme: String = ""
var debug: Boolean = false
val unknown = "(^-[^\\s])".r


val pf: PartialFunction[List[String], List[String]] = {
case "-v" :: tail => debug = true; tail
case "-f" :: (arg: String) :: tail => filename = arg; tail
case "-s" :: (arg: String) :: tail => showme = arg; tail
case unknown(bad) :: tail => die("unknown argument " + bad + "\n" + usage)
}


def main(args: Array[String]) {
// if there are required args:
if (args.length == 0) die()
val arglist = args.toList
val remainingopts = parseArgs(arglist,pf)


println("debug=" + debug)
println("showme=" + showme)
println("filename=" + filename)
println("remainingopts=" + remainingopts)
}


def parseArgs(args: List[String], pf: PartialFunction[List[String], List[String]]): List[String] = args match {
case Nil => Nil
case _ => if (pf isDefinedAt args) parseArgs(pf(args),pf) else args.head :: parseArgs(args.tail,pf)
}


def die(msg: String = usage) = {
println(msg)
sys.exit(1)
}


}

我从来都不喜欢ruby那样的选项解析器。大多数使用它们的开发人员从来没有为他们的脚本编写一个正确的手册页,并且由于解析器的原因,最终会有几页长的选项没有以正确的方式组织。

我一直更喜欢用Perl的Getopt:: Long来做事情。

我正在研究它的scala实现。早期的API看起来是这样的:

def print_version() = () => println("version is 0.2")


def main(args: Array[String]) {
val (options, remaining) = OptionParser.getOptions(args,
Map(
"-f|--flag"       -> 'flag,
"-s|--string=s"   -> 'string,
"-i|--int=i"      -> 'int,
"-f|--float=f"    -> 'double,
"-p|-procedure=p" -> { () => println("higher order function" }
"-h=p"            -> { () => print_synopsis() }
"--help|--man=p"  -> { () => launch_manpage() },
"--version=p"     -> print_version,
))

所以像这样调用script:

$ script hello -f --string=mystring -i 7 --float 3.14 --p --version world -- --nothing

将打印:

higher order function
version is 0.2

并返回:

remaining = Array("hello", "world", "--nothing")


options = Map('flag   -> true,
'string -> "mystring",
'int    -> 7,
'double -> 3.14)

该项目托管在github scala-getoptions

正如每个人都发布了自己的解决方案,这里是我的,因为我想为用户写一些更容易的东西:https://gist.github.com/gwenzek/78355526e476e08bb34d

要点包含一个代码文件,一个测试文件和一个简短的示例复制在这里:

import ***.ArgsOps._




object Example {
val parser = ArgsOpsParser("--someInt|-i" -> 4, "--someFlag|-f", "--someWord" -> "hello")


def main(args: Array[String]){
val argsOps = parser <<| args
val someInt : Int = argsOps("--someInt")
val someFlag : Boolean = argsOps("--someFlag")
val someWord : String = argsOps("--someWord")
val otherArgs = argsOps.args


foo(someWord, someInt, someFlag)
}
}

没有什么特别的选项可以强制变量在某些边界内,因为我觉得解析器不是这样做的最佳场所。

注意:对于一个给定的变量,你可以有任意多的别名。

我刚刚创建了我的简单列举

val args: Array[String] = "-silent -samples 100 -silent".split(" +").toArray
//> args  : Array[String] = Array(-silent, -samples, 100, -silent)
object Opts extends Enumeration {


class OptVal extends Val {
override def toString = "-" + super.toString
}


val nopar, silent = new OptVal() { // boolean options
def apply(): Boolean = args.contains(toString)
}


val samples, maxgen = new OptVal() { // integer options
def apply(default: Int) = { val i = args.indexOf(toString) ;  if (i == -1) default else args(i+1).toInt}
def apply(): Int = apply(-1)
}
}


Opts.nopar()                              //> res0: Boolean = false
Opts.silent()                             //> res1: Boolean = true
Opts.samples()                            //> res2: Int = 100
Opts.maxgen()                             //> res3: Int = -1

我知道这个解决方案有两个主要的缺陷可能会让你分心:它消除了自由(即对其他库的依赖,你非常重视)和冗余(DRY原则,你只输入一次选项名称,作为Scala程序变量,并在第二次输入命令行文本时消除它)。

scala-optparse-applicative

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

https://github.com/bmjames/scala-optparse-applicative < a href = " https://github.com/bmjames/scala-optparse-applicative " > < / >

对于相对简单的配置,我更喜欢滑动而不是参数。

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
}

我再多说一点。我用一行简单的代码解决了这个问题。我的命令行参数是这样的:

input--hdfs:/path/to/myData/part-00199.avro output--hdfs:/path/toWrite/Data fileFormat--avro option1--5

这将通过Scala的本机命令行功能(从App或main方法)创建一个数组:

Array("input--hdfs:/path/to/myData/part-00199.avro", "output--hdfs:/path/toWrite/Data","fileFormat--avro","option1--5")

然后我可以使用这一行来解析出默认的args数组:

val nArgs = args.map(x=>x.split("--")).map(y=>(y(0),y(1))).toMap

它创建了一个与命令行值关联的名称映射:

Map(input -> hdfs:/path/to/myData/part-00199.avro, output -> hdfs:/path/toWrite/Data, fileFormat -> avro, option1 -> 5)

然后,我可以在代码中访问命名参数的值,它们在命令行上出现的顺序不再相关。我意识到这相当简单,没有上面提到的所有高级功能,但在大多数情况下似乎足够了,只需要一行代码,并且不涉及外部依赖关系。

我建议使用http://docopt.org/。有一个scala端口,但Java实现https://github.com/docopt/docopt.java工作得很好,似乎维护得更好。这里有一个例子:

import org.docopt.Docopt


import scala.collection.JavaConversions._
import scala.collection.JavaConverters._


val doc =
"""
Usage: my_program [options] <input>


Options:
--sorted   fancy sorting
""".stripMargin.trim


//def args = "--sorted test.dat".split(" ").toList
var results = new Docopt(doc).
parse(args()).
map {case(key, value)=>key ->value.toString}


val inputFile = new File(results("<input>"))
val sorted = results("--sorted").toBoolean

这是我的一行字

    def optArg(prefix: String) = args.drop(3).find { _.startsWith(prefix) }.map{_.replaceFirst(prefix, "")}
def optSpecified(prefix: String) = optArg(prefix) != None
def optInt(prefix: String, default: Int) = optArg(prefix).map(_.toInt).getOrElse(default)

它删除了3个强制参数,并给出了选项。整数被指定为臭名昭著的-Xmx<size> java选项,与前缀一起。您可以像这样简单地解析二进制和整数

val cacheEnabled = optSpecified("cacheOff")
val memSize = optInt("-Xmx", 1000)

不需要进口任何东西。

这是我做的。它返回一个map和list的元组。列表是用于输入的,就像输入文件名一样。Map用于开关/选项。

val args = "--sw1 1 input_1 --sw2 --sw3 2 input_2 --sw4".split(" ")
val (options, inputs) = OptParser.parse(args)

将返回

options: Map[Symbol,Any] = Map('sw1 -> 1, 'sw2 -> true, 'sw3 -> 2, 'sw4 -> true)
inputs: List[Symbol] = List('input_1, 'input_2)

开关可以是“——t”,x将被设置为true,或者“——x 10”,x将被设置为“10”。其他的都将在列表中结束。

object OptParser {
val map: Map[Symbol, Any] = Map()
val list: List[Symbol] = List()


def parse(args: Array[String]): (Map[Symbol, Any], List[Symbol]) = _parse(map, list, args.toList)


private [this] def _parse(map: Map[Symbol, Any], list: List[Symbol], args: List[String]): (Map[Symbol, Any], List[Symbol]) = {
args match {
case Nil => (map, list)
case arg :: value :: tail if (arg.startsWith("--") && !value.startsWith("--")) => _parse(map ++ Map(Symbol(arg.substring(2)) -> value), list, tail)
case arg :: tail if (arg.startsWith("--")) => _parse(map ++ Map(Symbol(arg.substring(2)) -> true), list, tail)
case opt :: tail => _parse(map, list :+ Symbol(opt), tail)
}
}
}

命令行接口Scala Toolkit (CLIST)

这也是我的!(虽然有点晚了)

https://github.com/backuity/clist < a href = " 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

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

穷人用来解析键=值对的快速而肮脏的一行代码:

def main(args: Array[String]) {
val cli = args.map(_.split("=") match { case Array(k, v) => k->v } ).toMap
val saveAs = cli("saveAs")
println(saveAs)
}

我的方法基于上面的答案(来自dave4420),并试图通过使其更通用来改进它。

返回所有命令行参数的Map[String,String] 你可以查询你想要的特定参数(例如使用.contains)或将值转换为你想要的类型(例如使用toInt)

def argsToOptionMap(args:Array[String]):Map[String,String]= {
def nextOption(
argList:List[String],
map:Map[String, String]
) : Map[String, String] = {
val pattern       = "--(\\w+)".r // Selects Arg from --Arg
val patternSwitch = "-(\\w+)".r  // Selects Arg from -Arg
argList match {
case Nil => map
case pattern(opt)       :: value  :: tail => nextOption( tail, map ++ Map(opt->value) )
case patternSwitch(opt) :: tail => nextOption( tail, map ++ Map(opt->null) )
case string             :: Nil  => map ++ Map(string->null)
case option             :: tail => {
println("Unknown option:"+option)
sys.exit(1)
}
}
}
nextOption(args.toList,Map())
}

例子:

val args=Array("--testing1","testing1","-a","-b","--c","d","test2")
argsToOptionMap( args  )

给:

res0: Map[String,String] = Map(testing1 -> testing1, a -> null, b -> null, c -> d, test2 -> null)

我喜欢joslinm的slide()方法,而不是可变的vars;)这里有一个不可改变的方法:

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

freecli

package freecli
package examples
package command


import java.io.File


import freecli.core.all._
import freecli.config.all._
import freecli.command.all._


object Git extends App {


case class CommitConfig(all: Boolean, message: String)
val commitCommand =
cmd("commit") {
takesG[CommitConfig] {
O.help --"help" ::
flag --"all" -'a' -~ des("Add changes from all known files") ::
O.string -'m' -~ req -~ des("Commit message")
} ::
runs[CommitConfig] { config =>
if (config.all) {
println(s"Commited all ${config.message}!")
} else {
println(s"Commited ${config.message}!")
}
}
}


val rmCommand =
cmd("rm") {
takesG[File] {
O.help --"help" ::
file -~ des("File to remove from git")
} ::
runs[File] { f =>
println(s"Removed file ${f.getAbsolutePath} from git")
}
}


val remoteCommand =
cmd("remote") {
takes(O.help --"help") ::
cmd("add") {
takesT {
O.help --"help" ::
string -~ des("Remote name") ::
string -~ des("Remote url")
} ::
runs[(String, String)] {
case (s, u) => println(s"Remote $s $u added")
}
} ::
cmd("rm") {
takesG[String] {
O.help --"help" ::
string -~ des("Remote name")
} ::
runs[String] { s =>
println(s"Remote $s removed")
}
}
}


val git =
cmd("git", des("Version control system")) {
takes(help --"help" :: version --"version" -~ value("v1.0")) ::
commitCommand ::
rmCommand ::
remoteCommand
}


val res = runCommandOrFail(git)(args).run
}

这将产生以下用法:

Usage

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

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

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

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

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

扩展使用帮助信息 ()

我忍不住又加了一张截图来展示使用帮助信息的类型。使用帮助是应用程序的门面,所以要有创意,玩得开心!

picocli demo

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