An example of using sub-parsers with optparse applicative. Largely based on https://github.com/rob-b/pivotal
The first things we define are our data types.
data StoriesOption = StoriesDetail Integer
| StoriesList (Maybe B.ByteString) (Maybe B.ByteString)
deriving (Show)
data Command = Stories StoriesOption
| Profile
| Projects
deriving (Show)
data Options = Options { optionsProjectId :: (Maybe ProjectId)
optionsToken :: (Maybe Token)
, optionsCommand :: Command
, }
When defining the args parser, we start with optionsWithInfo
. Almost all of
my cli apps have this function; it’s the top-level entry that gets executed by
execParser
. Here we setup help, info, and description for our argument
parser - parseCommand
optionsWithInfo :: ParserInfo Options
= info (helper <*> parseCommand)
optionsWithInfo
(fullDesc<> progDesc "Some example cli app")
We define parseCommand
to add two option flags, both of which are optional,
as well as a sub-command commandParser
.
parseCommand :: Parser Options
= Options <$> optional (option readerText (long "project-id" <> help "Project id" <> metavar "PROJECTID"))
parseCommand <*> optional (option readerByteString (long "token" <> help "API token" <> metavar "TOKEN"))
<*> commandParser
We use a couple of custom Reader
s in this app so lets look at those
readerText :: ReadM T.Text
= do
readerText <- readerAsk
s return $ T.pack s
readerByteString :: ReadM BC.ByteString
= do
readerByteString <- readerAsk
s return $ BC.pack s
readerEnum :: Foldable t => t B.ByteString -> ReadM B.ByteString
= eitherReader pred'
readerEnum xs where
= let x = BC.pack arg
pred' arg in
if x `elem` xs
then return x
else Left $ "cannot parse value `" ++ arg ++ "'"
Optparse applicative has a few default
readers
of which I most commonly use auto
and str
to parse options as Integer
s
and String
s. In this case I was using functions internally that required
ByteString
and Text
so I wrote a couple of new readers in order to convert
my appliction’s options to the correct types at the fringes. I also used
readerEnum
to ensure that certain options only support certain
pre-determined values.
The sub-parsers look as follows.
storiesParser :: Parser Command
= Stories <$> (storiesDetailParser <|> storiesListParser)
storiesParser
storiesDetailParser :: Parser StoriesOption
= StoriesDetail <$> argument auto (metavar "story id")
storiesDetailParser
storiesListParser :: Parser StoriesOption
= StoriesList <$> optional (option (readerEnum storyStatuses) (long "status" <> help "Filter by status" <> metavar "status"))
storiesListParser <*> optional (option (readerEnum storyKinds) (long "kind" <> help "Filter by kind"))
The stories
command is designed to either operate on a list of stories or,
given a story id, one specific story. This is the reason that StoriesOption
is a sum type of either StoriesDetail
or StoriesList
.
And here we put together the commandParser
sub-command. The sub-commands use
a little helper function withInfo
(taken from
here)
that, like optionsWithInfo
, adds help, info and descriptions to each
sub-command.
For the Profile
and Projects
data types we don’t require any kind of
parsing as they do not take any form of further args and so for those we
simply use pure
to life them into the Parser
.
commandParser :: Parser Command
= subparser $
commandParser "stories" (withInfo storiesParser "View story")
command <> command "profile" (withInfo (pure Profile) "View user's profile")
<> command "projects" (withInfo (pure Projects) "View user's projects")