haskellconduit

How do I implement `cat` in Haskell?


I am trying to write a simple cat program in Haskell. I would like to take multiple filenames as arguments, and write each file sequentially to STDOUT, but my program only prints one file and exits.

What do I need to do to make my code print every file, not just the first one passed in?

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

I am running the code like this:

runghc cat.hs file1 file2

Solution

  • Your problem is that exitWith terminates the whole program. So, you cannot really use forever to loop through the file, because obviously you don't want to run the function "forever", just until the end of the file. You can rewrite catHandle like this

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

    I.e. if we haven't reached EOF, we recurse and read another line.

    However, this whole approach is overly complicated. You can write cat simply as

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

    Because of lazy i/o, the whole file contents are not actually loaded into memory, but streamed into stdout.

    If you are comfortable with the operators from Control.Monad, the whole program can be shortened down to

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