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

时间:2018-12-13 20:05:36

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

在我的使用optparse-applicative创建的Haskell可执行文件中,我想拥有--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"))

1 个答案:

答案 0 :(得分:3)

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

重要的情况:如果我们在使用OptP的{​​{1}}周围有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 } 合并选项集:

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