A Gentle Introduction to optparse-applicative
By Pedro R. Borges
In this article, you’ll learn to use optparse-applicative, an excellent Haskell library for parsing arguments for command-line programs. We will create a parser for an example app with several kinds of options, incrementally introducing the most commonly used library features.
I will also show how to restrict the accepted values of some options. I will not, however, cover features such as sub-command parsing and the library’s facilities for enabling shell completion.
Table Of Contents
First steps
In this section, I describe briefly how to include the library in our program and the command-line options of the program used as an example for this guide.
Including optparse-applicative in your program
The library is distributed in the optparse-applicative package, available in Hackage, and it is included in Stackage.
To use the library in our program we need, of course, to make this package, and its dependencies, available to the compiler.
Since I use stack in this tutorial, it is enough to include the package in an appropriate dependencies section in the project’s packageyaml
.
I’ll include optparse-applicative in the dependencies of the executable section. For our example, we won’t need any additional packages, apart from the base library, which I’ve included in the global dependencies. Following are the relevant lines, where you can see that we’ll be generating an executable called “arranger”:
dependencies:
- base >= 4.7 && < 5
executables:
arranger:
main: Main.hs
source-dirs: app
dependencies:
- optparse-applicative
In our app, and for most uses of the library, you’ll only need to include one module in the program:
import Options.Applicative
Example app: arithmetic arranger
We will create a parser for the options and argument of an arithmetic arranger, a generalization of the arithmetic formatter project that I did in Python for a FreeCodeCamp certification. The program takes an input file with arithmetic problems –sums and subtractions– and formats them in groups arranged vertically and side by side.
The parser will return the following type:
data AppOptions = AppOptions
{ withAnswers :: !Bool
, maxDigits :: !Int
, separation :: !Int
, probsInGroup :: !Int
, output :: !AppOutput
, inputFile :: !FilePath
}
With the AppOutput
type defined as:
data AppOutput = StdOut | OutputFile !FilePath
The inputFile
field will contain the name of the file with the arithmetic problems to be arranged, which must be provided as an argument in the CLI.
The rest of the fields have default values, but we’ll provide CLI options to modify them. The meaning of these fields are:
- If
withAnswers
is true the program will provide the solutions to the problems. The default value is false. - The value of
maxDigits
is the maximum number of digits for each operand in the problems. The default value is 4. - The value of
separation
is the number of spaces between each vertically-formatted problem. The default value is 4. - The problems are arranged in horizontal groups, each containing the number of problems indicated by
probsInGroup
, 5 by default. - The
output
field indicates whether the app will write the arranged problems to the standard output or a file.
The ParserInfo
Our main purpose is to get the app options from the CLI; not surprisingly, then, we’ll define a top-level IO function that returns the options:
arrangerOptions :: IO AppOptions
Our app’s main
function would typically be something like this:
main = do
opts <- arrangerOptions
...
However, since in this guide we won’t actually use the options, we’ll just print them.
Our main
would then be:
main = arrangerOptions >>= print
The function arrangerOptions
must run the parser that we are about to define.
But, the function provided by the library to run the parsers is:
execParser :: ParserInfo a -> IO a
Therefore, we need a ParserInfo AppOptions
, instead of a Parser AppOptions
, as you might have expected.
A ParserInfo
contains the actual (applicative) options parser,
as well as information about the app to use for help and error messages and to describe the parser’s behavior.
A ParserInfo
is constructed with the function info :: Parser a -> InfoMod a -> ParserInfo a
,
where InfoMod
is the type of the extra information that I just mentioned.
The elements of the type InfoMod
are called modifiers of the ParserInfo
and,
since this is an instance of Monoid
, its elements can be combined into one.
Being a monoid also means that we have a predefined identity element.
The library names this element as idm
, but I’ll use the already-known mempty
, and use it for our first definition of arrangerOptions
:
arrangerOptions =
execParser $ info appOptionsParser mempty
In the following sections, we’ll define appOptionsParser
,
and the ParserInfo
modifier to use instead of mempty
in the arrangerOptions
definition.
Meanwhile, I’ll define appOptionsParser
as undefined
, for the first version of our app, which compiles, but can not be run yet:
|
|
The appOptions parser
Since the Parser
type provided by the library is an applicative functor,
we can define appOptionsParser
with the (applicative) application of AppOptions
to the parsers for each option.
For now, we’ll use pure
for each option to define a trivial parser,
always returning a fixed name for the input file, and the default values for the remaining AppOptions
fields:
appOptionsParser =
AppOptions
<$> withAnswersParser
<*> maxDigitsParser
<*> separationParser
<*> probsInGroupParser
<*> outputParser
<*> inputFileParser
where
withAnswersParser :: Parser Bool
withAnswersParser = pure False
maxDigitsParser :: Parser Int
maxDigitsParser = pure 4
separationParser :: Parser Int
separationParser = pure 4
probsInGroupParser :: Parser Int
probsInGroupParser = pure 5
outputParser :: Parser AppOutput
outputParser = pure StdOut
inputFileParser :: Parser FilePath
inputFileParser = pure "input.txt"
Here, you might prefer to use pure AppOptions <*>
instead of AppOptions <$>
, as suggested by a reader.
The previous definition is not very useful, but with it, we can run our app and get the AppOptions
:
$stack exec arranger
AppOptions {withAnswers = False, maxDigits = 4, separation = 4, probsInGroup = 5, output = StdOut, inputFile = "input.txt"}
Since this parser does not read anything from the CLI, passing any argument to the app at this stage would be an error.
Interestingly, even with the current bare-bones definition for arrangerOptions
, we get an appropriate error message if we try to pass any argument to our app:
$stack exec arranger -- anything
Invalid argument `anything'
Usage: arranger.EXE
We now have to define the parsers to obtain the values for the AppOptions
components.
Each value will be provided as some kind of argument when the app is invoked in the CLI.
Optparse-applicative classifies these as flags, options, and arguments, and provides parser builders for each.
They are described in the documentation as follows.
- Flags:
- Simple no-argument options. When a flag is encountered on the command line, its value is returned.
- Options:
- Options with an argument. An option can define a reader, which converts its argument from String to the desired value, or throws a parse error if the argument does not validate correctly.
- Arguments:
- Positional arguments, validated in the same way as option arguments.
The specification, that is, the details for each flag, option, or argument, are contained, respectively, in the types FlagFields
, OptionFields
, and ArgumentFields
.
Some of these types have common properties, and some properties are not defined for some of them.
Flags, for example, have a name but do not have values, while arguments have a value but not a name.
The field types are classified according to their properties into four “has” type classes, whose names are self-descriptive. For this guide, I will consider only three of them:
-
HasName
, whose instances areFlagFields
andOptionFields
. -
HasValue
, whose instances areOptionFields
andArgumentFields
. Since the value returned by a flag parser is determined solely by the presence or absence of a flag, they are not given explicit values. -
HasMetavar
, whose instances areOptionFields
andArgumentFields
. This property specifies a metavariable to use in the help messages for the option or argument.
Modifiers
Usually, we don’t have to deal with these fields directly,
instead, we use fields’ modifiers, of type Mod
.
For example, in our app, the value for withAnswers
will be given by a flag.
Therefore, the specification for that flag will be of type FlagFields Bool
, and their modifiers of type Mod FlagFields Bool
.
The library provides several functions to create these modifiers, some of which are:
short :: HasName f => Char -> Mod f a
- This function yields a modifier that sets a one-character name for a flag or an option.
For example, to have “-a” as the short flag to set
withAnswers
toTrue
, we’ll use the modifiershort 'a'
. long :: HasName f => String -> Mod f a
- Similar to
short
, but allows setting a longer name, which can be used preceded by “--” in the CLI. ForwithAnswers
, for example, we’ll uselong "with-answers"
, so that the user can use either “-a” or “--with-answers”. value :: HasValue f => a -> Mod f a
- This function builds modifiers to set the default values for options and arguments.
For example, to set the default value for
maxDigits
, we’ll usevalue 4
, of typeMod OptionFields Int
. An option or argument without a default value is mandatory. metavar :: HasMetavar f => String -> Mod f a
- The modifiers created by this function assign a name for the metavariables for options and arguments.
help :: String -> Mod f a
- This is used to specify the help text for flags, options, and arguments. For example, we’ll use
help "Generate problem answers"
for the “-a” flag.
Just as the modifiers for the ParserInfo
, the modifiers for flags, options, and arguments, form a monoid.
That is, Mod f a
is an instance of Monoid
, and hence also of Semigroup
so that we can combine several of them into a single modifier with the semigroup operator (<>
).
As an example, the modifier that we will use for withAnswersParser
can be defined as:
modifier :: Mod FlagFields Bool
modifier =
short 'a'
<> long "with-answers"
<> help "Generate problem answers"
We will now define the parsers for the AppOptions
components. Let’s begin with withAnswersParser
, which is the simplest.
Parsing flags
As I said before, the value for withAnswers
will be provided by a flag.
The library defines three parser builders for flags:
flag :: a -> a -> Mod FlagFields a -> Parser a
- This function builds a parser that returns its second argument when the flag is given in the CLI, or the first argument if it is not. That is, the first argument is the default value. The third argument is the modifier with the specification of the flag.
flag' :: a -> Mod FlagFields a -> Parser a
- Similar to
flag
but without a default value. We don’t have a use case for this function in our app. switch :: Mod FlagFields Bool -> Parser Bool
- This is just a special case for a boolean flag, defined as
switch = False True
.
Using switch
with the modifier as described above we can define withAnswersParser
as:
withAnswersParser =
switch $
short 'a'
<> long "with-answers"
<> help "Generate problem answers"
Plugging this definition in our Main
module, we can now set withAnswers
to True
invoking our app with “–with-answers” (or with “-a”):
$stack exec arranger -- --with-answers
AppOptions {withAnswers = True, maxDigits = 4, separation = 4, probsInGroup = 5, output = StdOut, inputFile = "input.txt"}
Readers and parsing options and arguments
The remaining components of AppOptions
will correspond to options, except inputFile
, which will be given as an argument.
Options and arguments need an explicit value, which is read from the CLI and parsed into the appropriate data type by a reader, of type ReadM
.
The library allows the definition of complex parsers, and even the use of parsers from other libraries. In this article, I’ll only use two of the simple readers provided by the library:
auto :: Read a => ReadM a
- A simple reader based on the
Read
type class. str :: IsString s => ReadM s
- A reader for any member of the
IsString
type class.
We only need to read integers and strings (FilePath
) values, so these readers will suffice.
There are two parser builders for options and two for arguments. In each case, one general and one for string values. The general builders are:
option :: ReadM a -> Mod OptionFields a -> Parser a
argument :: ReadM a -> Mod ArgumentFields a -> Parser a
These functions take the reader to parse a value from the CLI and the specification of the option or argument and yield the parser for it.
The other two parser builders are special cases of the previous ones, using the str
reader:
strOption :: IsString s => Mod OptionFields s -> Parser s
strArgument :: IsString s => Mod ArgumentFields s -> Parser s
We’ll now define the remaining sub-parsers to complete the definition of appOptionsParser
.
The definition of maxDigitsParser
, separationParser
, and probsInGroupParser
are very similar. I’ll describe here maxDigitsParser
, and you can find the rest in the listing of the next version of the app module.
The definition of maxDigitsParser
is a straightforward application of the appropriate functions hitherto described.
Since maxDigits
is an integer, we can use the auto
reader in the option
builder, along with the modifier with the relevant properties for the option, to which we give the short name “d”, and the long name “max-digits”:
maxDigitsParser =
option auto $
short 'd'
<> long "max-digits"
<> metavar "Int"
<> help "Maximum number of digits allowed in a term."
<> value 4
The parser to obtain the value of output
in AppOptions
is a bit more interesting.
We can use the str
reader with the type ReadM FilePath
to get the filename from the CLI;
however, the value returned by the parser must be of type AppOutput
.
Since ReadM
is a functor, we can use the following reader:
OutputFile <$> str :: ReadM AppOutput
With this reader, and providing appropriate modifiers we have:
outputParser =
option (OutputFile <$> str) $
short 'o'
<> long "output-file"
<> metavar "FILE"
<> help
"Output file for the arranged problems. \
\ If not given, output is written to standard output."
<> value StdOut
Finally, for inputFileParser
, we use the strArgument
reader and the required modifiers, as you can see in the following listing:
|
|
What about --help?
Linux CLI apps are expected to display a help text when invoked with “--help” or “-h”. Optparse-applicative provides a special parser builder for this purpose:
helper :: Parser (a -> a)
If you don’t have much experience with applicative functors, you might find the type Parser (a->a)
unusual.
But remember the type of applicative application (<*>) :: Applicative f => f (a -> b) -> f a -> f b
and helper
’s type clearly makes it a candidate for a first argument of <*>
.
Using GHCI, we can check the type of (helper <*>)
:
ghci>:t (helper <*>)
(helper <*>) :: Parser b -> Parser b
Given this type, we can interpret helper
as a Parser
modifier.
This function adds to its argument a parser for “-h” and “–help”.
When any of these options are detected, the parser displays the help text and then fails, so that the app does not proceed with its normal execution.
We want to incorporate this feature into our options parser.
We do this using helper <*> appOptionsParser
instead of appOptionsParser
in arrangerOptions
:
arrangerOptions =
execParser $ info (helper <*> appOptionsParser) mempty
Using this definition, our program can provide better help which includes descriptions for each option:
$stack exec arranger -- --help
Usage: arranger.EXE [-a|--with-answers] [-d|--max-digits Int]
[-s|--separation Int] [-g|--group-length Int]
[-o|--output-file FILE] FILE
Available options:
-h,--help Show this help text
-a,--with-answers Generate problem answers.
-d,--max-digits Int Maximum number of digits allowed in a term.
-s,--separation Int Number of spaces between vertically arranged
problems.
-g,--group-length Int Number of problems in each horizontal group.
-o,--output-file FILE Output file for the arranged problems. If not given,
output is written to standard output.
FILE Input file with one problem per line.
The ParserInfo
modifiers
So far, we have been using mempty
as the modifier for the ParserInfo
defined in arrangerOptions
.
In this section, we will use a non-trivial info modifier, to provide some information about the app.
We will use only three of the functions provided by the library to create info modifiers, all of the same type:
progdesc, header, footer :: String -> InfoMod a
The modifiers created by these functions specify, respectively, a program description, a header, and a footer to use in the help text for the app. There are also functions to create modifiers to set the exit code when the parser fails, and some restrictions on the order of the flags, options, and arguments in the CLI.
As InfoMod a
is an instance of Semigroup
, we combine the modifiers with <>
to obtain the info modifier for our app, and we get the final version of arrangerOptions
:
arrangerOptions =
execParser $
info (helper <*> appOptionsParser) $
progDesc "Arranges arithmetic problems (sums and subtractions) vertically and side by side"
<> header "arranger - arithmetic arranger version 0.1.0.0"
<> footer
"An example app for \
\https://www.prborges.com/2023/introduction-to-optparse-applicative"
Using this definition, our app displays a help text with the information provided by the info modifiers:
$stack exec arranger -- -h
arranger - arithmetic arranger version 0.1.0.0
Usage: arranger.EXE [-a|--with-answers] [-d|--max-digits Int]
[-s|--separation Int] [-g|--group-length Int]
[-o|--output-file FILE] FILE
Arranges arithmetic problems (sums and subtractions) vertically and side by
side
Available options:
-h,--help Show this help text
-a,--with-answers Generate problem answers.
-d,--max-digits Int Maximum number of digits allowed in a term.
-s,--separation Int Number of spaces between vertically arranged
problems.
-g,--group-length Int Number of problems in each horizontal group.
-o,--output-file FILE Output file for the arranged problems. If not given,
output is written to standard output.
FILE Input file with one problem per line.
An example app for
https://www.prborges.com/2023/introduction-to-optparse-applicative
Restricting the values for some options
Our app is now capable of parsing all of its options and arguments and can display comprehensive help about its usage.
However, the options parser accepts invalid values for some components of AppOptions
:
- The value of
probsInGroup
must be greater than zero since each horizontal group must have at least one problem. - The separation between vertically-arranged problems can not be negative and, although technically it could be zero, we will ask for a positive value for
separation
. - The numbers in the problems must have, of course, at least one digit, and they can not have too many digits to be representable as an
Int
. We will allowmaxDigits
to be at most 15.
We have been using the auto
reader to read the values for these three options, which accept any integer value.
We’ll now define two custom readers to constrain the integer values accepted:
one for positive integers and one that accepts integers within given lower and upper bounds.
To read positive integers we’ll define positiveInt
, of type ReadM Int
, which will take an integer read by auto
, and fail with an error if the integer is not positive.
For this purpose, the library provides the function:
readerError :: String -> ReadM a
Since ReadM
is an instance of Monad
, one way to define positiveInt
is the following:
positiveInt = do
i <- auto
when (i < 1) $ readerError "Value must be greater than 0"
pure i
This requires importing when
from the Control.Monad module
if using the standard prelude, as we are doing in our app.
Another possible definition, which I’ll use, for no particular reason, is:
positiveInt = auto >>= checkPositive
where
checkPositive i
| i > 0 = pure i
| otherwise = readerError "Value must be greater than 0"
We must now use this reader instead of auto
in separationParser
, where we also update the help text for the option:
separationParser =
option positiveInt $
short 's'
<> long "separation"
<> metavar "Int"
<> help "Number of spaces between vertically arranged problems. Must be at least 1."
<> value 4
Using this separationParser
, our app does not accept non-positive values for separation
:
$stack exec arranger -- --separation -8
option --separation: Value must be greater than 0
Usage: arranger.EXE [-a|--with-answers] [-d|--max-digits Int]
[-s|--separation Int] [-g|--group-length Int]
[-o|--output-file FILE] FILE
Arranges arithmetic problems (sums and subtractions) vertically and side by
side
The definition of probsInGroupParser
must be modified in the same vein, as shown in the listing at the end of this section.
The reader for the value of maxDigits
must check the parsed value against the lower bound (1) and the upper bound (15).
We will parameterize the bounds and define the reader as:
boundedInt :: Int -> Int -> ReadM Int
boundedInt lower upper = auto >>= checkBounds
where
checkBounds i
| lower <= i && i <= upper = pure i
| otherwise =
readerError $
mconcat ["Value must be between ", show lower, " and ", show upper]
We must now use boundedInt 1 15
as the reader in maxDigitsParser
instead of auto
, and we should update the corresponding help message, as we did for separationParser
and probsInGroup
.
We could even use boundedInt
in those parsers, but I’ll leave them as defined before.
We arrive, then, at the final version of our app:
|
|
Final words
Thanks for reading until the end! Hopefully, I have successfully explained the basic features of the library and how to use it to read simple options and arguments for your CLI app. As I have hinted in the article, the library provides several features not covered here and allows the use of parsers from other libraries if needed. I trust that the reader is now on a better footing to explore the library’s documentation to take advantage of its more advanced feature.
I appreciate any feedback about the article, and please let me know if you detect any typos or mistakes in it. Finally, don’t forget to share it if you think it can be of interest to others.
Update on May 29th: I added a comment about the use of <$>
on the definition of appOptionsParser
and renamed the single option parsers after receiving some feedback from @kindaro on Haskell’s Discourse.