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

oro*_*ome 7 haskell command-line-interface optparse-applicative

在我使用的Haskell可执行文件中,optparse-applicative我希望有一个全局选项,可以与所有子命令中提供--version的全局--help选项一起使用.但是,提供示例(见下文)用于向--version具有子命令的CLI 添加选项会导致--version选项不一致

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

Usage: cli create NAME
  Create a thing

$ cli delete --version
0.0
Run Code Online (Sandbox Code Playgroud)

并且永远不会出现在子命令的帮助中

$ 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
Run Code Online (Sandbox Code Playgroud)

我想要的行为是--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
Run Code Online (Sandbox Code Playgroud)

从文档中不清楚如何实现这一点.

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

$ 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
Run Code Online (Sandbox Code Playgroud)

有没有办法实现这个目的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"))
Run Code Online (Sandbox Code Playgroud)

Pet*_*don 6

据我所知,这(特别是分类的帮助文本)并不是很容易处理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
Run Code Online (Sandbox Code Playgroud)

(注意:我建议采用这种方法,因为“命令之前的全局选项”是相当标准的)。

如果您还希望在每个子命令中都可以使用全局选项,那么您将遇到一些问题。

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

对于#2,重构示例以支持这一点的一种方法可能是这样的:

首先,标准样板和导入:

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

import Data.Monoid
import Data.Semigroup ((<>))
import Options.Applicative
import Options.Applicative.Types
Run Code Online (Sandbox Code Playgroud)

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

data Opts = Opts
    { optGlobals :: !GlobalOpts 
    , optCommand :: !Command
    }
data GlobalOpts = GlobalOpts { optGlobalFlag :: Bool }
Run Code Online (Sandbox Code Playgroud)

GlobalOpts应该是 aSemigroup和 a 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
Run Code Online (Sandbox Code Playgroud)

和以前一样,一种Command代表不同可能命令的类型:

data Command
    = Create String
    | Delete
Run Code Online (Sandbox Code Playgroud)

真正的魔法:mysubparser包装hsubparser以添加全局选项并处理合并它们。它将全局选项的解析器作为参数:

mysubparser :: forall a b. Monoid a
            => Parser a
            -> Mod CommandFields b
            -> Parser (a, b)
mysubparser globals cmds = do
Run Code Online (Sandbox Code Playgroud)

首先,它运行全局解析器(以捕获命令之前给出的任何全局变量):

  g1 <- globals
Run Code Online (Sandbox Code Playgroud)

然后它hsubparser用于获取命令解析器,并对其进行修改以解析全局选项:

  (g2, r) <- addGlobals $ hsubparser cmds
Run Code Online (Sandbox Code Playgroud)

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

  pure (g1 <> g2, r)
  where 
Run Code Online (Sandbox Code Playgroud)

addGlobals辅助函数:

        addGlobals :: forall c. Parser c -> Parser (a, c)
Run Code Online (Sandbox Code Playgroud)

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

        addGlobals (NilP x) = NilP $ (mempty,) <$> x
Run Code Online (Sandbox Code Playgroud)

重要的情况:如果我们有一个OptP围绕Option它使用CommandReaderglobals解析器被添加到每一个命令解释:

        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 }
Run Code Online (Sandbox Code Playgroud)

在所有其他情况下,要么仅使用默认选项集,要么根据需要合并来自 recursiveParser的选项集:

        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')
Run Code Online (Sandbox Code Playgroud)

对该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"))
Run Code Online (Sandbox Code Playgroud)

请注意,它mysubparser应该是一个非常通用/可重用的组件。

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

$ ./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
Run Code Online (Sandbox Code Playgroud)