Насколько я знаю, это (в частности, текст справки по категориям) не очень легко сделать с optparse-applicative
, поскольку это не совсем тот шаблон, который они планировали с глобальными аргументами.Если вы согласны с использованием program --global-options command --local-options
(что является довольно стандартным шаблоном) вместо program command --global-and-local-options
, тогда вы можете использовать подход, показанный в связанном примере:
$ ./optparse-sub-example
optparse-sub-example - a small example program for optparse-applicative with
subcommands
Usage: optparse [--version] [--global-flag] COMMAND
optparse subcommands example
Available options:
-h,--help Show this help text
--version Show version
--global-flag Set a global flag
Available commands:
create Create a thing
delete Delete the thing
$ ./optparse-sub-example --version create
0.0
$ ./optparse-sub-example --version delete
0.0
$ ./optparse-sub-example --global-flag create HI
Created the thing named HI
global flag: True
$ ./optparse-sub-example --global-flag delete
Deleted the thing!
global flag: True
(Примечание: я бы посоветовалпридерживаясь этого подхода, поскольку «глобальные параметры перед командой» довольно стандартны).
Если вы также хотите, чтобы глобальные параметры были доступны в каждой подкоманде, у вас возникнет несколько проблем.
- Насколько я знаю, не существует способа повлиять на вывод текста справки, чтобы сгруппировать их отдельно в текстах справки отдельной команды.
- Вам понадобится некоторая пользовательская
subparser
-подобная функцияэто добавляет ваши глобальные параметры и объединяет их с любыми глобальными параметрами перед командой.
Для # 2, один из способов реструктурировать пример для поддержки этого может быть что-то вроде этого:
Для начала, стандартный шаблон и импорт:
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ApplicativeDo #-}
import Data.Monoid
import Data.Semigroup ((<>))
import Options.Applicative
import Options.Applicative.Types
Opts
явно разделены на optGlobals
и optCommand
, что упрощает работу со всеми глобальными опциямисразу, если доступно больше:
data Opts = Opts
{ optGlobals :: !GlobalOpts
, optCommand :: !Command
}
data GlobalOpts = GlobalOpts { optGlobalFlag :: Bool }
GlobalOpts
должно быть Semigroup
и Monoid
, поскольку нам необходимо объединить параметры, видимые в различных точках (до команды, после команды, так далее.).Также должно быть возможно, с соответствующими изменениями mysubparser
ниже, требовать, чтобы глобальные опции давались только после команд, и пропустить это требование.
instance Semigroup GlobalOpts where
-- Code for merging option parser results from the multiple parsers run
-- at various different places. Note that this may be run with the default
-- values returned by one parser (from a location with no options present)
-- and the true option values from another, so it may be important
-- to distinguish between "the default value" and "no option" (since "no
-- option" shouldn't override another value provided earlier, while
-- "user-supplied value that happens to match the default" probably should).
--
-- In this case this doesn't matter, since the flag being provided anywhere
-- should be enough for it to be considered true.
(GlobalOpts f1) <> (GlobalOpts f2) = GlobalOpts (f1 || f2)
instance Monoid GlobalOpts where
-- Default values for the various options. These should probably match the
-- defaults used in the option declarations.
mempty = GlobalOpts False
Как и прежде, тип Command
для представленияразличные возможные команды:
data Command
= Create String
| Delete
Настоящая магия: mysubparser
обертывания hsubparser
для добавления глобальных опций и решения их слияния.В качестве аргумента он использует синтаксический анализатор глобальных опций:
mysubparser :: forall a b. Monoid a
=> Parser a
-> Mod CommandFields b
-> Parser (a, b)
mysubparser globals cmds = do
Для начала он запускает глобальный синтаксический анализатор (чтобы перехватить любые глобальные переменные, заданные перед командой):
g1 <- globals
Itзатем использует hsubparser
для получения синтаксического анализатора команд и модифицирует его для анализа глобальных параметров:
(g2, r) <- addGlobals $ hsubparser cmds
Наконец, он объединяет два набора глобальных параметров и возвращает проанализированные глобальные параметры и результат синтаксического анализатора команды.:
pure (g1 <> g2, r)
where
Вспомогательная функция addGlobals
:
addGlobals :: forall c. Parser c -> Parser (a, c)
Если задано NilP
, мы просто используем mempty
, чтобы получить набор параметров по умолчанию:
addGlobals (NilP x) = NilP $ (mempty,) <$> x
Важный случай: если у нас есть OptP
вокруг Option
, который использует CommandReader
, синтаксический анализатор globals
добавляется к каждому анализатору команд:
addGlobals (OptP (Option (CmdReader n cs g) ps)) =
OptP (Option (CmdReader n cs $ fmap go . g) ps)
where go pi = pi { infoParser = (,) <$> globals <*> infoParser pi }
Inво всех остальных случаях либо просто используйте набор параметров по умолчанию, либо используйте наборы параметров слияния из рекурсивных Parser
с в зависимости от ситуации:
addGlobals (OptP o) = OptP ((mempty,) <$> o)
addGlobals (AltP p1 p2) = AltP (addGlobals p1) (addGlobals p2)
addGlobals (MultP p1 p2) =
MultP ((\(g2, f) -> \(g1, x) -> (g1 <> g2, f x)) <$> addGlobals p1)
(addGlobals p2)
addGlobals (BindP p k) = BindP (addGlobals p) $ \(g1, x) ->
BindP (addGlobals $ k x) $ \(g2, x') ->
pure (g1 <> g2, x')
Модификации функции main
довольно минимальны и в основном связаны с использованиемновый GlobalOpts
.Как только парсер для GlobalOpts
станет доступен, передать его на mysubparser
довольно просто:
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 (optGlobals opts)))
where
optsParser :: ParserInfo Opts
optsParser =
info
(helper <*> 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")
globalOpts :: Parser GlobalOpts
globalOpts = versionOption <*>
(GlobalOpts <$> switch (long "global-flag" <> help "Set a global flag"))
programOptions :: Parser Opts
programOptions =
uncurry Opts <$> mysubparser globalOpts (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"))
Обратите внимание, что mysubparser
должен быть довольно универсальным / повторно используемым компонентом.
Thisдемонстрирует поведение ближе к тому, что вы хотели:
$ ./optparse-sub-example create --global-flag HI
Created the thing named HI
global flag: True
$ ./optparse-sub-example --global-flag create HI
Created the thing named HI
global flag: True
$ ./optparse-sub-example --global-flag delete
Deleted the thing!
global flag: True
$ ./optparse-sub-example delete --global-flag
Deleted the thing!
global flag: True
$ ./optparse-sub-example delete
Deleted the thing!
global flag: False
$ ./optparse-sub-example delete --version
0.0
$ ./optparse-sub-example create --version
0.0