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
Channel | Links |
---|---|
Forum | GitHub Discussions |
Chat | |
Issues | GitHub |
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
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
andos.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 iscp 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 itecho
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 therequiredParam()
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 aSeq[os.FilePath]
rather than a singleos.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 asflags
. -
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:
- the argument on the command line
- the environment variable
- 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:
-
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.
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
andmutableSetting
. A new macro-based parser is in the works. - Remove
show()
function fromReader
. - Add a
typeName
function toReader
. - Rename
BashCompletion
toInteractiveBashCompletion
. - 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 toparser.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
andgeny.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