我正在 Haskell 中开发一个小项目,目前我正在尝试通过添加使用可选 cli 标志的可能性来使其更具可扩展性。
所需的界面如下所示:
program: [--command1 {n} ] [--help]
其中
command1
和 help
是可选参数,此外,command1
还可以采用整数参数 n
。
使用模式匹配,我得到了以下结果
main :: IO ()
main = do
args <- getArgs
case args of
[] -> command1'
["--command1", a] -> if isJust (readMaybe n::Maybe Int)
then command1 a-- handle proper integer
else -- call command1' but also show a warning message
["--command1"] -> command1'
["--help"] -> putStrLn showWarningMessage
_ -> putStrLn (showErrorMessage args) -- show an error message
由于程序不依赖于某个特定参数,因此即使 cli 参数无效,它也应该能够运行。
我不确定如何处理以下情况:
command1'
?--command2
等参数,但如果参数的形式为 args=[--command2, --command1, n]
或 args=[--command1, --command2]
该怎么办。在这两种情况下,理想情况下都会打印一条关于 command2
未知的警告,但 cli 应接受 command1。注意:我看到有专门设计来处理这些问题的库,但我有兴趣解决这些简单的案例,以便更好地学习 Haskell。当然,可能还有更多的边缘情况,但就我的使用而言,提到的两个是我唯一关心的。
不是专家,但这是我的答案:
1. 对您有用的是
do
中的简单 else
表示法,并为调用 command1
提供默认值,因为我假设您的函数需要一个 Integer 来执行某些操作。最终结果会是这样的:
main :: IO ()
main = do
args <- getArgs
case args of
[] -> command1'
["--command1", a] -> if isJust (readMaybe n::Maybe Int)
then command1 a
-- here what I think you want to do
else do
-- here your warning message
putStrLn (showWarningMessage args)
-- here let's say 10 is the default value
command1 10
["--command1"] -> command1'
["--help"] -> putStrLn showWarningMessage
_ -> putStrLn (showErrorMessage args) -- show an error message
我发现在http://www.happylearnhaskelltutorial.com/1/times_table_train_of_terror.html#s18.4中你可以从头开始,它很好地解释了Haskell的概念。
2.我明白你想要做什么,但是命令行提示的目的不是快速失败吗?请记住,您必须关注默认值是什么,并且这样做,您的工具的默认行为应该是什么。也许您可以想到参数的不同实现。这是我从命令式语言学习几个月的 Haskell 中学到的东西。
无论如何,我有一些关于如何做到这一点的提示。如果添加
command3
会怎么样?您描述所有可能的用例并手动管理所有内容的方法很容易导致疏忽和错误。在不使用外部库的情况下,可以通过使用Haskell的内置解析器System.Console.GetOpt来避免这种管理。即使文档有点旧,他们也解释了一些关于标志和解析的概念。但是代码,即使不完美,也可以完美地处理标志/参数的每种组合,并捕获 4 个不同参数的所有错误。这是一些评论:
import Control.Monad (foldM)
import System.Console.GetOpt
import System.Environment ( getArgs, getProgName )
data Options = Options {
optVerbose :: Bool
, optShowVersion :: Bool
, optNbOfIterations :: Int
, optSomeString :: String
} deriving Show
defaultOptions = Options {
optVerbose = False
, optShowVersion = False
, optNbOfIterations = 1
, optSomeString = "a random string"
}
options :: [OptDescr (Options -> Either String Options)]
options =
[ Option ['v'] ["verbose"]
(NoArg (\ opts -> Right opts { optVerbose = True }))
"chatty output on stderr"
, Option ['V','?'] ["version"]
(NoArg (\ opts -> Right opts { optShowVersion = True })) -- here no need for a value for this option because of `NoArg`
"show version number"
, Option ['i'] ["iterations"]
(ReqArg (\ i opts -> -- here the value of the param is required `ReqArg`, but you can also have `OptArg` which makes the value optional
case reads i of
[(iterations, "")] | iterations >= 1 && iterations <= 20 -> Right opts { optNbOfIterations = iterations } -- here your conditions
_ -> Left "--iterations must be a number between 1 and 20" -- error message in case previous conditions are not met
) "ITERATIONS") -- here the name of the param in the cli help
"Number of times you want to repeat the reversed string" -- here the description of param's usage in the cli help
, Option ['s'] ["string"]
(ReqArg (\arg opts -> Right opts { optSomeString = return arg !! 0 } -- really dirty, sorry but could not make it work another way
"Outputs the string reversed"
]
parseArgs :: IO Options
parseArgs = do
argv <- getArgs
progName <- getProgName
let header = "Usage: " ++ progName ++ " [OPTION...]"
let helpMessage = usageInfo header options
case getOpt RequireOrder options argv of
(opts, [], []) ->
case foldM (flip id) defaultOptions opts of
Right opts -> return opts
Left errorMessage -> ioError (userError (errorMessage ++ "\n" ++ helpMessage))
(_, _, errs) -> ioError (userError (concat errs ++ helpMessage))
main :: IO ()
main = do
options <- parseArgs
if isVerbose options
then mapM_ putStrLn ["verbose mode activated !","You will have a lot of logs","With a lot of messages","And so on ..."]
else return () -- do nothing
mapM_ print (repeatReversedString options)
if getVersion options
then print "version : 0.1.0-prealpha"
else return ()
return ()
-- With those functions we extract the option setting from the data type Options (and maybe we play with it)
isVerbose :: Options -> Bool
isVerbose (Options optVerbose _ _ _) = optVerbose
getVersion :: Options -> Bool
getVersion (Options _ optShowVersion _ _) = optShowVersion
repeatReversedString :: Options -> [String]
repeatReversedString (Options _ _ optNbOfIterations optSomeString) =
replicate optNbOfIterations (reverse optSomeString)
getString :: Options -> String
getString (Options _ _ _ optSomeString) = optSomeString
我认为你可以通过使用 OptArg (而不是我使用的 NoArg 和 ReqArg)来实现你的目标,它有这样的定义:
OptArg (Maybe String -> a) String
这是 hackage 的链接:
https://hackage.haskell.org/package/base-4.14.1.0/docs/System-Console-GetOpt.html
请记住,Haskell 与其他常见语言有很大不同,例如没有 for 循环!