代码之家  ›  专栏  ›  技术社区  ›  orome ErikR

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

  •  6
  • orome ErikR  · 技术社区  · 6 年前

    在我的haskell可执行文件中,创建时使用 optparse-applicative ,我想有一个全局选项 --version 在全球范围内 --help 可从所有子命令中获得的选项。然而 example provided (见下文)用于添加 -版本 带有子命令的CLI选项会导致 -版本 不一致可用的选项

    $ 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
    

    我想要的行为是 -版本 在全球范围内和所有子命令都可用:

    $ 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应用 ?


    {-#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"))
    
    1 回复  |  直到 6 年前
        1
  •  3
  •   Lucy Maya Menon    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
    

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

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

    1. 据我所知,没有办法影响帮助文本输出,以便在单个命令帮助文本中对它们进行单独分组。
    2. 你需要一些习惯 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 和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
    

    像以前一样 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
    

    然后使用 子分析器 要获取命令分析器,并将其修改为同时分析全局选项:

      (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 使用A CommandReader , the 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 功能相当小,主要与使用新的 全球橄榄球队 . 曾经是的解析器 全球橄榄球队 有空,传给 MySub分析器 很简单:

    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"))
    

    注意到 MySub分析器 应该是一个非常通用/可重用的组件。

    这种行为更接近于你想要的:

    $ ./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
    
    推荐文章