Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Howto create a nested/conditional option with optparse-applicative?

Is possible to create a haskell expression, using the methods in optparse-applicative, that parses program options like this?

program [-a [-b]] ...

-a and -b are optionals flags (implemented using switch), with the constraint that the -b option only is valid if -a is typed before.

Thanks

like image 739
The Linux Kitten Avatar asked Aug 09 '14 23:08

The Linux Kitten


2 Answers

This is possible, with slight tweaks, two different ways:

  1. You can make a parser that only allows -b if you've got -a, but you can't insist then that the -a comes first, since optparse-applicative's <*> combinator doesn't specify an order.
  2. You can insist that the -b option follows the a option, but you do this by implementing a as a command, so you lose the - in front of it.

Applicative is definitely strong enough for this, since there's no need to inspect the values returned by the parsers to determine whether -b is allowed, so >>= is not necessary; If -a succeeds with any output, -b is allowed.

Examples

I'll use a data type to represent which arguments are present, but in reality these would be more meaningful.

import Options.Applicative

data A = A (Maybe B)   deriving Show 
data B = B             deriving Show

So the options to our program maybe contain an A, which might have a B, and always have a string.

boption :: Parser (Maybe B)
boption = flag Nothing (Just B) (short 'b')

Way 1: standard combinators - -b can only come with -a (any order)

I'll use flag' () (short 'a') which just insists that -a is there, but then use *> instead of <*> to ignore the return value () and just return whatever the boption parser returns, giving options -a [-b]. I'll then tag that with A :: Maybe B -> A and finally I'll make the whole thing optional, so you have options [-a [-b]]

aoption :: Parser (Maybe A)
aoption = optional $ A <$> (flag' () (short 'a' ) *> boption)

main = execParser (info (helper <*> aoption) 
                        (fullDesc <> progDesc "-b is only valid with -a")) 
        >>= print

Notice that since <*> allows any order, we can put -a after -b (which isn't quite what you asked for, but works OK and makes sense for some applications).

ghci> :main -a 
Just (A Nothing)
ghci> :main -a -b
Just (A (Just B))
ghci> :main -b -a
Just (A (Just B))
ghci> :main -b
Usage: <interactive> [-a] [-b]
  -b is only valid with -a
*** Exception: ExitFailure 1

Way 2: command subparser - -b can only follow a

You can use command to make a subparser which is only valid when the command string is present. You can use it to handle arguments like cabal does, so that cabal install and cabal update have completely different options. Since command takes a ParserInfo argument, any parser you can give to execParser can be used, so you can actually nest commands arbitrarily deeply. Sadly, commands can't start with -, so it'll be program [a [-b]] ... instead of program [-a [-b]] ....

acommand :: Parser A
acommand = subparser $ command "a" (info (A <$> (helper <*> boption)) 
                                         (progDesc "you can '-b' if you like with 'a'"))

main = execParser (info (helper <*> optional acommand) fullDesc) >>= print

Which runs like this:

ghci> :main 
Nothing
ghci> :main a 
Just (A Nothing)
ghci> :main a -b
Just (A (Just B))
ghci> :main -b a
Usage: <interactive> [COMMAND]
*** Exception: ExitFailure 1

So you have to precede -b with a.

like image 181
AndrewC Avatar answered Oct 04 '22 20:10

AndrewC


I'm afraid you can't. This is precisely the scenario that Applicative alone can't handle while Monad can: changing the structure of later actions based on earlier results. In an applicative computation, the "shape" always needs to be known beforehand; this has some advantages (like speeding up so array combinations, or giving out a nice readable help screen for command-line options), but here it limits you to parsing "flat" options.

The interface of optparse-applicative also has Alternative though, which does allow dependent parsing, albeit in a different way as shown by AndrewC.

like image 33
leftaroundabout Avatar answered Oct 04 '22 20:10

leftaroundabout