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:
-
Standalone. A completion script has been sourced by your shell and is what is called to generate completions when you press tab.
-
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.