subcommands with optparse-applicative
Here is a Reddit thread about optparse-applicative, a powerful Haskell library for command-line parsing.
For simple cases, pattern-matching on the list of arguments returned by getArgs is surprisingly effective, and more pleasant that handling the argument array in other languages.
But the moment you want do do something more complex, with good error messages and sub-commands with individual descriptions, it’s time to use a library.
I always turn optparse-applicative when I want to write parsers with sub-commands. However I always forget the details about how to do it. This is a refresher for future-me and other people who happen to read this.
First is the distinction between Parser and ParserInfo.
Parser values know how to parse positional arguments, flags and options. Unsurprisingly, they are constructed with functions like argument, flag and option.
A potentially confusing bit is that the command-line names for flags and options are supplied through the Mod FlagFields / Mod OptionFields monoids. See the long and short modifiers and the HasName typeclass for more details.
(Notice that the ArgumentFields type doesn’t have a HasName instance, as it wouldn’t make sense for positional arguments. You can’t use long or short with it. It does have a HasMetavar instance, so you can still use metavar. Conversely, FlagFields doesn’t have a HasMetavar instance.)
ParserInfo values are “completed” Parsers which are ready to be run with functions like execParser. They hold extra information that belongs to the command-line parser as a whole, like headers, footers, the program description, and so on. You go from Parser to ParserInfo through the info function.
I often write this little function which adds a help option and a program description:
info’ :: O.Parser a -> String -> O.ParserInfo ainfo’ p desc = O.info (O.helper <*> p) (O.fullDesc <> O.progDesc desc)
(Notice that, unlike Parser, ParserInfo doesn’t have Applicative or Alternative instances, it only has a Functor instance. It makes sense that ParserInfo combination is restricted, because it holds global info.)
Now for the subcommands. You must start by having ParserInfo values ready for each of your planned subcommands, constructed using the functions described in the previous section. Each subcommand is like a tiny full-featured command-line parser!
Then you use the command function to give an actual name name to each sub-command in the higher-level parser. The result is a Mod CommandFields monoidal value for each sub-command.
Finally, smash together all your named sub-commands using functions from Monoid, and pass them to the subparser function to get the higher-level Parser value.
For some example code, check out this old project of mine. Notice how I define a Command sum type to hold the results of each possible sub-command.