通过模式匹配创建一个简单的 cli 参数解析器

问题描述 投票:0回答:1

我正在 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 参数无效,它也应该能够运行。

我不确定如何处理以下情况:

  1. 如何处理当 a 不是整数时的情况,因此我们想警告用户但也调用
    command1'
  2. 添加错误参数时该怎么办。模式匹配是详尽的,因此将捕获
    --command2
    等参数,但如果参数的形式为
    args=[--command2, --command1, n]
    args=[--command1, --command2]
    该怎么办。在这两种情况下,理想情况下都会打印一条关于
    command2
    未知的警告,但 cli 应接受 command1。

注意:我看到有专门设计来处理这些问题的库,但我有兴趣解决这些简单的案例,以便更好地学习 Haskell。当然,可能还有更多的边缘情况,但就我的使用而言,提到的两个是我唯一关心的。

haskell command-line-interface
1个回答
0
投票

不是专家,但这是我的答案:

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来避免这种管理。即使文档有点旧,他们也解释了一些关于标志和解析的概念。
我还在 Stack Overflow 上找到了这篇文章,其中展示了一个使用 GetOpt 的工作示例 https://stackoverflow.com/a/10816382/6270743。基于此,我无法显示错误消息并执行您尝试执行的命令。

但是代码,即使不完美,也可以完美地处理标志/参数的每种组合,并捕获 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 循环!

© www.soinside.com 2019 - 2024. All rights reserved.