haskelloptparse-applicative

Parsing user options into custom data types with OptParse-Applicative


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.


Solution

  • 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")