I'm trying to build a CLI food journal app.
And this is the data type I want the user input to be parsed in.
data JournalCommand =
JournalSearch Query DataTypes Ingridents BrandOwnder PageNumber
| JournalReport Query DataTypes Ingridents BrandOwnder PageNumber ResultNumber
| JournalDisplay FromDate ToDate ResultNumber
| JournalStoreSearch Query DataTypes Ingridents BrandOwnder PageNumber ResultNumber StoreFlag
| JournalStoreCustom CustomEntry OnDate StoreFlag
| JournalDelete FromDate ToDate ResultNumber
| JournalEdit CustomEntry ResultNumber
deriving (Show, Eq)
and because there's a lot of overlap I have a total of 8 functions with Parser a
type.
Functions like these
-- | Search Query
aQueryParser :: Parser String
aQueryParser = strOption
( long "search"
<> short 's'
<> help "Search for a term in the database"
)
The idea if to ultimately have a function like this
runJournal :: JournalCommand -> MT SomeError IO ()
runJournal = \case
JournalSearch q d i b p
-> runSearch q d i b p
JournalReport q d i b p r
-> runSearchAndReport q d i b p r
...
...
where MT
is some monad transformer that can handle error
+ IO
. Not sure yet.
The question is: How do I setup the parseArgs
function
parseArgs :: IO JournalCommand
parseArgs = execParser ...
and parser
function
parser :: Parser JournalCommand
parser = ...
so that I'd be able to parse user input into JournalCommand
and then return the data to relevant functions.
I know I can fmap
a data type like this
data JournalDisplay { jdFromDate :: UTCTime
, jdToDate :: UTCTime
, jdResultNumber :: Maybe Int
}
as
JournalDisplay
<$>
fromDateParser
<*>
toDateParser
<*>
optional resultNumberParser
But I'm not sure how to go about doing that with my original data structure.
I think I need to have a list like this [Mod CommandFields JournalCommand]
which I may be able to pass into subparser
function by concatenating the Mod
list. I'm not completely sure.
In optparse-applicative there's the Parser
type, but also the ParserInfo
type which represents a "completed" parser holding extra information like header, footer, description, etc... and which is ready to be run with execParser
.
We go from Parser
to ParserInfo
by way of the info
function which adds the extra information as modifiers.
Now, when writing a parser with subcommands, each subcommand must have its own ParserInfo
value (implying that it can have its own local help and description).
We pass each of these ParserInfo
values to the command
function (along with the name we want the subcommand to have) and then we combine the [Mod CommandFields JournalCommand]
list using mconcat
and pass the result to subparser
. This will give us the top-level Parser
. We need to use info
again to provide the top-level description and get the final ParserInfo
.
An example that uses a simplified version of your type:
data JournalCommand =
JournalSearch String String
| JournalReport String
deriving (Show, Eq)
journalParserInfo :: O.ParserInfo JournalCommand
journalParserInfo =
let searchParserInfo :: O.ParserInfo JournalCommand
searchParserInfo =
O.info
(JournalSearch
<$> strArgument (metavar "ARG1" <> help "This is arg 1")
<*> strArgument (metavar "ARG2" <> help "This is arg 2"))
(O.fullDesc <> O.progDesc "desc 1")
reportParserInfo :: O.ParserInfo JournalCommand
reportParserInfo =
O.info
(JournalReport
<$> strArgument (metavar "ARG3" <> help "This is arg 3"))
(O.fullDesc <> O.progDesc "desc 2")
toplevel :: O.Parser JournalCommand
toplevel = O.subparser (mconcat [
command "search" searchParserInfo,
command "journal" reportParserInfo
])
in O.info toplevel (O.fullDesc <> O.progDesc "toplevel desc")