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"))
<*> commandParserWe use a couple of custom Readers 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 Integers
and Strings. 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.
commandParser :: Parser Command
commandParser = subparser $
command "stories" (withInfo storiesParser "View story")
<> command "profile" (withInfo (pure Profile) "View user's profile")
<> command "projects" (withInfo (pure Projects) "View user's projects")