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.