如何使用 'optparse-applicative' 创建和区分全局选项?

How do I create, and distinguish, global options using 'optparse-applicative'?

在我的 Haskell 可执行文件中,使用 optparse-applicative 创建,我希望有一个 --version 的全局选项以及所有可用的全局 --help 选项子命令。然而,example provided(见下文)使用子命令向 CLI 添加 --version 选项会导致 --version 选项不一致可用

$ cli create --version
Invalid option `--version'

Usage: cli create NAME
  Create a thing

$ cli delete --version
0.0

并且从不出现在子命令的帮助中

$ 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

我想要的行为是 --version 全局可用并且对所有子命令可用:

$ 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

文档中并不清楚如何实现这一点。

事实上,我非常希望能够在帮助输出中清楚地对选项进行分组:

$ 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

有没有办法使用 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"))

据我所知,这(特别是分类帮助文本)使用 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

(注意:我建议采用这种方法,因为 "global options before the command" 是相当标准的)。

如果您还希望全局选项在每个子命令中可用,则会遇到一些问题。

  1. 据我所知,没有办法影响帮助文本输出以便将它们单独分组在各个命令帮助文本中。
  2. 您将需要一些类似于 subparser 的自定义函数来添加您的全局选项并将它们与命令之前的任何全局选项合并。

对于 #2,一种重构示例以支持这一点的方法可能是沿着这些思路进行的:

首先,标准样板和导入:

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ApplicativeDo #-}

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

Opts 明确分为 optGlobalsoptCommand,如果有更多可用选项,可以轻松地同时处理所有全局选项:

data Opts = Opts
    { optGlobals :: !GlobalOpts 
    , optCommand :: !Command
    }
data GlobalOpts = GlobalOpts { optGlobalFlag :: Bool }

GlobalOpts 应该是 SemigroupMonoid,因为我们需要合并在各个不同点(命令之前、命令之后等)看到的选项。也应该可以通过对下面的 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

然后使用hsubparser得到一个命令解析器,并修改它来解析全局选项:

  (g2, r) <- addGlobals $ hsubparser cmds

最后,它合并了两个全局选项集,returns解析的全局选项和命令解析器结果:

  pure (g1 <> g2, r)
  where 

addGlobals 辅助函数:

        addGlobals :: forall c. Parser c -> Parser (a, c)

如果给出NilP,我们只使用mempty来获取默认选项集:

        addGlobals (NilP x) = NilP $ (mempty,) <$> x

重要的情况:如果我们在使用 CommandReaderOption 周围有一个 OptP,则 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 }

在所有其他情况下,要么只使用默认选项集,要么根据需要合并递归 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 应该是一个相当 generic/reusable 的组件。

这表现出更接近您想要的行为:

$ ./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