我正在尝试在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
答案 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在文件读取过程中已经清空了字符串。