Command line parsing for Scala applications.

Example

In Scala:

import argparse.default as ap

/** This is an example app. It shows how a command line interface can be
  * generated from various kinds of method parameters.
  *
  * @param server a sample named parameter
  * @param secure this is a flag
  * @param path a positional parameter
  */
@ap.command()
def main(
  server: String = "localhost",
  secure: Boolean = false,
  path: os.SubPath
): String =
  val scheme = if secure then "https" else "http"
  s"$scheme://$server/$path"

// boilerplate necessary until macro annotations become available in Scala 3
def main(args: Array[String]): Unit = argparse.main(this, args)

In a terminal:

$ app --server example.org --secure a/b/c
https://example.org/a/b/c

$ app
missing argument: path
run with '--help' for more information

$ app --help
Usage: [OPTIONS] PATH

This is an example app. It shows how a command line interface can be generated from various kinds of method parameters.

Options:
      --bash-completion string  generate bash completion for this command
      --help                    show this message and exit
      --secure                  this is a flag
      --server string           a sample named parameter

Highlights

  • Simple interfaces

    • High-level, annotation-based, CLI generator.

    • Lower-level interface inspired by the argparse package from Python.

  • Bash completion

    • Standalone bash completion for a super snappy user experience, even on the JVM.

    • Interactive bash completion for the most custom needs.

  • Works with Scala 3.2, on the JVM and Native (the lower-level interface also works with Scala 2.13)

  • Support for subcommands (aka "verbs")

Binaries

This library is published for Scala 3.2 and 2.13, for the JVM and Native. It is available on maven central under the coordinates:

  • mill: ivy"io.crashbox::argparse::0.20.0"

  • sbt: "io.crashbox" %%% "argparse" % "0.20.0"

Getting Help

ChannelLinks
ForumGitHub Discussions
Chatdiscord project chat
IssuesGitHub

Usage

Annotate the desired main function with command(). Scala-argparse will then generate a standard main method and command line parser with bells-and-whistles such as help messages and bash completion.1

1

Note that until Scala 3 supports macro annotations (probably in version 3.3.0), you will need to write a tiny boilerplate snippet as shown in the introductory example.

The generated code uses a lower-level interface, which you can also use directly if you would like more flexibility than what the auto-generated CLI provides.

Parameter Mapping

Scala method parameters will be mapped to command line parameters in the following way:

  • Parameters with defaults will become --named-parameters= on the command line. Furthermore, boolean parameters become --flags, meaning that they don't need to take a 'true' argument on the command line.

  • Parameters without defaults become positional parameters.

  • Parameters of type Seq[?] become repeatable parameters on the command line.

E.g.

import argparse.default as ap

@ap.command()
def main(
  namedParameter: String = "a",
  flag: Boolean = false,
  repeatable: Seq[String] = Seq(),
  positional1: Int,
  positional2: Int,
  remaining: Seq[String]
) =
  println(s"namedParameter=$namedParameter")
  println(s"flag=$flag")
  println(s"repeatable=$repeatable")
  println(s"positional1=$positional1")
  println(s"positional2=$positional2")
  println(s"remaining=$remaining")

// boilerplate until Scala 3 supports macro annotations
def main(args: Array[String]) = argparse.main(this, args)
$ app --named-parameter b --repeatable a 1 2 3 --flag 4 --repeatable b 5
namedParameter=b
flag=true
repeatable=List(a, b)
positional1=1
positional2=2
remaining=List(3, 4, 5)

$ app --help
Usage: [OPTIONS] POSITIONAL1 POSITIONAL2 REMAINING...
Options:
      --bash-completion string  generate bash completion for this command
      --flag                    
      --help                    show this message and exit
      --named-parameter string  
      --repeatable string       

Parameter Types

Support for reading arguments from the command line as Scala types is provided for many types out-of-the-box. Some examples:

  • numeric types
  • java.io, java.nio and os.Path file types
  • various java.time date types
  • key=value pairs of other supported types

E.g.

import argparse.default as ap

@ap.command()
def main(
  num: Int = 0,
  num2: Double = 0,
  path: os.Path = os.pwd, // relative paths on the command line will be resolved to absolute paths w.r.t. to pwd
  keyValue: (String, Int) = ("a" -> 2),
  keyValues: Seq[(String, Int)] = Seq()
) =
  println(s"num=$num")
  println(s"num2=$num2")
  println(s"path=$path")
  println(s"keyValue=$keyValue")
  println(s"keyValues=$keyValues")

// boilerplate until Scala 3 supports macro annotations
def main(args: Array[String]) = argparse.main(this, args)
$ app --num 42 --num2 1.1 --path /a/b/c --key-value hello=2 --key-values a=1 --key-values b=2
num=42
num2=1.1
path=/a/b/c
keyValue=(hello,2)
keyValues=List((a,1), (b,2))

$ app --num 1.1
error processing argument --num: '1.1' is not an integral number
run with '--help' for more information

$ app --help
Usage: [OPTIONS]
Options:
      --bash-completion string  generate bash completion for this command
      --help                    show this message and exit
      --key-value string=int    
      --key-values string=int   
      --num int                 
      --num2 float              
      --path path               

The mechanism by which command line arguments are converted to Scala types is highly customizable and new types can easily be added.

Parameter Overrides

The generated command line parameters can further be customized by annotating Scala parameters with certain annotations:

  • @alias(): set other names by which the parameter will be available. This is particularly useful for defining single-letter short names for frequently used parameters.

  • @env(): set the name of an environment variable which will be used to lookup the parameter if it is not found on the command line.

  • @name(): override the name derived from the parameter name. This can be used as an escape hatch for changing positional to named arguments and vice versa.

E.g.

import argparse.default as ap

@ap.command()
def main(
  @ap.alias("-s", "--address") server: String = "a",
  @ap.env("APPLICATION_CREDENTIALS") creds: os.Path = os.pwd / "creds",
  @ap.name("--named") positional: Int
) =
  println(s"server=$server")
  println(s"creds=$creds")
  println(s"positional=$positional")

// boilerplate until Scala 3 supports macro annotations
def main(args: Array[String]) = argparse.main(this, args)
$ APPLICATION_CREDENTIALS=/secret app -s localhost --named 42
server=localhost
creds=/secret
positional=42

Output Mapping

The returned values of annotated functions are automatically converted to strings and printed to standard out. There are builtin conversions for some common return values:

  • iterables of products (aka case classes) are printed in a tabular format
  • other iterables are printed one per line
  • byte arrays and other sources of binary data are streamed
  • futures are awaited

In other cases, the toString method of the returned value is simply called.

E.g.

import argparse.default as ap

case class Item(
  name: String,
  value: Double,
  comment: String
)

@ap.command()
def main() =
  List(
    Item("item1", 2, ""),
    Item("item2", 0.213, "notice the numeric alignment"),
    Item("item3", -100.2, ""),
    Item("item4", 10.2, "a comment"),
    Item("another_item", 12.54, "a comment"),
    Item("", 12.54, "item has no name"),
    Item("etc", 0, "...")
  )

// boilerplate necessary until macro annotations become available in Scala 3
def main(args: Array[String]): Unit = argparse.main(this, args)
$ app
NAME         VALUE    COMMENT                     
item1           2.0                               
item2           0.213 notice the numeric alignment
item3        -100.2                               
item4          10.2   a comment                   
another_item   12.54  a comment                   
               12.54  item has no name            
etc             0.0   ...                         

You can also define your own conversions by defining instances of the argparse.core.OutputApi#Printer typeclass.

Error Handling

In case a command throws, only the exception's message is printed. The stack trace is not shown unless a DEBUG environment variable is defined.

You can change this behavior by overriding the handleError function of the OutputApi trait.

Nested Commands

As an application grows, it is common to organise different "actions" or "flavours" of the application under nested commands, each taking their own list of parameters. See the git or docker tools for some such examples.

In scala-argparse, nested commands use the same mechanism as single, top-level commands, with one small twist: instead of annotating a method with command(), you annotate a class definition (or a method that returns an instance of a class containing other commands). This can be done recursively, and classes can declare parameters which can be referenced by child commands.

E.g.

import argparse.default as ap

@ap.command()
class app():

  @ap.command()
  def version() = println(s"v1000")

  @ap.command()
  class op(factor: Double = 1.0):

    @ap.command()
    def showFactor() = println(s"the factor is $factor")

    @ap.command()
    def multiply(operand: Double) = println(s"result is: ${factor * operand}")

// this is boilerplate for now; it will become obsolete once macro-annotations
// are released
def main(args: Array[String]): Unit = argparse.main(this, args)
$ app
missing argument: command
run with '--help' for more information

$ app version
v1000

$ app op show-factor
the factor is 1.0

$ app op multiply 2
result is: 2.0

$ app op --factor 3 multiply 2
result is: 6.0

Bells and Whistles

Any program that uses scala-argparse automatically gets:

  • A concise help dialogue (that is formatted according to your terminal's current dimensions) derived from the main function's scaladoc comment.

    You can view the help dialogue by passing the --help flag.

  • A bash-completion script, which will allow users to get tab-completion in their terminal.

    The bash completion script can be generated by passing a --bash-completion=<program name> argument.

  • Bash-awareness for interactive bash completion.

Next Steps

  • Now that you know the high-level API, check out the lower-level API, which underpins the former and can be helpful for understanding customizations.

  • Read the API docs. Start with the argparse.default bundle.

This is the documentation for the lower-level interface. It is used by the annotation-based higher-level interface, but offers more flexibility.

Tutorial

Acknowledgement: this tutorial is inspired by and largely copied from the Python Argparse Tutorial by Tshepang Lekhonkhobe, made available under the Zero Clause BSD License.

This tutorial is intended to be an introduction to the lower-level interface of argparse. This interface deals with building command line parsers, and is generated by the higher-level annotation-based interface. As such, it is helpful to understand these concepts, even if you only use the higher-level interface.

Concepts

Let's show the sort of functionality that we are going to explore in this introductory tutorial by making use of the ls command:

$ ls
argparse  build.sc  ci  examples  ini  LICENSE.md  mill  out  README.md
$ ls argparse
src  src-2  src-3  test
$ ls -l
total 48
drwxr-xr-x 9 jodersky jodersky  4096 Jul  3 10:12 argparse
-rw-r--r-- 1 jodersky jodersky  4836 Jul  3 15:30 build.sc
drwxr-xr-x 2 jodersky jodersky  4096 Jan 31 18:05 ci
drwxr-xr-x 5 jodersky jodersky  4096 Jul  3 15:07 examples
drwxr-xr-x 4 jodersky jodersky  4096 May 20 19:58 ini
-rw-r--r-- 1 jodersky jodersky  1473 Apr 30  2020 LICENSE.md
-rwxr-xr-x 1 jodersky jodersky  1646 Mar  7  2021 mill
drwxr-xr-x 5 jodersky jodersky  4096 Jul  3 15:54 out
-rw-r--r-- 1 jodersky jodersky 10090 Jul  3 17:10 README.md
$ ls --help
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.
...

A few concepts we can learn from the four commands:

  • The ls command is useful when run without any arguments at all. It defaults to displaying the contents of the current directory.

  • If we want beyond what it provides by default, we tell it a bit more. In this case, we want it to display a different directory, argparse. What we did is specify what is known as a positional argument. It's called so because the program should know what to do with the value, solely based on where it appears on the command line. This concept is more relevant to a command like cp, whose most basic usage is cp SRC DEST. The first position is what you want copied, and the second position is where you want it copied to.

  • Now, say we want to change behaviour of the program. In our example, we display more info for each file instead of just showing the file names. The -l in that case is known as a named argument.

  • That's a snippet of the help text. It's very useful in that you can come across a program you have never used before, and can figure out how it works simply by reading its help text.

These concepts are core to argparse:

parameter : a named variable, a placeholder, in a command line definition

argument : the value assigned to a parameter

named argument : an argument that starts with -. The characters following determine the name of the parameter that the argument is assigned to. The actual value assigned to the parameter is given after an '=' or a space. For instance --foo=bar assigns bar to foo. Named arguments may appear in any order on a command line.

positional argument : an argument that is not named. Positional arguments are assigned to positional parameters according to their respective order of occurence.

Basics

Let's start with an example that does almost nothing:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  parser.parseOrExit(args)

The following is a result of running the code:

$ app


$ app --help
Usage: [OPTIONS]
Options:
      --bash-completion string  generate bash completion for this command
      --help                    show this message and exit

$ app --verbose
unknown argument: --verbose
run with '--help' for more information

$ app foo
unknown argument: foo
run with '--help' for more information

Here is what is happening:

  • Running the program without any arguments results in nothing displayed to stdout. Not so useful.

  • The second one starts to display the usefulness of the argparse library. We have done almost nothing, but already we get a help message.

  • The --help option is the only option we get for free (i.e. no need to specify it). Specifying anything else results in an error. But even then, we do get a useful usage message, also for free.

Required Parameters

An example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val echo = parser.requiredParam[String]("echo")
  parser.parseOrExit(args)
  println(echo.value)

And running the code:

$ app
missing argument: echo
run with '--help' for more information

$ app --help
Usage: [OPTIONS] ECHO
Options:
      --bash-completion string  generate bash completion for this command
      --help                    show this message and exit

$ app foo
foo

Here is what's happening:

  • We've added the requiredParam() method, which is what we use to specify which command-line arguments the program needs. In this case, I've named it echo so that it's in line with its function.

    The result of this method is a holder to some future value of an argument, which can be accessed by calling the .value method.

  • Calling our program now requires us to specify an argument.

  • We can specify how an argument should be read from the command line by specifying the type of the parameter, String in this case.

  • The parser.parseOrExit() method is what actually goes through the command line arguments and sets the argument holders' values.

    After calling this method, the arguments can be accessed via the .value method of the argument holders.

Note however that, although the help display looks nice and all, it currently is not as helpful as it can be. For example we see that we got echo as a positional argument, but we don't know what it does, other than by guessing or by reading the source code. So, let's make it a bit more useful:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val echo: argparse.Argument[String] = parser.requiredParam[String](
    "echo",
    help = "echo the string you use here"
  )
  parser.parseOrExit(args)
  print(echo.value)

And we get:

$ app --help
Usage: [OPTIONS] ECHO
Options:
      --bash-completion string  generate bash completion for this command
      --help                    show this message and exit

Now, how about doing something even more useful:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val square = parser.requiredParam[Int](
    "square",
    help = "display a square of a given number"
  )
  parser.parseOrExit(args)
  print(square.value * square.value)

Following is a result of running the code:

$ app 4
16

$ app four
error processing argument square: 'four' is not an integral number
run with '--help' for more information

That went well. The program now even helpfully quits on bad input.

Optional Parameters

So far the parameters that we have specified were required. Let's look at how we can make an argument optional.

An example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val answer = parser.param[Int](
    "answer",
    default = 42,
    help = "display the answer to the universe"
  )
  parser.parseOrExit(args)
  println(answer.value)

Following is a result of running the code:

$ app
42

$ app 1000
1000

Here is what's happening:

  • The parameter is made optional by declaring it with the param() method instead of the requiredParam() method.

  • This method requires a default value.

  • The default value will be used if the argument is not encountered on the command-line.

Repeated Parameters

An example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val files = parser.repeatedParam[os.FilePath](
    "files",
    help = "remove these files"
  )
  parser.parseOrExit(args)
  for (file <- files.value) {
    println(s"if this were a real program, we would delete $file")
  }

Following is a result of running the code:

$ app


$ app file1
if this were a real program, we would delete file1

$ app file1 file2 file3
if this were a real program, we would delete file1
if this were a real program, we would delete file2
if this were a real program, we would delete file3

Here is what's happening:

  • The parameter is made repeated by declaring it with the repeatedParam().

  • Repeated parameters accumulate all ocurences in the argument holder.

    Thus, here, value will give us back a Seq[os.FilePath] rather than a single os.FilePath.

Named Parameters and Positional Parameters

So far you may have noticed that all our examples have used positional parameters. Recall from the initial ls example, that a positional parameter is one which is set solely based on the position of its argument. When parameter lists get very long or change over time, it can become very difficult to keep matching argument lists coherent. Therefore, most command line tools will mostly use named parameters.

An example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val verbosity = parser.param[Int](
    "--verbosity",
    default = 0,
    help = "level of verbosity"
  )
  val files = parser.repeatedParam[java.nio.file.Path](
    "files",
    help = "remove these files"
  )
  parser.parseOrExit(args)
  println(s"verbosity: ${verbosity.value}")
  for (file <- files.value) {
    println(s"if this were a real program, we would delete $file")
  }

Following is a result of running the code:

$ app --verbosity
error processing argument --verbosity: argument expected
run with '--help' for more information

$ app --verbosity 1
verbosity: 1

$ app --verbosity=1
verbosity: 1

$ app --verbosity 1 file1 file2
verbosity: 1
if this were a real program, we would delete file1
if this were a real program, we would delete file2

$ app file1 --verbosity 1 file2
verbosity: 1
if this were a real program, we would delete file1
if this were a real program, we would delete file2

$ app file1 -- --verbosity 1 file2
verbosity: 0
if this were a real program, we would delete file1
if this were a real program, we would delete --verbosity
if this were a real program, we would delete 1
if this were a real program, we would delete file2

Here is what is happening:

  • A named parameter is a parameter that is identified by a name on the command line instead of a position. Named arguments are syntactically distinguished from positional arguments by a leading - (or -- as is common).

  • Arguments to named parameters can be either separated by a space or an equals sign.

  • Named arguments may appear in any order on the command line. They can be intermingled with positional arguments.

  • A standalone -- serves as a delimiter, and allows arguments that start with - to be treated as positionals. This is very handy for accepting untrusted input in scripts, or deleting files that start with a hyphen.

All parameter declaration methods, param(), requiredParam() and repeatedParam() allow defining parameters as positional and named. The only hint you can see is in the name: if it starts with '-', then it is a named parameter, otherwise it is positional. Note however, that it is most common and good practice to only use positional parameters for required parameters, or conversely, always make named parameters optional.

Flags

Flags are a special kind of named parameter. They don't take an argument, and we are only ever interested if they are present on the command-line or not.

An example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val verbose = parser.param[Boolean](
    "--verbose",
    default = false,
    help = "use verbose output",
    flag = true
  )
  parser.parseOrExit(args)
  println(verbose.value)

Running it:

$ app
false

$ app --verbose
true

$ app --verbose=false
false

How it works:

  • A parameter is declared as a flag by setting the flag parameter,

    This instructs the argument parser that the parameter does not take an argument.

  • If the flag is encountered on the command line, then it is assigned the string value "true".

    Thus, it only makes sense to declare Boolean parameters as flags.

  • You can still override the argument by explicitly passing it after an equals sign.

Short Named Parameters and Aliases

If you are familiar with command line usage, you will notice that I haven't yet touched on the topic of short versions of named parameters. It's quite simple:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val verbose = parser.param[Boolean](
    "--verbose",
    default = false,
    aliases = Seq("-v", "--talkative"),
    flag = true,
    help = "use verbose output"
  )
  parser.parseOrExit(args)
  println(verbose.value)

And here goes:

$ app -v
true

$ app --help
Usage: [OPTIONS]
Options:
      --bash-completion string  generate bash completion for this command
      --help                    show this message and exit
  -v, --verbose                 use verbose output

Note that the new aliases are also reflected in the help text.

Reading from the Environment

In some cases it can be useful to fall back to reading a command line from an environment variable.

Example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val creds = parser.param[os.Path](
    "--credentials-file",
    default = os.root / "etc" / "creds",
    env = "APP_CREDENTIALS_FILE",
    help = "the file containing service credentials"
  )
  parser.parseOrExit(args)
  println(creds.value)

Run it:

$ APP_CREDENTIALS_FILE=/etc/foo app
/etc/foo

$ APP_CREDENTIALS_FILE=/etc/foo2 app --credentials-file=/etc/foo1
/etc/foo1

$ app
/etc/creds

How it works:

  • Required and optional parameter declarations can specify an env, which will name an environment variable to use if the argument cannot be found on the command line.

  • The order of precedence is:

    1. the argument on the command line
    2. the environment variable
    3. the default value

Cookbook

This cookbook contains recipes (i.e. "how-tos") for accomplishing common tasks which are more advanced than what is described in the Tutorial.

Subcommands

Many applications actually split their functionality into multiple nested commands, each corresponding to the verb of an action (such as docker run or git clone). This approach works particularly well if an application performs different functions which require different kinds of arguments.

ArgumentParser has built-in support for these kinds of sub-commands with the subparser() method. This method will return a new ArgumentParser which can be modified as usual. The parent parser is aware of the child parser, and will include it in help messages and bash completion scripts. Each child parser will have its own parameters, but can access the arguments declared in the parent.

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser(description = "an example application")

  val global = parser.param[String]("--global", "unset")

  val getter = parser.subparser("get", "get a value")
  val getterKey = getter.requiredParam[String]("key")
  getter.action{
    println(s"global: ${global.value}")
    println(s"getting key ${getterKey.value}")
  }

  val setter = parser.subparser("set", "set a value")
  val setterKey = setter.requiredParam[String]("key")
  val setterValue = setter.requiredParam[String]("value")
  setter.action{
    println(s"global: ${global.value}")
    println(s"setting key ${setterKey.value} to ${setterValue.value}")
  }

  val nested = parser.subparser("nested", "another command")
  nested.subparser("inner1")
  nested.subparser("inner2")

  parser.parseOrExit(args)
$ app
missing argument: command
run with '--help' for more information

$ app get k
global: unset
getting key k

$ app set k v
global: unset
setting key k to v

$ app --global=a set k v
global: a
setting key k to v

$ app set --global=a k v
unknown argument: --global
run with '--help' for more information

$ app --help
Usage: [OPTIONS] COMMAND ARGS...

an example application

Options:
      --bash-completion string  generate bash completion for this command
      --global string           
      --help                    show this message and exit
Commands:
  set       set a value
  get       get a value
  nested    another command

Bash Completion

If you are an avid user of the command line, you will probably have noticed that you can get argument suggestions by pressing the tab key on a partially typed word. This is a very helpful feature for quickly navigating and exploring command line tools. It is known as bash completion, and can work in one of two ways:

  1. Standalone. A completion script has been sourced by your shell and is what is called to generate completions when you press tab.

  2. Interactive. Your program is called to complete the partially typed word.

The argparse library allows you to use both options with minimal setup. We do however recommend to use standalone completion if you are writing your program for the JVM, since you otherwise have to suffer the JVM's startup delay when you're waiting for tab completion.

Standalone Bash Completion

Every ArgumentParser accepts a --bash-completion parameter which will generate a bash-completion script. You can source this script at the start of your shell session, for example by adding it to your ~/.bashrc.

Example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  parser.param[os.Path]("--foo", os.pwd)
  parser.param[os.Path]("--bar", os.pwd)
  parser.param[os.Path](
    "--baz",
    os.pwd,
    standaloneCompleter = argparse.BashCompleter.Fixed(Set("a", "b"))
  )
  parser.parseOrExit(args)
$ app --bash-completion app > complete.sh
$ source complete.sh
$ app -[press tab]
--bar=  --baz=  --foo=  --help
$ app --baz=[press tab]
a  b

How it works:

  • The argument --bash-completion will generate a completion script for bash.

    You can read the full details of how bash completion works on man 1 bash.

  • Completions are based on the parameter type, but can be overriden by explicitly setting the standaloneCompleter parameter.

  • Completion will only complete positional arguments by default, unless you have started typing a word which starts with -.

  • This works also with subcommands.

We suggest that you include the bash completion script in the files distributed alongside the binary distribution of your application.

Interactive Bash Completion

You can also write completion logic in the program itself. In this case, you will need to instruct bash to call your program with a special environment when you press tab. You can do this by running complete -o nospace -C <program> <program> at the start of your shell session, for exmaple by putting it into ~/.bashrc.

Example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  parser.param[os.Path]("--foo", os.pwd)
  parser.param[os.Path]("--bar", os.pwd)
  parser.param[os.Path](
    "--baz",
    os.pwd,
    interactiveCompleter = s => Seq("a", "b")
  )
  parser.parseOrExit(args)
$ complete -o nospace -C app app
$ app -[press tab]
--bar    --baz    --foo    --help
$ app --baz [press tab]
a  b

How it works:

  • Bash sets a few "magic" environment variables before invoking your program, which will cause it to behave differently than when invoked normally.

  • Completions are based on the parameter type, but can be overriden by explicitly setting the interactiveCompleter parameter.

  • You can read the full details of how bash completion works on man 1 bash.

We suggest that you only use interactive completion for programs targeting Scala Native.

Depending on another Parameter

In some situations you may want a parameter's default value to depend on the value of another parameter. You can achieve this by simply calling the argument holder's value method in the default.

Example:

def main(args: Array[String]): Unit =
  val parser = argparse.default.ArgumentParser()
  val dir = parser.param[os.Path]("-C", default = os.root)
  val file = parser.param[os.Path]("--file", default = dir.value / "file")
  parser.parseOrExit(args)
  println(file.value)
$ app
/file

$ app -C /foo
/foo/file

$ app --file /bar
/bar

How it works:

  • The default method parameter is call-by-name.
  • Arguments are parsed in order of parameter definition. Hence a parameter can reference the values of others in its default value.

Adding Support for a New Type of Parameter

This library has support for reading arguments for many kinds of Scala types. In advanced programs however, it can happen that you run into an unsupported type. You will receive a compile-time error, informing you that a specific type is not supported. In this situation, you can define a custom API bundle with an additional Reader for your type of parameter.

Example of the problem:

case class Level(n: Int)

def main(args: Array[Strig]): Unit =
  val parser = argparse.default.ArgumentParser()
  val level = parser.requiredParam[Level]("log-level") // Compile error: no Reader[Level] found
  parser.parseOrExit(args)
  println(bytes.value.n)

Solution:

case class Level(n: Int)

object custom extends argparse.core.Api {

  given Reader[Level] with {
    def read(str: String): Reader.Result[Level] = str match {
      case "DEBUG" => Reader.Success(Level(0))
      case "INFO" => Reader.Success(Level(1))
      case "WARN" => Reader.Success(Level(2))
      case "ERROR" => Reader.Success(Level(3))
      case other => Reader.Error(s"'$other' is not a valid level")
    }
    def typeName = "level"
  }

}

def main(args: Array[String]): Unit =
  val parser = custom.ArgumentParser() // notice how we use `custom` instead of `argparse.default`
  val level = parser.requiredParam[Level]("log-level")
  parser.parseOrExit(args)
  println(level.value.n)
$ app WARN
2

$ app FATAL
error processing argument log-level: 'FATAL' is not a valid level
run with '--help' for more information

How it works:

  • Reader is a typeclass which is responsible for parsing strings from the command line into instances of Scala types.

  • Readers are declared in an API bundle. An API bundle is a bunch of traits that are mixed together in order to define "a flavor" of argparse.

    The default bundle implemented in this library is argparse.default, which includes Readers for most common types.

  • You can create a custom bundle by creating an object which extends argparse.core.Api, and declare additional readers in it.

  • The ArgumentParser from the custom bundle will find the Reader instance which can parse the desired parameter type.

Real World Examples

  • Feel free to open a pull request and list your application here! (you can use the "edit" link on the top-right of this page)

Utilities

Argparse has a few utility classes that can help with some common application-related tasks.

Terminal Properties

The argparse.term helper contains methods to retrieve terminal properties, such as number of rows and columns.

Standard File Paths

You can use argparse.userdirs to access standard directories for configuration, state or data of user applications. This utility is based on the XDG Base Directory Specification, with some adaptations made for macOS. It is recomended to use this helper instead of hardcoding directories or creating a new folder in the user's home directory.

Writing Man Pages

The built-in help message system is useful for quick reference, but is too terse for thoroughly documenting command line applications. For this, I recommend that you write a man page and ship it alongside every application that you create.

I recommend that you watch the presentation "Man, ‘splained: 40 Plus Years of Man Page History", by Breanne Boland. It goes into the reasons and best-practices of writing man pages. You can also read the manual page's manual page (run man man) if you like.

Template

Instead of writing a manual page by hand in troff, you can use the following markdown template and run it through pandoc.

---
title: MY-APP
section: 1
header: User Manual
footer: App 1.0.0
date: June 2022
---

# NAME

my-app \- do something

# SYNOPSIS

**`my-app [--option1 <name>] [--option2 <name>] <arg>`**

# DESCRIPTION

An application which does something useful with `<arg>`.

The description can go into details of what the application does, and can span
multiple paragraphs.

## A subsection

You can use subsections in the description to go into finer details.

# OPTIONS

**`--option1=<string>`**
: An arbitrary string which sets some specific configuration value. Defaults to
some sane value.

**`--option2=<string>`**
: Another arbitrary string which sets some specific configuration value.
Defaults to some sane value.

# EXIT STATUS

Return 0 on success, 1 on error.

# ENVIRONMENT

`MY_APP_VARIABLE`
: Environment variable used by this application.

# FILES

`/etc/my-app.conf`
: This is an important file. Configuration values are read from this file if
they are not specified on the command-line

# EXAMPLES

**An example tells a thousand words.**

```
you can use markdown code sections
```

**Please include at least one example!**

# SEE ALSO

[A reference](https://pandoc.org/)

To preview the page after editing, run:

pandoc -s -f markdown-smart manpage.md -t man | man -l -

(the -smart is necessary here, to avoid converting '--' into an em-dash)

And, once ready, save it as a man page:

pandoc -s -f markdown-smart manpage.md -t man > manpage.1

then finally ship it alongside your application.

Since it's written in markdown, you can also convert to html and make it available online.


Changelog

0.20.0

This release focuses on changes to the annotation-based API. The minimum Scala 3 version has been bumped to 3.2

  • Refactor the annotation-based macro parser to work with Scala 3.2 and avoid some strange compiler crashes.
  • Include the exception class's name in the default error message,

0.19.1

  • Add support for defining top-level main functions with nested commands.

0.19.0

  • Add error handling and output printing to the annotation API.

0.18.1

  • Fix flag-derivation in the annotation API.

0.18.0

  • Migrate documentation from custom static site to mdbook.
  • Improve documentation
  • Replace general arg() annotation with more specialized versions

0.17.0

Breaking

  • Rework annotation-based macro API and remove experimental status. This is now the recommended interface for basic use cases.

  • Replace sub-commands with more structured sub-parsers.

    While the idea of independent subcommands seems quite elegant, there are unfortunately a couple of pitfalls which make them quite brittle to use. The first is that the current API made it easy to capture arguments at the wrong time in lambdas, hence leading to incomplete argument errors. The second, more fundamental issue, is that independent commands are by definition independent, and hence generating shared help messages and bash completion is quite tricky. For example, we needed to resort to hacky workarounds that relied on the stack and thread locals in combination with "magic" parameters to achieve composable bash completion that worked with nested commands.

    Subparsers are less powerful but also less brittle and should still be suitable for the majority of usecases. In situations where absolute control is necessary, the user can still define an all-absorbing parameter and handle subcommands manually.

Minor

  • Remove parameter style checker.

Experimental

  • Add experimental configuration parsing library.

0.16.2

  • Add low-level escape hatches for manually adding parameter descriptors to the argument parser and bash completion scripts.
  • Add a hook provide hook for handling unknown subcommands.

0.16.1

Add support for annotation-based, macro-generated argument parsers (experimental).

0.16.0

The major change in this release is the migration to a mixin-based API. The ArgumentParser trait as well as all readers have been moved to traits in argparse.core._, and the new top-level object for users is argparse.default. Thus, any references to argparse.ArgumentParser need to updated to argparse.default.ArgumentParser.

Other breaking changes:

  • Disable most experimental macro-based parser. This includes settings and mutableSetting. A new macro-based parser is in the works.
  • Remove show() function from Reader.
  • Add a typeName function to Reader.
  • Rename BashCompletion to InteractiveBashCompletion.
  • Make argument a top-level class, instead of () => A.
  • Remove deprecated features.

Other changes:

  • Implement terminal properties for Native (the code between Native and JVM is now shared).
  • Upgrade to Scala Native 0.4.4

0.15.2

  • Add an INI printer
  • Derive publish version from Git

0.15.1

  • Cleanup INI parser support and add more tests.

0.15.0

  • Add support for Scala Native for Scala 3.
  • Add standalone bash completion. This allows user programs to generate bash scripts for completion, rather than relying on the program itself to generate completions. Although the former is less powerful than the latter, it is suitable for JVM programs, where the startup cost would be prohibitive for interactive completions.
  • Remove the ability for parameters to fall back to values provided in a configuration file. This was experimental, and configuration files are not in scope of this project.
  • Move INI-style configuration parser into separate package.
  • Add toggles for default help and bash-completion parameters.

0.14.0

Rename project to scala-argparse.

0.13.0

  • Remove ability to read args from a file (predef reader). This caused ordering issues in the parser and seemed like a recipe for obsuring config origins.
  • Implement an ini-style config parser, and allow params to read from config.

0.12.1

  • Add range reader.
  • Upgrade mill to 0.9.10

0.12.0

  • Add experimental case class parser (Scala 3 only), available under parser.settings (the previous mutable settings parser has been renamed to parser.mutableSettings).
  • Add readers for:
    • scala.concurrent.time.Duration
    • Common java.time data types
    • Collections of paths. These readers use : as a separator, instead of the usual ,'.
  • Upgrade to Scala 3.0.2.

0.11.0

  • Upgrade to Scala 2.13.6
  • Refactor XDG directory implemention
  • Refactor default help message

0.10.3

  • Add support for Scala 3.0.0

0.10.2

  • Wrap text in help messages
  • Add support for Scala 3.0.0-RC3

0.10.1

  • Add support for Scala 3.0.0-RC2

0.10.0

  • Add readers for () => InputStream, () => OutputStream and geny.Readable. These readers follow the convention of using '-' to read/write from stdin/stdout.
  • Change the parser to support inserting parameters during parsing. Predefs can now be specified as parameters.

0.9.0

  • Show default values of named parameters in help messages.
  • Implement XDG Base Directory Specification.
  • Introduce the concept of a "predef", a flat configuration file which contains command line arguments

0.8.0 and before

A command line parser for Scala 2 and 3, featuring:

  • simple interfaces
  • bash completion