haskellcommand-line-argumentsapplicativeoptparse-applicative

optparse-applicative: parsing list of pairs


I'm trying to parse a list of pairs with optparse-applicative. Parsing a single pair works, but parsing arbitrarily many using the many combinator fails.

import           Options.Applicative

pairParser = (,) <$> argument str (metavar "s1")
                 <*> argument str (metavar "s2")

testParser p = getParseResult . execParserPure (prefs idm)
  (info (helper <*> p) fullDesc)

main = do
  print $ testParser pairParser ["one", "two"]
  print $ testParser (many pairParser) []
  print $ testParser (many pairParser) ["one", "two"]
  print $ testParser (many pairParser) ["one", "two", "three", "four"]

Output:

Just ("one","two")   <- good
Just []              <- still good
Nothing              <- does not work
Nothing              <- also does not work

Any ideas?


Solution

  • Disclaimer: I have no experience of doing advanced optparse-applicative tricks, so I might be missing something obvious. Readers: please point it out if that's the case.

    Your problem is that what many does is (in a hand-wavy description) to apply the parser to each chunk of the input, with the chunks in this case consisting of individual arguments, and then collect the results. So many pairParser applies pairParser to ["one"] and then to ["two"], and both parses fail. That being so, you might either replace execParserPure with a function that chunks the arguments in an appropriate way and adjust the rest of the program accordingly, or (what I suspect is the easier choice) abandon pairParser and just post-process the parsed arguments, as in:

    pairArgs :: [a] -> [(a, a)]
    pairArgs = noLeftover . foldr pairNext (Nothing, [])
        where
        noLeftover (m, ps) = case m of
            Nothing -> ps
            _       -> []
        pairNext x (m, ps) = case m of
            Just y  -> (Nothing, (x, y) : ps)
            Nothing -> (Just x, ps)
    
    manyPairsParser :: Parser [(String, String)]
    manyPairsParser = pairArgs <$> many (argument str (metavar "s1 s2.."))
    
    GHCi> testParser manyPairsParser []
    Just []
    GHCi> testParser manyPairsParser ["foo"]
    Just []
    GHCi> testParser manyPairsParser ["foo","bar"]
    Just [("foo","bar")]
    GHCi> testParser manyPairsParser ["foo","bar","baz"]
    Just []
    GHCi> testParser manyPairsParser ["foo","bar","baz","quux"]
    Just [("foo","bar"),("baz","quux")]
    

    (Note that in the demo above I'm handling failure by returning an empty list of pairs, and considering that an odd number of arguments should lead to failure. You'll need to do some adjustments if you want a different behaviour.)