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
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)