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
withAnswersis true the program will provide the solutions to the problems. The default value is false. - The value of
maxDigitsis the maximum number of digits for each operand in the problems. The default value is 4. - The value of
separationis 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
outputfield 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 areFlagFieldsandOptionFields. -
HasValue, whose instances areOptionFieldsandArgumentFields. 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 areOptionFieldsandArgumentFields. 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
withAnswerstoTrue, 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
flagbut 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
Readtype class. str :: IsString s => ReadM s- A reader for any member of the
IsStringtype 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 aargument :: 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 sstrArgument :: 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
probsInGroupmust 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 allowmaxDigitsto 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.