Уловка/захват stdout в haskell

Как я могу определить "catchOutput", так что запуск основных выходов только "бар"?

То есть, как я могу получить доступ как к выходному потоку (stdout), так и к фактическому выходу отдельного действия io?

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

Лучшее гипотетическое "решение", которое я нашел до сих пор, включает отключение stdout, вдохновленный этим, в поток файлов, а затем чтение из этого файла (помимо того, что супер-уродливый Я не смог прочитать сразу после записи из файла. Можно ли создать "настраиваемый поток буфера", который не нужно хранить в файле?). Хотя это немного похоже на боковую дорожку.

Другой угол, похоже, использует "hGetContents stdout", если он должен делать то, что, как я думаю, должен. Но мне не дано разрешение на чтение из stdout. Хотя googling, похоже, показывает, что он использовался .

Ответ 1

Почему бы просто не использовать монаду-писателю? Например,

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

В качестве альтернативы вы можете изменить свое внутреннее действие, чтобы вместо stdout записать Handle вместо записи. Затем вы можете использовать что-то вроде knob, чтобы создать дескриптор файла в памяти, который вы можете передать во внутреннее действие, и затем проверить его содержимое.

Ответ 2

В Hackage есть несколько пакетов, которые обещают сделать это: io-capture и молча. тихо поддерживается и работает на Windows тоже (io-capture работает только в Unix). С молчанием вы используете захват:

import System.IO.Silently

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

Обратите внимание, что он работает, перенаправляя вывод во временный файл, а затем читайте его... Но до тех пор, пока он работает!

Ответ 3

Я использовал следующую функцию для unit test функции, которая выводит на стандартный вывод.

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

Я не уверен в подходе к файлу в памяти, но он работает нормально для небольшого объема вывода с временным файлом.

Ответ 4

Как отметил @hammar, вы можете использовать ручку для создания файла в памяти, но вы также можете использовать hDuplicate и hDuplicateTo для изменения stdout в файл памяти и обратно. Что-то вроде следующего полностью непроверенного кода:

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)