我想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)
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是一个生成由命令行参数命名的文件内容的源.
我的第一个想法是这样的:
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 在文件读取期间已经可以清空字符串。