haskelloptparse-applicative

(Sub)command synonyms for optparse-applicative


I would like to add synonyms for the subcommands in my Haskell command line tool. For example summarise and summarize should yield the same result. Of course I could just add an entirely separate command summarize, that appears as an own element in the output of --help. But maybe there is a more elegant way.

Here is a self-contained example in a stack script opt_ex.hs:

#!/usr/bin/env stack
-- stack --resolver lts-18.17 script --package optparse-applicative

import Options.Applicative
import Data.Semigroup ((<>))

data Options = CmdGreet GreetArgs | CmdGroot GreetArgs

newtype GreetArgs = GreetArgs String

main :: IO ()
main = do
    cmdOpts <- customExecParser (prefs showHelpOnEmpty) (info optParser fullDesc)
    runCmd cmdOpts

optParser :: Parser Options
optParser = subparser (
    command "greet" (info (CmdGreet <$> sample) (progDesc "Print greeting 1")) <>
    command "groot" (info (CmdGroot <$> sample) (progDesc "Print greeting 2"))
    )

runCmd :: Options -> IO ()
runCmd o = case o of
    CmdGreet opts -> greet opts
    CmdGroot opts -> groot opts

greet :: GreetArgs -> IO ()
greet (GreetArgs h) = putStrLn $ "Hello, " ++ h ++ "!"

groot :: GreetArgs -> IO ()
groot (GreetArgs h) = putStrLn $ "Howdy, " ++ h ++ "!"

sample :: Parser GreetArgs
sample = GreetArgs <$> strArgument ( metavar "TARGET" )

You can run this with ./opt_ex.hs greet John to get Hello, John! and with ./opt_ex.hs groot John to get Howdy, John!. Running ./opt_ex.hs will give you the following overview:

Usage: opt_ex.hs COMMAND

Available commands:
  greet                    Print greeting 1
  groot                    Print greeting 2

What would be the most elegant way, to add a command gruut in this example, which behaves exactly like greet, but produces the least amount of overhead, both in the code and for the user?

Ideally I would like ./opt_ex.hs to yield something like this:

Usage: opt_ex.hs COMMAND

Available commands:
  greet|gruut              Print greeting 1
  groot                    Print greeting 2

Solution

  • I don't think you can do this. It works fine for options, because the definition of OptField contains a list of OptName, and adds to that list when you use (<>). But the definition of CommandFields, the thing returned by command, is

    data CommandFields a = CommandFields
      { cmdCommands :: [(String, ParserInfo a)]
      , cmdGroup :: Maybe String }
    

    Each String name is thus associated with a different ParserInfo. Of course, you can define a variable containing any ParserInfo you like, and reuse it across two commands, so you won't have to repeat the ParserInfo. But as far as optparse-applicative is concerned, those two commands are distinct, so it will list them separately in the help text. For your example, this would look like

    optParser = let greeting1 = info (CmdGreet <$> sample) (progDesc "Print greeting 1")
                in subparser $
                   command "greet" greeting1 <>
                   command "gruut" greeting1 <>
                   command "groot" (info (CmdGroot <$> sample) (progDesc "Print greeting 2"))
    

    and indeed, when run we see both commands listed:

    Usage: optparse COMMAND
    
    Available commands:
      greet                    Print greeting 1
      gruut                    Print greeting 1
      groot                    Print greeting 2