如何使用 '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" 是相当标准的)。
如果您还希望全局选项在每个子命令中可用,则会遇到一些问题。
- 据我所知,没有办法影响帮助文本输出以便将它们单独分组在各个命令帮助文本中。
- 您将需要一些类似于
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
和 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
然后使用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
重要的情况:如果我们在使用 CommandReader
的 Option
周围有一个 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
在我的 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" 是相当标准的)。
如果您还希望全局选项在每个子命令中可用,则会遇到一些问题。
- 据我所知,没有办法影响帮助文本输出以便将它们单独分组在各个命令帮助文本中。
- 您将需要一些类似于
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
和 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
然后使用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
重要的情况:如果我们在使用 CommandReader
的 Option
周围有一个 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