Catching/hijacking stdout in haskell

2019-03-15 23:50发布

问题:

How can I define 'catchOutput' so that running main outputs only 'bar'?

That is, how can I access both the output stream (stdout) and the actual output of an io action separately?

catchOutput :: IO a -> IO (a,String)
catchOutput = undefined

doSomethingWithOutput :: IO a -> IO ()
doSomethingWithOutput io = do
   (_ioOutp, stdOutp) <- catchOutput io
   if stdOutp == "foo"
      then putStrLn "bar"
      else putStrLn "fail!"

main = doSomethingWithOutput (putStr "foo")

The best hypothetical "solution" I've found so far includes diverting stdout, inspired by this, to a file stream and then reading from that file (Besides being super-ugly I haven't been able to read directly after writing from a file. Is it possible to create a "custom buffer stream" that doesn't have to store in a file?). Although that feels 'a bit' like a side track.

Another angle seems to use 'hGetContents stdout' if that is supposed to do what I think it should. But I'm not given permission to read from stdout. Although googling it seems to show that it has been used.

回答1:

Why not just use a writer monad instead? For example,

import Control.Monad.Writer

doSomethingWithOutput :: WriterT String IO a -> IO ()
doSomethingWithOutput io = do
   (_, res) <- runWriterT io
   if res == "foo"
      then putStrLn "bar"
      else putStrLn "fail!"

main = doSomethingWithOutput (tell "foo")

Alternatively, you could modify your inner action to take a Handle to write to instead of stdout. You can then use something like knob to make an in-memory file handle which you can pass to the inner action, and check its contents afterward.



回答2:

I used the following function for an unit test of a function that prints to stdout.

import GHC.IO.Handle
import System.IO
import System.Directory

catchOutput :: IO () -> IO String
catchOutput f = do
  tmpd <- getTemporaryDirectory
  (tmpf, tmph) <- openTempFile tmpd "haskell_stdout"
  stdout_dup <- hDuplicate stdout
  hDuplicateTo tmph stdout
  hClose tmph
  f
  hDuplicateTo stdout_dup stdout
  str <- readFile tmpf
  removeFile tmpf
  return str

I am not sure about the in-memory file approach, but it works okay for a small amount of output with a temporary file.



回答3:

There are some packages on Hackage that promise to do that : io-capture and silently. silently seems to be maintained and works on Windows too (io-capture only works on Unix). With silently, you use capture :

import System.IO.Silently

main = do
   (output, _) <- capture $ putStr "hello"
   putStrLn $ output ++ " world"

Note that it works by redirecting output to a temporary file and then read it... But as long as it works !



回答4:

As @hammar pointed out, you can use a knob to create an in-memory file, but you can also use hDuplicate and hDuplicateTo to change stdout to the memory file, and back again. Something like the following completely untested code:

catchOutput io = do
  knob <- newKnob (pack [])
  let before = do
        h <- newFileHandle knob "<stdout>" WriteMode
        stdout' <- hDuplicate stdout
        hDuplicateTo h stdout
        hClose h
        return stdout'
      after stdout' = do
        hDuplicateTo stdout' stdout
        hClose stdout'
  a <- bracket_ before after io
  bytes <- Data.Knob.getContents knob
  return (a, unpack bytes)