Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

optparse-applicative option with multiple values

Tags:

haskell

I'm using optparse-applicative and I'd like to be able to parse command line arguments such as:

$ ./program -a file1 file2 -b filea fileb

i.e., two switches, both of which can take multiple arguments.

So I have a data type for my options which looks like this:

data MyOptions = MyOptions {
    aFiles :: [String]
  , bFiles :: [String] }

And then a Parser like this:

config :: Parser MyOptions
config = MyOptions
         <$> option (str >>= parseStringList)
             ( short 'a' <> long "aFiles" )
         <*> option (str >>= parseStringList)
             ( short 'b' <> long "bFiles" )

parseStringList :: Monad m => String -> m [String]
parseStringList = return . words

This approach fails in that it will give the expected result when just one argument is supplied for each switch, but if you supply a second argument you get "Invalid argument" for that second argument.

I wondered if I could kludge it by pretending that I wanted four options: a boolean switch (i.e. -a); a list of strings; another boolean switch (i.e. -b); and another list of strings. So I changed my data type:

data MyOptions = MyOptions {
    isA    :: Bool
  , aFiles :: [String]
  , isB    :: Bool
  , bFiles :: [String] }

And then modified the parser like this:

config :: Parser MyOptions
config = MyOptions
         <$> switch
             ( short 'a' <> long "aFiles" )
         <*> many (argument str (metavar "FILE"))
         <*> switch
             ( short 'b' <> long "bFiles" )
         <*> many (argument str (metavar "FILE"))

This time using the many and argument combinators instead of an explicit parser for a string list.

But now the first many (argument str (metavar "FILE")) consumes all of the arguments, including those following the -b switch.

So how can I write this arguments parser?

like image 563
ironchicken Avatar asked Jan 20 '16 00:01

ironchicken


1 Answers

Aside from commands, optparse-applicative follows the getopts convention: a single argument on the command line corresponds to a single option argument. It's even a little bit more strict, since getopts will allow multiple options with the same switch:

./program-with-getopts -i input1 -i input2 -i input3

So there's no "magic" that can help you immediately to use your program like

./program-with-magic -a 1 2 3 -b foo bar crux

since Options.Applicative.Parser wasn't written with this in mind; it also contradicts the POSIX conventions, where options take either one argument or none.

However, you can tackle this problem from two sides: either use -a several times, as you would in getopts, or tell the user to use quotes:

./program-as-above -a "1 2 3" -b "foo bar crux" 
# works already with your program!

To enable the multiple use of an option you have to use many (if they're optional) or some (if they aren't). You can even combine both variants:

multiString desc = concat <$> some single
  where single = option (str >>= parseStringList) desc

config :: Parser MyOptions
config = MyOptions
     <$> multiString (short 'a' <> long "aFiles" <> help "Use quotes/multiple")
     <*> multiString (short 'b' <> long "bFiles" <> help "Use quotes/multiple")

which enables you to use

./program-with-posix-style -a 1 -a "2 3" -b foo -b "foo bar"

But your proposed style isn't supported by any parsing library I know, since the position of free arguments would be ambiguous. If you really want to use -a 1 2 3 -b foo bar crux, you have to parse the arguments yourself.

like image 52
Zeta Avatar answered Nov 16 '22 01:11

Zeta