2012-07-13 26 views
12

Estoy tratando de escribir un programa simple cat en Haskell. Me gustaría tomar varios nombres de archivo como argumentos y escribir cada archivo secuencialmente en STDOUT, pero mi programa solo imprime un archivo y sale.¿Cómo implemento `cat` en Haskell?

¿Qué debo hacer para que mi código imprima todos los archivos, no solo el primero ingresado?

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 

Me postulo el código como el siguiente:

runghc cat.hs file1 file2 

Respuesta

16

Tu problema es que exitWith termina el programa completo. Por lo tanto, no puede usar realmente forever para recorrer el archivo, porque obviamente no desea ejecutar la función "para siempre", solo hasta el final del archivo. Puede volver a escribir catHandle como esto

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

es decir, si no hemos llegado a EOF, recurrimos y leemos otra línea.

Sin embargo, todo este enfoque es demasiado complicado. Se puede escribir simplemente como gato

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

Debido perezoso de E/S, el contenido del archivo enteros no son realmente cargado en la memoria, pero transmitido en la salida estándar.

Si se siente cómodo con los operadores de Control.Monad, todo el programa se puede acortar hasta

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

Cambié la respuesta aceptada a la tuya porque corrigió mi error y también me explicó la transmisión lenta de IO. – Sam

+0

¿Cómo se pronuncia '> =>'? – Sam

+0

"composición kleisli". No conozco un nombre mejor (más corto) para él. – shang

5

catHandle, que se llama indirectamente de catFileArray, pide exitWith cuando se llega al final del primer archivo. Esto termina el programa y ya no se leen más archivos.

En su lugar, debe devolver normalmente desde la función catHandle cuando se haya alcanzado el final del archivo. Esto probablemente significa que no debe hacer la lectura forever.

+0

¡Ah, lo tengo, gracias! – Sam

4

Mi primera idea es la siguiente:

import System.Environment 
import System.IO 
import Control.Monad 
main = getArgs >>= mapM_ (\name -> readFile name >>= putStr) 

En realidad, no falle en UNIX -y way, y no hace cosas stdin ni multibyte, pero es "mucho más haskell", así que solo quería compartir eso. Espero eso ayude.

Por otro lado, supongo que debería manejar archivos grandes fácilmente sin llenar la memoria, gracias al hecho de que putStr ya puede vaciar la cadena durante la lectura del archivo.

+0

+1 para un enfoque alternativo, gracias. – Sam

17

Si instala el muy útiles conduit package, puede hacerlo de esta manera:

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 

Esto es similar a Shang le sugiere una solución sencilla, pero el uso de conductos y ByteString en lugar de perezoso de E/S y String. Ambas cosas son buenas para aprender a evitar: la E/S diferida libera recursos en tiempos impredecibles; String tiene mucha sobrecarga de memoria.

Tenga en cuenta que ByteString está destinado a representar datos binarios, no texto.En este caso, solo estamos tratando los archivos como secuencias de bytes no interpretadas, por lo que ByteString está bien de usar. Si OTOH estuviéramos procesando el archivo como texto -contenedores de caracteres, análisis, etc.-querríamos usar Data.Text.

EDIT: También puede escribir así:

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

type Filename = String 

catFiles :: [Filename] -> IO() 
catFiles files = runResourceT $ mapM_ sourceFile files $$ sinkHandle stdout 

En el original, sourceFile filename crea una Source que lee desde el archivo llamado; y usamos forM_ en el exterior para recorrer cada argumento y ejecutar el cálculo ResourceT sobre cada nombre de archivo.

Sin embargo, en Conduit puede usar monadic >> para concatenar fuentes; source1 >> source2 es una fuente que produce los elementos de source1 hasta que se hace, y luego produce los de source2. Por lo tanto, en este segundo ejemplo, mapM_ sourceFile files es equivalente a sourceFile file0 >> ... >> sourceFile filen -a Source que concatena todas las fuentes.

EDIT 2: Y siguiendo la sugerencia de Dan Burton en el comentario a esta respuesta:

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 

En Inglés, sourceArgs $= readFileConduit es una fuente que produce el contenido de los archivos nombrados por los argumentos de línea de comandos.

+3

+1 un excelente testimonio de la simplicidad y elegancia que 'conduit' ha logrado. Me pregunto si una Fuente 'getArgs'-esque sería de utilidad. Luego podría escribir 'runResourceT $ sourceArgs $ = readFileConduit $$ sinkHandle stdout' donde' sourceArgs :: MonadIO m => Source m String' y 'readFileConduit :: MonadResource m => Conduit FileName m ByteString' –

+0

@DanBurton: Estoy seguí aprendiendo conductos, así que decidí intentarlo, y lo logré en 10 minutos. Editaré la respuesta para agregar esa versión. –

+0

Esto no es técnicamente la respuesta a mi pregunta, pero es tan informativo que lo consideraría "lectura obligatoria" para cualquier persona con preguntas similares sobre Haskell. – Sam