In my Haskell executable, created using optparse-applicative
, I would like to have a global option for --version
alongside the global --help
option that is available from all subcommands. However the example provided (see below) for adding a --version
option to to a CLI with subcommands results in a --version
option that is inconsistently available
$ cli create --version
Invalid option `--version'
Usage: cli create NAME
Create a thing
$ cli delete --version
0.0
and never shows up in help for subcommands
$ cli create -h
Usage: cli create NAME
Create a thing
Available options:
NAME Name of the thing to create
-h,--help Show this help text
$ cli delete -h
Usage: cli delete
Delete the thing
Available options:
-h,--help Show this help text
The behavior I would like is for --version
to be available globally and to all subcommands:
$ cli create -h
Usage: cli create NAME
Create a thing
Available options:
NAME Name of the thing to create
--version Show version
-h,--help Show this help text
$ cli delete -h
Usage: cli delete
Delete the thing
Available options:
--version Show version
-h,--help Show this help text
$ cli create --version
0.0
$ cli delete --version
0.0
It's not clear from the documentation how to achieve this.
In fact, I'd ideally like to be able to clearly group options in the help output:
$ cli create -h
Usage: cli create NAME
Create a thing
Arguments:
NAME Name of the thing to create
Global options:
--version Show version
-h,--help Show this help text
$ cli delete -h
Usage: cli delete
Delete the thing
Global options:
--version Show version
-h,--help Show this help text
Is there a way to achieve this using optparse-applicative
?
{-#LANGUAGE ScopedTypeVariables#-}
import Data.Semigroup ((<>))
import Options.Applicative
data Opts = Opts
{ optGlobalFlag :: !Bool
, optCommand :: !Command
}
data Command
= Create String
| Delete
main :: IO ()
main = do
(opts :: Opts) <- execParser optsParser
case optCommand opts of
Create name -> putStrLn ("Created the thing named " ++ name)
Delete -> putStrLn "Deleted the thing!"
putStrLn ("global flag: " ++ show (optGlobalFlag opts))
where
optsParser :: ParserInfo Opts
optsParser =
info
(helper <*> versionOption <*> programOptions)
(fullDesc <> progDesc "optparse subcommands example" <>
header
"optparse-sub-example - a small example program for optparse-applicative with subcommands")
versionOption :: Parser (a -> a)
versionOption = infoOption "0.0" (long "version" <> help "Show version")
programOptions :: Parser Opts
programOptions =
Opts <$> switch (long "global-flag" <> help "Set a global flag") <*>
hsubparser (createCommand <> deleteCommand)
createCommand :: Mod CommandFields Command
createCommand =
command
"create"
(info createOptions (progDesc "Create a thing"))
createOptions :: Parser Command
createOptions =
Create <$>
strArgument (metavar "NAME" <> help "Name of the thing to create")
deleteCommand :: Mod CommandFields Command
deleteCommand =
command
"delete"
(info (pure Delete) (progDesc "Delete the thing"))
As far as I know, this (in particular, the categorized help text) isn't really easy to do with
optparse-applicative
, since it isn't quite the pattern that they were planning for with global arguments. If you are okay with usingprogram --global-options command --local-options
(which is a fairly standard pattern) instead ofprogram command --global-and-local-options
, then you can use the approach shown in the linked example:(Note: I would advise going with this approach, since "global options before the command" is fairly standard).
If you also want the global options to be available in every subcommand, you will have a few issues.
subparser
-like function that adds your global options & merges them with any global options before the command.For #2, one way to restructure the example to support this might be something along these lines:
To start with, standard boilerplate and imports:
Opts
are explicitly split intooptGlobals
andoptCommand
, making it easy to deal with all of the global options at once if more are available:GlobalOpts
should be aSemigroup
and aMonoid
, since we need to merge options seen at various different points (before the command, after the command, etc.). It should also be possible, with suitable alterations tomysubparser
below, to require global options to be given only after commands and omit this requirement.As before, a
Command
type to represent the different possible commands:The real magic:
mysubparser
wrapshsubparser
to add global options and deal with merging them. It takes the parser for global options as an argument:To start with, it runs the global parser (to catch any globals given before a command):
It then uses
hsubparser
to get a command parser, and modifies it to also parse global options:Finally, it merges the two global option sets, and returns the parsed global options and the command parser result:
The
addGlobals
helper function:If
NilP
was given, we just usemempty
to get the default option set:The important case: if we have an
OptP
around anOption
that uses aCommandReader
, theglobals
parser is added to every command parser:In all other cases, either just use the default option set, or merge option sets from recursive
Parser
s as appropriate:Modifications to the
main
function are fairly minimal, and mostly related to using the newGlobalOpts
. Once a parser forGlobalOpts
is available, passing it tomysubparser
is quite easy:Notice that
mysubparser
should be a quite generic/reusable component.This exhibits behavior closer to what you wanted: