Scala scopt 命令行解析
参考:https://github.com/scopt/scopt
scopt is a little command line options parsing library.
Sonatype
libraryDependencies += "com.github.scopt" %% "scopt" % "X.Y.Z"
See the Maven Central badge above.
Usage
scopt 4.x provides two styles of constructing a command line option parser: functional DSL and object-oriented DSL. Either case, first you need a case class that represents the configuration:
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())
During the parsing process, a config object is passed around as an argument into
action
callbacks.Functional DSL
Here's how you create a
scopt.OParser[Config]
.import scopt.OParser val builder = OParser.builder[Config] val parser1 = { import builder._ OParser.sequence( programName("scopt"), head("scopt", "4.x"), // option -f, --foo opt[Int]('f', "foo") .action((x, c) => c.copy(foo = x)) .text("foo is an integer property"), // more options here... ) } // OParser.parse returns Option[Config] OParser.parse(parser1, args, Config()) match { case Some(config) => // do something case _ => // arguments are bad, error message will have been displayed }
See Scaladoc API and the rest of this page for the details on various builder methods.
Full example
import scopt.OParser val builder = OParser.builder[Config] val parser1 = { import builder._ OParser.sequence( programName("scopt"), head("scopt", "4.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." + sys.props("line.separator")), 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) ) ) } // OParser.parse returns Option[Config] OParser.parse(parser1, args, Config()) match { case Some(config) => // do something case _ => // arguments are bad, error message will have been displayed }
The above generates the following usage text:
scopt 4.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
Command line options are defined using
opt[A]('f', "foo")
oropt[A]("foo")
whereA
is any type that is an instance ofRead
typeclass.
Unit
works as a plain flag--foo
or-f
Int
,Long
,Double
,String
,BigInt
,BigDecimal
,java.io.File
,java.net.URI
, andjava.net.InetAddress
accept a value like--foo 80
or--foo:80
Boolean
accepts a value like--foo true
or--foo:1
java.util.Calendar
accepts a value like--foo 2000-12-01
scala.concurrent.duration.Duration
accepts a value like--foo 30s
- A pair of types like
(String, Int)
accept a key-value like--foo:k=1
or-f k=1
- A
Seq[File]
accepts a string containing comma-separated values such as--jars foo.jar,bar.jar
- A
Map[String, String]
accepts a string containing comma-separated pairs like--kwargs key1=val1,key2=val2
This could be extended by defining
Read
instances in the scope. For example,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 _)
By default these options are optional.
Short options
For plain flags (
opt[Unit]
) short options can be grouped as-fb
to mean--foo --bar
.
opt
accepts only a single character, but usingabbr("ab")
a string can be used too:opt[Unit]("no-keepalive").abbr("nk").action( (x, c) => c.copy(keepalive = false) )
Help, Version, and Notes
There are special options with predefined action called
help("help")
andversion("version")
, which prints usage text and header text respectively. Whenhelp("help")
is defined, parser will print out short error message when it fails instead of printing the entire usage text.
note("...")
is used add given string to the usage text.Arguments
Command line arguments are defined using
arg[A]("<file>")
. It works similar to options, but instead it accepts values without--
or-
. By default, arguments accept a single value and are required.arg[String]("<file>...")
Occurrence
Each opt/arg carries occurrence information
minOccurs
andmaxOccurs
.minOccurs
specify at least how many times an opt/arg must appear, andmaxOccurs
specify at most how many times an opt/arg may appear.Occurrence can be set using the methods on the opt/arg:
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
Visibility
Each opt/arg can be hidden from the usage text using
hidden()
method:opt[Unit]("debug") .hidden() .action( (_, c) => c.copy(debug = true) ) .text("this option is hidden in the usage text")
Validation
Each opt/arg can carry multiple validation functions.
opt[Int]('f', "foo") .action( (x, c) => c.copy(intValue = x) ) .validate( x => if (x > 0) success else failure("Option --foo must be >0") ) .validate( x => failure("Just because") )
The first function validates if the values are positive, and the second function always fails.
Check configuration
Consistency among the option values can be checked using
checkConfig
.checkConfig( c => if (c.keepalive && c.xyz) failure("xyz cannot keep alive") else success )
These are called at the end of parsing.
Commands
Commands may be defined using
cmd("update")
. Commands could be used to expressgit branch
kind of argument, whose name means something. Usingchildren
method, a command may define child opts/args that get inserted in the presence of the command. To distinguish commands from arguments, they must appear in the first position within the level. It is generally recommended to avoid mixing args both in parent level and commands to avoid confusion.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"), checkConfig( c => if (c.keepalive && c.xyz) failure("xyz cannot keep alive") else success ) )
In the above,
update test.txt
would trigger the update command, buttest.txt update
won't.Commands could be nested into another command as follows:
cmd("backend") .text("commands to manipulate backends:\n") .action( (x, c) => c.copy(flag = true) ) .children( cmd("update").children( arg[String]("<a>").action( (x, c) => c.copy(a = x) ) ) )
Object-oriented DSL, immutable parsing
Here's the object-oriented DSL that's mostly source-compatible with scopt 3.x.
Create a parser by extending
scopt.OptionParser[Config]
. See Scaladoc API for the details on various builder methods.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") } // 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 }
Object-oriented DSL, mutable parsing
Create a
scopt.OptionParser[Unit]
and customize it with the options you need, passing in functions to process each option or argument. Useforeach
instead ofaction
.val parser = new scopt.OptionParser[Unit]("scopt") { head("scopt", "3.x") opt[Int]('f', "foo") .foreach( x => c = c.copy(foo = x) ) .text("foo is an integer property") opt[File]('o', "out") .required() .valueName("<file>") .foreach( x => c = c.copy(out = x) ) .text("out is a required file property") } if (parser.parse(args)) { // do stuff } else { // arguments are bad, usage message will have been displayed }
Advanced: showUsageOnError
When
help("help")
is defined, parser will print out short error message when it fails instead of printing the entire usage text.This behavior could be changed by overriding
showUsageOnError
as follows:import scopt.{ OParserSetup, DefaultOParserSetup } val setup: OParserSetup = new DefaultOParserSetup { override def showUsageOnError = Some(true) } val result = OParser.parse(parser1, args, Config(), setup)
Advanced: Termination handling
By default, when the
--help
or--version
are invoked, they callsys.exit(0)
after printing the help or version information. If this is not desired (e.g. testing purposes), you can override theterminate(exitState: Either[String, Unit])
method:import scopt.{ OParserSetup, DefaultOParserSetup } val setup: OParserSetup = new DefaultOParserSetup { // Overriding the termination handler to no-op. override def terminate(exitState: Either[String, Unit]): Unit = () } val result = OParser.parse(parser1, args, Config(), setup)
Advanced: Captured output
By default, scopt emits output when needed to stderr and stdout. This is expected behavior when using scopt to process arguments for your stand-alone application. However, if your application requires parsing arguments while not producing output directly, you may wish to capture stderr and stdout output rather than emit them directly. Redirecting Console in Scala can accomplish this in a thread-safe way, within a scope of your chosing, like this:
val outCapture = new ByteArrayOutputStream val errCapture = new ByteArrayOutputStream Console.withOut(outCapture) { Console.withErr(errCapture) { val result = OParser.parse(parser1, args, Config()) } } // Now stderr output from this block is in errCapture.toString, and stdout in outCapture.toString
Advanced: Rendering mode
scopt 3.5.0 introduced rendering mode, and adopted two-column rendeing of the usage text by default. To switch back to the older one-column rendering override the
renderingMode
method:import scopt.{ OParserSetup, DefaultOParserSetup } val setup: OParserSetup = new DefaultOParserSetup { override def renderingMode = scopt.RenderingMode.OneColumn } val result = OParser.parse(parser1, args, Config(), setup)
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决