如何在Haskell中实现`cat`?

Sam*_*Sam 13 haskell conduit

我想cat在Haskell中编写一个简单的程序.我想将多个文件名作为参数,并将每个文件顺序写入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
Run Code Online (Sandbox Code Playgroud)

我正在运行这样的代码:

runghc cat.hs file1 file2
Run Code Online (Sandbox Code Playgroud)

sha*_*ang 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
Run Code Online (Sandbox Code Playgroud)

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

但是,这整个方法过于复杂.你可以简单地把猫写成

main = do
    files <- getArgs
    forM_ files $ \filename -> do
        contents <- readFile filename
        putStr contents
Run Code Online (Sandbox Code Playgroud)

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

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

main = getArgs >>= mapM_ (readFile >=> putStr)
Run Code Online (Sandbox Code Playgroud)

  • 谢谢,很高兴知道实际的名字,所以我可以阅读它.我可能会继续称它为"左鱼"和"右鱼". (5认同)

Lui*_*las 17

如果您安装了非常有用的conduit软件包,可以这样做:

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
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:您也可以这样写:

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

type Filename = String

catFiles :: [Filename] -> IO ()
catFiles files = runResourceT $ mapM_ sourceFile files $$ sinkHandle stdout
Run Code Online (Sandbox Code Playgroud)

在原文中,sourceFile filename创建一个Source从命名文件中读取的内容; 我们forM_在外面使用循环遍历每个参数并对ResourceT每个文件名运行计算.

但是在Conduit中你可以使用monadic >>来连接源代码; source1 >> source2是一个产生元素的源,source1直到它完成,然后产生的元素source2.所以在第二个例子中,mapM_ sourceFile files相当于连接所有源的sourceFile file0 >> ... >> sourceFile filen-a 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
Run Code Online (Sandbox Code Playgroud)

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

  • +1是"管道"实现的简洁和优雅的绝佳证明.我想知道`getArgs`-esque来源是否有用.然后你可以写`runResourceT $ sourceArgs $ = readFileConduit $$ sinkHandle stdout`其中`sourceArgs :: MonadIO m => Source m String`和`readFileConduit :: MonadResource m => Conduit FileName m ByteString` (3认同)

sth*_*sth 5

catHandle,这是间接地从调用catFileArray,调用exitWith当它到达第一文件的末尾.这将终止程序,并且不再读取其他文件.

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


exa*_*exa 5

我的第一个想法是这样的:

import System.Environment
import System.IO
import Control.Monad
main = getArgs >>= mapM_ (\name -> readFile name >>= putStr)
Run Code Online (Sandbox Code Playgroud)

它并没有真正以unix-y的方式失败,并且不做标准输入或多字节的事情,但它“更像haskell”,所以我只是想分享这一点。希望能帮助到你。

另一方面,我想它应该可以轻松处理大文件而不会填满内存,因为 putStr 在文件读取期间已经可以清空字符串。