5

I just found out that my carefully crafted parser fails to parse any string I throw at it:

roi :: Parser (Maybe ROI)
roi = optional $ option (ROI <$> auto <*> auto <*> auto <*> auto)
               $ long "roi" <> metavar "ROI" <> help "Only process selected region of interest"

where ROI = ROI Int Int Int Int

If that is important, it is nested in a higher parser

options :: Parser Opts
options = Opts <$> input <*> output <*> roi <*> startT <*> endT  

where Opts is an appropriate ADT.

Now I assumed that the roi parser will parse expressions such as --roi 1 2 3 4 but it fails with Invalid argument '128' and giving me usage message.

--roi 1 instead parses but returns Just (ROI 1 1 1 1)

Is there a way to make this work?

duplode
  • 33,731
  • 7
  • 79
  • 150
fho
  • 6,787
  • 26
  • 71
  • I have edited my answer to clarify that `--roi 1 2 3 4` won't work with any of the approaches here -- the closest you can get is something like `--roi 1,2,3,4`, as in Cubic's answer. – duplode Dec 16 '16 at 14:55

2 Answers2

7

I don't think options are supposed to consume multiple arguments. At least I'm not sure how you'd go about implementing that. I'd suggest simply going away from that idea and putting your ROI options into a single argument, using syntax like --roi 1,2,3,4.

You'd simply have to implement a custom reader for that, here's an example of how you could do that:

module Main where

import Options.Applicative

data ROI = ROI Int Int Int Int
  deriving Show

-- didn't remember what this function was called, don't use this
splitOn :: Eq a => a -> [a] -> [[a]]
splitOn sep (x:xs) | sep==x     = [] : splitOn sep xs
                   | otherwise = let (xs':xss) = splitOn sep xs in (x:xs'):xss
splitOn _ [] = [[]]

roiReader :: ReadM ROI
roiReader = do
  o <- str
  -- no error checking, don't actually do this
  let [a,b,c,d] = map read $ splitOn ',' o
  return $ ROI a b c d

roiParser :: Parser ROI
roiParser = option roiReader (long "roi")

main :: IO ()
main = execParser opts >>= print where
  opts = info (helper <*> roiParser) fullDesc
Cubic
  • 14,902
  • 5
  • 47
  • 92
  • Why can't you just use `' '` as a separator instead of `','`? That way you could parse `roi 1 2 3 4` – jkeuhlen Dec 16 '16 at 15:24
  • @jkeuhlen Because if you then try to use it from a shell you'll have to quote the argument: `hello --roi "1 2 3 4"`, which just makes it harder to use, so kind of missing the point ;) – Cubic Dec 16 '16 at 15:29
5

The type of option is:

option :: ReadM a -> Mod OptionFields a -> Parser a

ReadM, in turn, is "A newtype over 'ReaderT String Except', used by option readers". Since option is using ReaderT under the hood, when you use it with the Applicative instance of ReadM like you did here...

ROI <$> auto <*> auto <*> auto <*> auto

... the same, and whole, input string is supplied to each of the four auto parsers, because that's how the reader/function applicative instances work.

If you want values separated by spaces to be parsed into a single ROI, you need to write a custom parser. Here is a not particularly tidy attempt at that, built around eitherReader. Note that this will require the values to be within quotation marks (--roi "1 2 3 4"), so that they are taken in as a single string. Cubic's answer suggests an alternative approach, which uses comma-separated values instead (--roi 1,2,3,4).

import Text.Read (readEither)

-- etc.

roi :: Parser (Maybe ROI)
roi = optional
    $ option (eitherReader $ \inp -> case traverse readEither (words inp) of
        Right [x, y, z, w] -> Right (ROI x y z w)
        Right _ -> Left "ROI requires exactly 4 values"
        Left _ -> Left "ROI requires integer values")
    $ long "roi" <> metavar "ROI" <> help "Only process selected region of interest"

Success and failure modes:

GHCi> execParserPure defaultPrefs (info roi mempty) ["--roi","1 2 3 4"]
Success (Just (ROI 1 2 3 4))
GHCi> execParserPure defaultPrefs (info roi mempty) ["--roi","1 2 3"]
Failure (ParserFailure (option --roi: ROI requires exactly 4 values

Usage: <program> [--roi ROI],ExitFailure 1,80))
GHCi> execParserPure defaultPrefs (info roi mempty) ["--roi","1 2 foo 4"]
Failure (ParserFailure (option --roi: ROI requires integer values

Usage: <program> [--roi ROI],ExitFailure 1,80))
duplode
  • 33,731
  • 7
  • 79
  • 150
  • 2
    Note that this reads the command line option `--roi "1 2 3 4"`, which is different than `--roi 1 2 3 4`. There isn't a sane optparse-applicative parser that parses `--roi 1 2 3 4`; an optparse-applicative `option` "is an option which takes a *single* argument, parses it, and returns a value." – Cirdec Dec 16 '16 at 14:39
  • @Cirdec Clarification added to the answer; thanks for highlighting that. – duplode Dec 16 '16 at 14:55
  • Yep ... this works (and I am doing something similar by parsing for a four tuple) ... but I feel @Cubics answer is "more true". – fho Dec 16 '16 at 15:06
  • @fho From an end-user perspective, `--roi 1,2,3,4` is indeed a bit more pleasant than `--roi "1 2 3 4"`. – duplode Dec 16 '16 at 15:33
  • Definitely... But in this case the end user is me... So... I don't mind :-) – fho Dec 16 '16 at 15:47