haskellapplicativeoptparse-applicative

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


Solution

  • 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.