如何在Haskell中实现`cat`?

时间:2012-07-13 17:00:39

标签: haskell conduit

我正在尝试在Haskell中编写一个简单的cat程序。我想将多个文件名作为参数,并将每个文件按顺序写入 STDOUT ,但我的程序只打印一个文件并退出。

我需要做什么才能让我的代码打印每个文件,而不仅仅是第一个传入的文件?

import Control.Monad as Monad
import System.Exit
import System.IO as IO
import System.Environment as Env

main :: IO ()
main = do
    -- Get the command line arguments
    args <- Env.getArgs

    -- If we have arguments, read them as files and output them
    if (length args > 0) then catFileArray args

    -- Otherwise, output stdin to stdout
    else catHandle stdin

catFileArray :: [FilePath] -> IO ()
catFileArray files = do
    putStrLn $ "==> Number of files: " ++ (show $ length files)
    -- run `catFile` for each file passed in
    Monad.forM_ files catFile

catFile :: FilePath -> IO ()
catFile f = do
    putStrLn ("==> " ++ f)
    handle <- openFile f ReadMode
    catHandle handle

catHandle :: Handle -> IO ()
catHandle h = Monad.forever $ do
    eof <- IO.hIsEOF h
    if eof then do
        hClose h
        exitWith ExitSuccess
    else
        hGetLine h >>= putStrLn

我正在运行这样的代码:

runghc cat.hs file1 file2

4 个答案:

答案 0 :(得分:18)

您的问题是exitWith终止整个程序。因此,您无法真正使用forever来遍历文件,因为很明显您不希望“永远”运行该函数,直到文件结束。您可以像这样重写catHandle

catHandle :: Handle -> IO ()
catHandle h = do
    eof <- IO.hIsEOF h
    if eof then do
        hClose h
     else
        hGetLine h >>= putStrLn
        catHandle h

即。如果我们没有达到EOF,我们会递归并读另一条线。

然而,整个方法过于复杂。你可以简单地把猫写成

main = do
    files <- getArgs
    forM_ files $ \filename -> do
        contents <- readFile filename
        putStr contents

由于懒惰的i / o,整个文件内容实际上并未加载到内存中,而是流入stdout。

如果您对Control.Monad的操作员感到满意,整个程序可以缩短为

main = getArgs >>= mapM_ (readFile >=> putStr)

答案 1 :(得分:17)

如果你安装了非常有帮助的conduit package,你可以这样做:

module Main where

import Control.Monad
import Data.Conduit
import Data.Conduit.Binary
import System.Environment
import System.IO

main :: IO ()
main = do files <- getArgs
          forM_ files $ \filename -> do
            runResourceT $ sourceFile filename $$ sinkHandle stdout

这与shang建议的简单解决方案类似,但使用管道和ByteString代替惰性I / O和String。这两个都是学习要避免的好事:懒惰的I / O在不可预测的时间释放资源; String有很多内存开销。

请注意,ByteString旨在表示二进制数据,而不是文本。在这种情况下,我们只是将文件视为未解释的字节序列,因此可以使用ByteString。如果OTOH我们将文件处理为 text - 计算字符,解析等 - 我们想要使用Data.Text

编辑:您也可以这样写:

main :: IO ()
main = getArgs >>= catFiles

type Filename = String

catFiles :: [Filename] -> IO ()
catFiles files = runResourceT $ mapM_ sourceFile files $$ sinkHandle stdout

在原文中,sourceFile filename创建了一个从命名文件中读取的Source;我们在外部使用forM_来遍历每个参数,并对每个文件名运行ResourceT计算。

但是在Conduit中,您可以使用monadic >>来连接源代码; source1 >> source2是一个生成source1元素的源,直到它完成,然后生成source2的元素。因此,在第二个示例中,mapM_ sourceFile files相当于sourceFile file0 >> ... >> sourceFile filen - 一个Source,它连接了所有来源。

编辑2:并遵循Dan Burton对此答案的评论中的建议:

module Main where

import Control.Monad
import Control.Monad.IO.Class
import Data.ByteString
import Data.Conduit
import Data.Conduit.Binary
import System.Environment
import System.IO

main :: IO ()
main = runResourceT $ sourceArgs $= readFileConduit $$ sinkHandle stdout

-- | A Source that generates the result of getArgs.
sourceArgs :: MonadIO m => Source m String
sourceArgs = do args <- liftIO getArgs
                forM_ args yield

type Filename = String          

-- | A Conduit that takes filenames as input and produces the concatenated 
-- file contents as output.
readFileConduit :: MonadResource m => Conduit Filename m ByteString
readFileConduit = awaitForever sourceFile

在英语中,sourceArgs $= readFileConduit是一个生成命令行参数命名的文件内容的源。

答案 2 :(得分:5)

catHandle间接调用的

catFileArray在到达第一个文件末尾时调用exitWith。这将终止程序,并且不再读取其他文件。

当您到达文件末尾时,您应该从catHandle函数正常返回。这可能意味着您不应该阅读forever

答案 3 :(得分:4)

我的第一个想法是:

import System.Environment
import System.IO
import Control.Monad
main = getArgs >>= mapM_ (\name -> readFile name >>= putStr)

它并没有真正以unix-y方式失败,并且不会执行stdin或多字节的东西,但它是“更多的haskell”所以我只是想分享它。希望它有所帮助。

另一方面,我猜它应该很容易处理大文件而不会填满内存,这要归功于putStr在文件读取过程中已经清空了字符串。