【问题标题】:How do I create, and distinguish, global options using 'optparse-applicative'?如何使用“optparse-applicative”创建和区分全局选项?
【发布时间】:2018-12-21 05:55:04
【问题描述】:

在我使用optparse-applicative 创建的Haskell 可执行文件中,我希望有一个--version 的全局选项以及可从所有子命令获得的全局--help 选项。然而,example provided(见下文)用于将 --version 选项添加到带有子命令的 CLI 会导致 --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"))

【问题讨论】:

    标签: haskell command-line-interface optparse-applicative


    【解决方案1】:

    据我所知,使用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
    

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

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

    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
    

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

      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 }
    

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

            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 应该是一个非常通用/可重用的组件。

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

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

    【讨论】:

    • 呃,看来这不太可能。这很奇怪,因为提供的包(在链接的示例中)不一致。全局 --help 选项的行为方式与我正在寻找的方式相同,但全局 --version 不能真正以相同的方式工作。
    • 更糟糕的是,子命令的帮助并未将 --version 列为合法(全局)选项,即使它是。
    • @orome - --help 是按照我展示的方式实现的(hsubparser 中的h 是因为它添加了--help)。 --help 的处理方式与其他全局选项略有不同,因为它确实具有特定于命令的含义。我没有在最后的例子中展示它,但是如果你使用我提供的mysubparser--version确实会出现在子命令帮助中。
    • 所以听起来,如果没有大量的定制工作(你在这里做的),这是不可能的。
    • 我认为这可能是真的。也就是说,这里的mysubparser 应该是相当可重用的(并且不是非常复杂/难以适应),所以如果你真的需要这个功能来做某事,它可能是有用的/可能的途径。跨度>
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多