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.command
s.
ivy"com.lihaoyi::mainargs:0.2.3"
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(name = "my-num", 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 = ParserForMethods(this).runOrExit(args)
}
$ ./mill example.hello -f hello
hellohello false
$ ./mill example.hello -f hello --my-num 3
hellohellohello false
$ ./mill example.hello -f hello --my-num 3 --bool
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 flag
Setting 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 ParserForMethods(...)
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 proess
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 def
s
with @main
. Each entrypoint can have their own set of arguments:
package testhello2
import mainargs.{main, arg, ParserForMethods, Flag}
object Main{
@main
def foo(@arg(short = 'f', doc = "String to print repeatedly")
foo: String,
@arg(name = "my-num", 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 = ParserForMethods(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
lolslolslolslolslolslolslolslolslolslols
If you want to construct a configuration object instead of directly calling a
method, you can do so via ParserForClass[T]
and `constructOrExit:
package testclass
import mainargs.{main, arg, ParserForClass, Flag}
object Main{
@main
case class Config(@arg(short = 'f', doc = "String to print repeatedly")
foo: String,
@arg(name = "my-num", doc = "How many times to print string")
myNum: Int = 2,
@arg(doc = "Example flag")
bool: Flag)
def main(args: Array[String]): Unit = {
val config = ParserForClass[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 flag
ParserForClass[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 ParserForClass[T]
defined:
package testclassarg
import mainargs.{main, arg, ParserForMethods, ParserForClass, Flag}
object Main{
@main
case class Config(@arg(short = 'f', doc = "String to print repeatedly")
foo: String,
@arg(name = "my-num", doc = "How many times to print string")
myNum: Int = 2,
@arg(doc = "Example flag")
bool: Flag)
implicit def configParser = ParserForClass[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 = ParserForMethods(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 false
This 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 class
es, and @main case class
es 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, ParserForMethods}
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 = ParserForMethods(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)
The library's annotations and methods support the following parameters to customize your usage:
-
name: String
: lets you specify the top-level name of@main
method you are defining. If multiple@main
methods are provided, this name controls the sub-command name in the CLI -
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
. Defaults to the name of the function parameter if not given -
short: Char
: lets you specify the short name of a CLI parameter, e.g.-f
. If not given, theargument can only be provided via its long name -
doc: String
: a documentation string used to provide additional information about the command
Apart from taking the name of the main object
or config case class
,
ParserForMethods
and ParserForClass
both have methods that support a number
of useful configuration values:
-
allowPositional: Boolean
: allows you to pass CLI arguments "positionally" without the--name
of the parameter being provided, e.g../mill example.hello -f hello --my-num 3 --bool
could 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 thedoc
strings 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--help
being 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 inNone
if you want to handle help text manually (e.g. by calling.helpText
on the parser object) -
customName
/customNames
andcustomDoc
/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.
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, ParserForMethods, TokensReader}
object Main{
implicit object PathRead extends TokensReader[os.Path](
"path",
strs => 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 = ParserForMethods(this).runOrExit(args)
}
$ ./mill example.custom --from mainargs --to out
from: /Users/lihaoyi/Github/mainargs/mainargs
to: /Users/lihaoyi/Github/mainargs/out
In this example, we define an implicit PathRead
to teach MainArgs how to parse
os.Path
s from the OS-Lib library.
ArgReader
requires the following fields:
class ArgReader[T](val shortName: String, // what to print in <...> in the help text
val read: Seq[String] => Either[String, T],
val alwaysRepeatable: Boolean = false, // used to allow Seq[T]-like parsers
val allowEmpty: Boolean = false) // used to allow Option[T]-like parsers
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.
The allowRepeats
parameter can also result in multiple tokens being passed to
your ArgReader
; for ArgReader
s that do not expect that, the convention is to
simply pick the last token in the list. There is no need to raise an error on
duplicates, as you can simply disable allowRepeats
if you want the parser to
raise an error when a parameter is provided more than once.
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, ParserForMethods, 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 = ParserForMethods(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, ParserForMethods, Leftover}
object Main{
@main
def run(foo: String,
myNum: Int,
rest: String*) = {
println(foo * myNum + " " + rest.value)
}
def main(args: Array[String]): Unit = ParserForMethods(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.command
s. 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 class
es was inspired by Alex
Archambault's case-app
library:
MainArgs has the following differentiators over case-app
:
- Support for directly dispatching to
@main
method(s), rather than only parsing intocase class
es - 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 class
es 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.
- Support Scala 3 #18
- Scala-Native 0.4.0 support
-
Add support for
positional=true
flag inmainargs.arg
, to specify a specific argument can only be passed positionally regardless of whetherallowPositional
is enabled for the entire parser -
Allow
-
and--
to be passed as argument values without being treated as flags
- First release