mainargs

repository·main·Indexed 42 days ago

https://github.com/com-lihaoyi/mainargs

A lightweight, dependency-free Scala library for parsing command-line arguments. It allows developers to define CLI entrypoints and subcommands using @main and @arg annotations, powering the interfaces for Ammonite and Mill. Features include automatic kebab-case conversion, support for Flags, optional parameters via Option, repeatable parameters via Seq, and flexible execution strategies like runOrExit, runOrThrow, and runEither.

Tokens
6.4K
Snippets
22
Records
25
Agent score
53%

What's inside mainargs

  1. Handle leftover arguments with Leftover[T]

    main

    Use the Leftover[T] type to capture any tokens that are not consumed by other defined parameters. This is useful for implementing commands that take a variable number of arguments at the end of the command line.

    You can specify the type for Leftover, such as Leftover[String] or Leftover[Int]. If tokens do not conform to the specified type, a parsing error will occur.

    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)
    }
  2. Use varargs for remainder tokens

    main

    You can use the Scala varargs syntax (*) to define a parameter that consumes all remaining tokens.

    Limitation: When using varargs, you cannot assign default values to other parameters in the same method. In such cases, using Leftover[T] is the preferred approach.

    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)
    }
  3. Use Option and Sequence for optional or repeatable parameters

    main

    Main method parameters can use Scala standard collection types to represent specific CLI behaviors:

    • Option[T]: Represents an optional parameter that does not have a default value.
    • Seq[T] or Vector[T]: Represents repeatable parameters (e.g., --seq 1 --seq 2).
    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)
    }
  4. Implement custom argument parsers with TokensReader

    main

    To support types not natively handled by mainargs, define an implicit TokensReader[T]. The most common way is to extend TokensReader.Simple[T].

    Note that the read method receives a Seq[String] containing all tokens passed to that parameter. This sequence is typically length 1, but can be longer if alwaysRepeatable is true or if allowEmpty is enabled.

    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)
    }
  5. Parse command line arguments for a main method

    main

    You can parse command line arguments and dispatch them to a method by using Parser(objectContainingMethod).

    Key features:

    • Use @main to mark entrypoint methods.
    • Use @arg to customize arguments (e.g., setting short names or doc strings).
    • Arguments with default values are automatically treated as optional.
    • CamelCase parameter names are automatically converted to kebab-case for the CLI (e.g., myNum becomes --my-num).
    • Flag types can be passed without a value to become true.
    package testhello
    import mainargs.{main, arg, Parser, 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)
    }
  6. Reuse argument sets across multiple methods

    main

    To avoid duplicating argument definitions, define a @main case class and provide an implicit def for its Parser. Other @main methods can then take that case class as a parameter.

    package testclassarg
    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)
      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)
    }
  7. Use short arguments and combined flags

    main

    Arguments with single-character names are automatically treated as short arguments (using - instead of --).

    Key behaviors:

    • Combining flags: Multiple short flags can be combined, e.g., -ab is equivalent to -a -b.
    • Combining with values: A combined short argument group can include a value for the last argument, e.g., -ab true or -abtrue.
    • Key-value pairs with =: You can use -b=value to set a value. If using a combined group, -a -b=value works.
    • Concise key-value pairs: -bkey=value passes key=value as the value to the first short argument b.
    • Single token consumption: The first non-Flag short argument in a token can consume subsequent characters as a string.
    # Examples of short argument usage
    $ ./mill example.short bools -a
    Seq(true, false)
    
    $ ./mill example.short bools -b true
    Seq(false, true)
    
    $ ./mill example.short bools -ab true
    Seq(true, true)
    
    $ ./mill example.short bools -btrue
    Seq(false, true)
    
    $ ./mill example.short strs -b=value
    Seq(false, value)
    
    $ ./mill example.short strs -a -b=value
    Seq(true, value)
    
    $ ./mill example.short strs -a -b=
    Seq(true, )
    
    $ ./mill example.short strs -a -bkey=value
    Seq(true, key=value)
  8. Configure Parser behavior

    main

    The Parser object provides several configuration options to customize the CLI experience beyond annotations.

    Parser Configuration Options

    • allowPositional: Boolean: Allows passing arguments without their --name prefix. Defaults to false.
    • allowRepeats: Boolean: Allows a flag to be passed multiple times, using the last value provided instead of erroring. Defaults to false.
    • totalWidth: Int: Width for re-formatting doc strings in help text. Defaults to 100.
    • printHelpOnExit: Boolean: Whether to print full help text on parsing failure. Defaults to true.
    • docsOnNewLine: Boolean: Whether to print argument doc-strings on a new line. Defaults to false.
    • autoprintHelpAndExit: Option[(Int, PrintStream)]: Automatically detects --help. Defaults to Some((0, System.out)). Pass None to handle help manually.
    • sorted: Boolean: Whether to sort arguments alphabetically in help text. Defaults to true.
    • nameMapper: String => Option[String]: Defines how camelCase names map to CLI names. Defaults to kebab-case. Use mainargs.Util.snakeCaseNameMapper for snake_case or mainargs.Util.nullNameMapper to disable mapping.
    • customName/customNames and customDoc/customDocs: Overrides method names and documentation at runtime (useful for dynamic strings).
  9. Implement multiple entrypoints (subcommands)

    main

    By annotating multiple methods with @main, you can create a program with subcommands. When running the program, you must specify which subcommand to execute.

    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)
    }
  10. Handle argument parsing errors with different run strategies

    main

    After creating a Parser, you can choose how to handle parsing results and errors using these methods:

    • runOrExit(args): Runs the method if successful; otherwise, prints help to stderr and calls System.exit(1).
    • runOrThrow(args): Runs the method if successful; otherwise, throws an exception containing the help text.
    • runEither(args): Returns Right(v: Any) on success or Left(s: String) (the error message) on failure.
    • runRaw(args): Returns mainargs.Result.Success(v: Any) on success or mainargs.Result.Error on failure, providing maximum flexibility for custom error handling.
  11. Configure @main and @arg annotations

    main

    Use annotations to customize how Scala methods and parameters are exposed as CLI commands and flags.

    @main

    Used on a method to define a top-level command.

    • name: String: Specifies the command name. If multiple @main methods exist, this name determines the sub-command. If omitted, camelCase and kebab-case versions of the method name are accepted.
    • doc: String: Documentation string printed below the command name in help messages.

    @arg

    Used on method parameters to define CLI flags or positional arguments.

    • name: String: The long name of the parameter (e.g., --foo). If omitted, camelCase and kebab-case versions of the parameter name are accepted.
    • short: Char: The short name (e.g., -f).
    • doc: String: Documentation string for the argument.
    • noDefaultName: Boolean: If true, the argument can only be called via its mangled name (e.g., --foo-bar) and not the original camelCase name. Defaults to false.
    • positional: Boolean: If true, the argument can be passed without the --name prefix. Defaults to false.
    • hidden: Boolean: If true, the argument is excluded from help text. Defaults to false.