3

使用 optparse-applicative,我想要一个可选参数,它应该是文件的路径,或者如果没有指定,则为stdin. 这里明显的选择是使这个参数类型IO Handle和当一个参数被传递时使用openFile。这是我目前拥有的:

module Main where

import Data.Semigroup ((<>))
import Options.Applicative
import System.IO

data Args = Args { input :: IO Handle }

parseArgs = Args <$> argument parseReadHandle (value defaultHandle)
  where defaultHandle = return stdin :: IO Handle

parseReadHandle :: ReadM (IO Handle)
parseReadHandle = eitherReader $ \path -> Right $ openFile path ReadMode

getArgs :: IO Args
getArgs = execParser $ info (parseArgs <**> helper) fullDesc

main :: IO ()
main = run =<< getArgs

run :: Args -> IO ()
run (Args input) = putStrLn =<< hGetContents =<< input

这样做的问题是,我们没有正确地处理handle异常openFile,而是依赖于未处理异常的默认行为(打印错误并退出)。这看起来很恶心。

我认为更合适的方法是LeftopenFile. 问题是,eitherReader期望 aString -> Either String a所以我们不能做这样的事情:

{-# LANGUAGE ScopedTypeVariables #-}
import Control.Exception

parseReadHandle :: ReadM (IO Handle)
parseReadHandle = eitherReader tryOpenFile

tryOpenFile :: IO (Either String (IO Handle)) -> FilePath
tryOpenFile path = do
  handle (\(e :: IOException) -> return $ Left $ show e) $ do
    return $ Right $ openFile path ReadMode

当然,你可以从类型中tryOpenFile看出 this 不会进行类型检查。我不确定我所要求的是否可行,因为似乎错误消息必须是IO String,因为要获得错误,必须执行 IO 计算。所以至少看起来你需要eitherReader采取 aString -> IO (Either String a)或 a String -> Either (IO String) (IO Handle)。根据我对它们的基本理解,听起来可以在这里使用 monad 转换器来包装 ReadM(或相反?)。但这比我的理解要深一些,我不知道如何先行。

有没有办法在handleoptparse IOException-applicative 中完成ReadM

4

1 回答 1

4

我相信您的方法有些误导。

你说:"I'd like to have an optional argument, which should be a path to a file..."

好的,那么类似的东西怎么样Maybe FilePath?听起来这可能是您想要的。或等效的 ADT:

data Path = StandardInput | Path FilePath

当你说的时候,"The obvious choice here is to make this argument type IO Handle and when an argument is passed in use openFile" 你是在超越自己。

从命令行解析应该是将要解析的输入转换为适合将来在程序中使用的数据。不要担心在这个阶段打开文件,或者在文件不存在时处理异常,或者你将要使用这些数据的任何其他方式......只要担心这个问题,我的程序的用户给我一个文件路径与否?也就是说,我有什么数据?其他的东西不是(也不应该是)optparse-applicative的工作。

所以只需为这个数据类型构建你的解析器Path。它可能由每个构造函数的解析器组成。例如:

stdInputParser :: Parser Path
stdInputParser = ...

pathSuppliedParser :: Parser Path
pathSuppliedParser = ...

pathParser :: Parser Path
pathParser = pathSuppliedParser <|> stdInputParser

无论如何,一旦你运行execParser,你就会留下你的Path数据类型。所以你将把它作为参数传递给你的run函数:

run :: Path -> IO ()
run StandardInput = ... use stdin here
run (Path filePath) = ... use openFile here, catch and handle exceptions if the file doesn't exist, etc.
于 2017-07-03T03:39:10.943 回答