MainArgs is a small, dependency-free library for command line argument parsing in Scala.
MainArgs is used for command-line parsing of the Ammonite Scala REPL and for user-defined @main methods in its scripts, as well as for command-line parsing for the Mill Build Tool and for user-defined T.commands.
ivy"com.lihaoyi::mainargs:0.7.7"You can parse command line arguments and use them to call a main method via ParserForMethods(...):
package testhello import mainargs.{main, arg, ParserForMethods, Flag} object Main{ @main def run(@arg(short = 'f', doc = "String to print repeatedly") foo: String, @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag, can be passed without any value to become true") bool: Flag) = { println(foo * myNum + " " + bool.value) } def main(args: Array[String]): Unit = Parser(this).runOrExit(args) }$ ./mill example.hello -f hello # short name hellohello false $ ./mill example.hello --foo hello # long name hellohello false $ ./mill example.hello --foo=hello # gflags-style hellohello false $ ./mill example.hello --foo "" # set to empty value false $ ./mill example.hello --foo= # gflags-style empty value false $ ./mill example.hello -f x --my-num 3 # camelCase automatically converted to kebab-case xxx false $ ./mill example.hello -f hello --my-num 3 --bool # flags hellohellohello true $ ./mill example.hello --wrong-flag Missing argument: --foo <str> Unknown argument: "--wrong-flag" Expected Signature: run -f --foo <str> String to print repeatedly --my-num <int> How many times to print string --bool Example flagSetting default values for the method arguments makes them optional, with the default value being used if an explicit value was not passed in from the command-line arguments list.
After calling Parser(...) on the object containing your @main methods, you can call the following methods to perform the argument parsing and dispatch:
Runs the given main method if argument parsing succeeds, otherwise prints out the help text to standard error and calls System.exit(1) to exit the process
Runs the given main method if argument parsing succeeds, otherwise throws an exception with the help text
Runs the given main method if argument parsing succeeds, returning Right(v: Any) containing the return value of the main method if it succeeds, or Left(s: String) containing the error message if it fails.
Runs the given main method if argument parsing succeeds, returning mainargs.Result.Success(v: Any) containing the return value of the main method if it succeeds, or mainargs.Result.Error if it fails. This gives you the greatest flexibility to handle the error cases with custom logic, e.g. if you do not like the default CLI error reporting and would like to write your own.
Programs with multiple entrypoints are supported by annotating multiple defs with @main. Each entrypoint can have their own set of arguments:
package testhello2 import mainargs.{main, arg, Parser, Flag} object Main{ @main def foo(@arg(short = 'f', doc = "String to print repeatedly") foo: String, @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag") bool: Flag) = { println(foo * myNum + " " + bool.value) } @main def bar(i: Int, @arg(doc = "Pass in a custom `s` to override it") s: String = "lols") = { println(s * i) } def main(args: Array[String]): Unit = Parser(this).runOrExit(args) }$ ./mill example.hello2 Need to specify a sub command: foo, bar $ ./mill example.hello2 foo -f hello hellohello false $ ./mill example.hello2 bar -i 10 lolslolslolslolslolslolslolslolslolslolsIf you want to construct a configuration object instead of directly calling a method, you can do so via Parser[T] and `constructOrExit:
package testclass import mainargs.{main, arg, Parser, Flag} object Main{ @main case class Config(@arg(short = 'f', doc = "String to print repeatedly") foo: String, @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag") bool: Flag) def main(args: Array[String]): Unit = { val config = Parser[Config].constructOrExit(args) println(config) } }$ ./mill example.caseclass --foo "hello" Config(hello,2,Flag(false)) $ ./mill example.caseclass Missing argument: --foo <str> Expected Signature: apply -f --foo <str> String to print repeatedly --my-num <int> How many times to print string --bool Example flagParser[T] also provides corresponding constructOrThrow, constructEither, or constructRaw methods for you to handle the error cases in whichever style you prefer.
You can share arguments between different @main methods by defining them in a @main case class configuration object with an implicit Parser[T] defined:
package testclassarg import mainargs.{main, arg, Parser, Parser, Flag} object Main{ @main case class Config(@arg(short = 'f', doc = "String to print repeatedly") foo: String, @arg(doc = "How many times to print string") myNum: Int = 2, @arg(doc = "Example flag") bool: Flag) implicit def configParser = Parser[Config] @main def bar(config: Config, @arg(name = "extra-message") extraMessage: String) = { println(config.foo * config.myNum + " " + config.bool.value + " " + extraMessage) } @main def qux(config: Config, n: Int) = { println((config.foo * config.myNum + " " + config.bool.value + "\n") * n) } def main(args: Array[String]): Unit = Parser(this).runOrExit(args) }$ ./mill example.classarg bar --foo cow --extra-message "hello world" cowcow false hello world $ ./mill example.classarg qux --foo cow --n 5 cowcow false cowcow false cowcow false cowcow false cowcow falseThis allows you to re-use common command-line parsing configuration without needing to duplicate it in every @main method in which it is needed. A @main def can make use of multiple @main case classes, and @main case classes can be nested arbitrarily deeply.
@main method parameters can be Option[T] or Seq[T] types, representing optional parameters without defaults or repeatable parameters
package testoptseq import mainargs.{main, arg, Parser} object Main{ @main def runOpt(opt: Option[Int]) = println(opt) @main def runSeq(seq: Seq[Int]) = println(seq) @main def runVec(seq: Vector[Int]) = println(seq) def main(args: Array[String]): Unit = Parser(this).runOrExit(args) }$ ./mill example.optseq runOpt None $ ./mill example.optseq runOpt --opt 123 Some(123) $ ./mill example.optseq runSeq --seq 123 --seq 456 --seq 789 List(123, 456, 789)@main method arguments that have single-character names are automatically converted to short arguments, invoked with a single - instead of double --. The short version of an argument can also be given explicitly via the @arg(short = '...'):
object Base { @main def bools(a: Flag, b: Boolean = false) = println(Seq(a.value, b, c.value)) @main def strs(a: Flag, b: String) = println(Seq(a.value, b)) }These can be invoked as normal, for Flags like -a or normal arguments that take a value like -b below:
$ ./mill example.short bools -a Seq(true, false) $ ./mill example.short bools -b true Seq(false, true)Multiple short arguments can be combined into one -ab call:
$ ./mill example.short bools -ab true Seq(true, true)
Short arguments can be combined with their value: ```scala $ ./mill example.short bools -btrue Seq(false, true) And you can combine both multiple short arguments as well as the resulting value:
$ ./mill example.short bools -abtrue Seq(true, true)Note that when multiple short arguments are combined, whether via -ab true or via -abtrue, only the last short argument (in this case b) can take a value.
If an = is present in the short argument group after the first character, the short argument group is treated as a key-value pair with the remaining characters after the = passed as the value to the first short argument:
$ ./mill example.short strs -b=value Seq(false, value) $ ./mill example.short strs -a -b=value Seq(true, value)You can use -b= as a shorthand to set the value of b to an empty string:
$ ./mill example.short strs -a -b= Seq(true, )If an = is present in the short argument group after subsequent character, all characters except the first are passed to the first short argument. This can be useful for concisely passing key-value pairs to a short argument:
$ ./mill example.short strs -a -bkey=value Seq(true, key=value)These can also be combined into a single token, with the first non-`Flag` short argument in the token consuming the subsequent characters as a string (unless the subsequent characters start with an `=`, which is skipped): ```scala $ ./mill example.short strs -ab=value Seq(true, value) $ ./mill example.short strs -abkey=value Seq(true, key=value)The library's annotations and methods support the following parameters to customize your usage:
-
name: String: lets you specify the top-level name of@mainmethod you are defining. If multiple@mainmethods are provided, this name controls the sub-command name in the CLI. If an explicitnameis not passed, both the (typically)camelCasename of the Scaladefas well as itskebab-caseequivalents will be accepted -
doc: String: a documentation string used to provide additional information about the command. Normally printed below the command name in the help message
-
name: String: lets you specify the long name of a CLI parameter, e.g.--foo. If an explicitnameis not passed, both the (typically)camelCasename of the Scala method parameter as well as itskebab-caseequivalents will be accepted -
short: Char: lets you specify the short name of a CLI parameter, e.g.-f. If not given, the argument can only be provided via its long name -
doc: String: a documentation string used to provide additional information about the command -
noDefaultName: Boolean: iftruethis arg (e.gfooBar) can only be called by its mangled name--foo-barand not by the original name--fooBar. Defaults tofalse -
positional: Boolean: iftruethis arg can be passed "positionally" without the--nameof the parameter being provided, e.g../mill example.hello hello 3 --bool. Defaults tofalse -
hidden: Boolean: iftruethis arg will not be included in the rendered help text.
Apart from taking the name of the main object or config case class, Parser has methods that support a number of useful configuration values:
-
allowPositional: Boolean: allows you to pass CLI arguments "positionally" without the--nameof the parameter being provided, e.g../mill example.hello -f hello --my-num 3 --boolcould be called via./mill example.hello hello 3 --bool. Defaults tofalse -
allowRepeats: Boolean: allows you to pass in a flag multiple times, and using the last provided value rather than raising an error. Defaults tofalse -
totalWidth: Int: how wide to re-format thedocstrings to when printing the help text. Defaults to100 -
printHelpOnExit: Boolean: whether or not to print the full help text when argument parsing fails. This can be convenient, but potentially very verbose if the list of arguments is long. Defaults totrue -
docsOnNewLine: Boolean: whether to print argument doc-strings on a new line below the name of the argument; this may make things easier to read, but at a cost of taking up much more vertical space. Defaults tofalse -
autoprintHelpAndExit: Option[(Int, PrintStream)]: whether to detect--helpbeing passed in automatically, and if so where to print the help message and what exit code to exit the process with. Defaults t,Some((0, System.out)), but can be disabled by passing inNoneif you want to handle help text manually (e.g. by calling.helpTexton the parser object) -
customName/customNamesandcustomDoc/customDocs: allows you to override the main method names and documentation strings at runtime. This allows you to work around limitations in the use of the@main(name = "...", doc = "...")annotation that only allows simple static strings. -
sorted: Boolean: whether to sort the arguments alphabetically in the help text. Defaults totrue -
nameMapper: String => Option[String]: how ScalacamelCasenames are mapping to CLI command and flag names. Defaults to translation tokebab-case, but you can pass inmainargs.Util.snakeCaseNameMapperforsnake_caseCLI names ormainargs.Util.nullNameMapperto disable mapping.
If you want to parse arguments into types that are not provided by the library, you can do so by defining an implicit TokensReader[T] for that type:
package testcustom import mainargs.{main, arg, Parser, TokensReader} object Main{ implicit object PathRead extends TokensReader.Simple[os.Path]{ def shortName = "path" def read(strs: Seq[String]) = Right(os.Path(strs.head, os.pwd)) } @main def run(from: os.Path, to: os.Path) = { println("from: " + from) println("to: " + to) } def main(args: Array[String]): Unit = Parser(this).runOrExit(args) }$ ./mill example.custom --from mainargs --to out from: /Users/lihaoyi/Github/mainargs/mainargs to: /Users/lihaoyi/Github/mainargs/outIn this example, we define an implicit PathRead to teach MainArgs how to parse os.Paths from the OS-Lib library.
Note that read takes all tokens that were passed to a particular parameter. Normally this is a Seq of length 1, but if allowEmpty is true it could be an empty Seq, and if alwaysRepeatable is true then it could be arbitrarily long.
You can see the Scaladoc for TokenReaders.Simple for other things you can override:
You can use the special Leftover[T] type to store any tokens that are not consumed by other parsers:
package testvararg import mainargs.{main, arg, Parser, Leftover} object Main{ @main def run(foo: String, myNum: Int = 2, rest: Leftover[String]) = { println(foo * myNum + " " + rest.value) } def main(args: Array[String]): Unit = Parser(this).runOrExit(args) }$ ./mill example.vararg --foo bar i am cow barbar List(i, am, cow)This also works with ParserForClass:
package testvararg2 import mainargs.{main, arg, ParserForClass, Leftover} object Main{ @main case class Config(foo: String, myNum: Int = 2, rest: Leftover[String]) def main(args: Array[String]): Unit = { val config = ParserForClass[Config].constructOrExit(args) println(config) } }$ ./mill example.vararg2 --foo bar i am cow Config(bar,2,Leftover(List(i, am, cow)))You can also pass in a different type to Leftover, e.g. Leftover[Int] or Leftover[Boolean], if you want to specify that leftover tokens all parse to a particular type. Any tokens that do not conform to that type will result in an argument parsing error.
You can also use * "varargs" to define a parameter that takes in the remainder of the tokens passed to the CLI:
package testvararg import mainargs.{main, arg, Parser, Leftover} object Main{ @main def run(foo: String, myNum: Int, rest: String*) = { println(foo * myNum + " " + rest.value) } def main(args: Array[String]): Unit = Parser(this).runOrExit(args) }Note that this has a limitation that you cannot then assign default values to the other parameters of the function, and hence using Leftover[T] is preferable for those cases.
MainArgs grew out of the user-defined @main method feature supported by Ammonite Scala Scripts:
This implementation was largely copy-pasted into the Mill build tool, to use for its user-defined T.commands. A parallel implementation was used to parse command-line parameters for Ammonite and Mill themselves.
Now all four implementations have been unified in the MainArgs library, which both Ammonite and Mill rely heavily upon. MainArgs also provides some additional features, such as making it easy to define short versions of flags like -c via the short = '...' parameter, or re-naming the command line flags via name = "...".
MainArgs' support for parsing Scala case classes was inspired by Alex Archambault's case-app library:
MainArgs has the following differentiators over case-app:
- Support for directly dispatching to
@mainmethod(s), rather than only parsing intocase classes - A dependency-free implementation, without pulling in the heavyweight Shapeless library.
MainArgs takes a lot of inspiration from the old Scala Scopt library:
Unlike Scopt, MainArgs lets you call @main methods or instantiate case classes directly, without needing to separately define a case class and parser. This makes it usable with much less boilerplate than Scopt: a single method annotated with @main is all you need to turn your program into a command-line friendly tool.
- Add
mainargs.Parseras a shorthand alias formainargs.ParserForClassandmainargs.ParserForMethods#201
- Add support for Scala 2 varargs in scala 3 macro #167
- Fix issue with binary infinite recursion in binary compatibility forwarders #156
- Add missing
nameMapperargument to.runOrExit, makesortparam onrunEitherreturntruefor consistency with the docs #155
- Various improvements to Scala 3 macros to match Scala 2 implementation #148
- Fix detection of
@mainmethods inherited fromtraits in Scala 3.x #142
- Support for Scala-Native 0.5.0
- Minimum version of Scala 3.x raised to 3.3.1
- Fix usage of
ParserForClassforcase classes with more than 22 parameters in Scala 2.x
- Make combine short args that fail to parse go through normal leftover-token code paths #112
- Fix stackoverflow from incorrect binary compatibility shim #107
-
Automatically map
camelCaseScala method and argument names tokebab-caseCLI commands and flag names, with configurability by passing in customnameMappers#101 -
Allow short arguments and their values to be combined into a single token #102
- Remove unnecessary PPrint dependency
- Support GFlags-style
--foo=barsyntax #98
- Fix handling of case class main method parameter default parameters and annotations in Scala 3 #88
-
Remove hard-code support for mainargs.Leftover/Flag/Subparser to support alternate implementations #62. Note that this is a binary-incompatible change, and any custom
mainargs.TokenReaders you may implement will need to be updated to implement themainargs.TokenReader.Simpletrait -
Fix argument parsing of flags in the presence of
allowPositional=true#66
- Support sorting to args in help text and sort by default
- Various dependency updates
- This release is binary compatible with mainargs 0.3.0
- Update all dependencies to latest
- Support for Scala Native on Scala 3
- Backport of Fix usage of
ParserForClassforcase classes with more than 22 parameters with some default values in Scala 2.x (#123) on top of 0.2.3
- Support Scala 3 #18
- Scala-Native 0.4.0 support
-
Add support for
positional=trueflag inmainargs.arg, to specify a specific argument can only be passed positionally regardless of whetherallowPositionalis enabled for the entire parser -
Allow
-and--to be passed as argument values without being treated as flags
- First release