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
optionsWithInfo = info (helper <*> parseCommand)
(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
parseCommand = Options <$> optional (option readerText (long "project-id" <> help "Project id" <> metavar "PROJECTID"))
<*> 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
readerText = do
s <- readerAsk
return $ T.pack s
readerByteString :: ReadM BC.ByteString
readerByteString = do
s <- readerAsk
return $ BC.pack s
readerEnum :: Foldable t => t B.ByteString -> ReadM B.ByteString
readerEnum xs = eitherReader pred'
where
pred' arg = let x = BC.pack 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
storiesParser = Stories <$> (storiesDetailParser <|> storiesListParser)
storiesDetailParser :: Parser StoriesOption
storiesDetailParser = StoriesDetail <$> argument auto (metavar "story id")
storiesListParser :: Parser StoriesOption
storiesListParser = StoriesList <$> optional (option (readerEnum storyStatuses) (long "status" <> help "Filter by status" <> metavar "status"))
<*> 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
.