{-# LANGUAGE CPP #-} {-# LANGUAGE ScopedTypeVariables #-} -- | Need to prevent output to the terminal, a file, or stderr? -- Need to capture it and use it for your own means? -- Now you can, with 'silence' and 'capture'. module System.IO.Silently ( silence, hSilence , capture, capture_, hCapture, hCapture_ ) where import Prelude import qualified Control.Exception as E import Control.DeepSeq ( deepseq ) import GHC.IO.Handle ( hDuplicate, hDuplicateTo ) import System.Directory ( getTemporaryDirectory, removeFile ) import System.IO ( Handle, IOMode(AppendMode), SeekMode(AbsoluteSeek) , hClose, hFlush, hGetBuffering, hGetContents, hSeek, hSetBuffering , openFile, openTempFile, stdout ) mNullDevice :: Maybe FilePath #ifdef WINDOWS mNullDevice = Just "\\\\.\\NUL" #elif UNIX mNullDevice = Just "/dev/null" #else mNullDevice = Nothing #endif -- | Run an IO action while preventing all output to stdout. silence :: IO a -> IO a silence = hSilence [stdout] -- | Run an IO action while preventing all output to the given handles. hSilence :: forall a. [Handle] -> IO a -> IO a hSilence handles action = case mNullDevice of Just nullDevice -> E.bracket (openFile nullDevice AppendMode) hClose prepareAndRun Nothing -> withTempFile "silence" prepareAndRun where prepareAndRun :: Handle -> IO a prepareAndRun tmpHandle = go handles where go [] = action go (h:hs) = goBracket go tmpHandle h hs -- Provide a tempfile for the given action and remove it afterwards. withTempFile :: String -> (Handle -> IO a) -> IO a withTempFile tmpName action = do tmpDir <- getTempOrCurrentDirectory E.bracket (openTempFile tmpDir tmpName) cleanup (action . snd) where cleanup :: (FilePath, Handle) -> IO () cleanup (tmpFile, tmpHandle) = do hClose tmpHandle removeFile tmpFile getTempOrCurrentDirectory :: IO String getTempOrCurrentDirectory = getTemporaryDirectory `catchIOError` (\_ -> return ".") where -- NOTE: We can not use `catchIOError` from "System.IO.Error", it is only -- available in base >= 4.4. catchIOError :: IO a -> (IOError -> IO a) -> IO a catchIOError = E.catch -- | Run an IO action while preventing and capturing all output to stdout. -- This will, as a side effect, create and delete a temp file in the temp directory -- or current directory if there is no temp directory. capture :: IO a -> IO (String, a) capture = hCapture [stdout] -- | Like `capture`, but discards the result of given action. capture_ :: IO a -> IO String capture_ = fmap fst . capture -- | Like `hCapture`, but discards the result of given action. hCapture_ :: [Handle] -> IO a -> IO String hCapture_ handles = fmap fst . hCapture handles -- | Run an IO action while preventing and capturing all output to the given handles. -- This will, as a side effect, create and delete a temp file in the temp directory -- or current directory if there is no temp directory. hCapture :: forall a. [Handle] -> IO a -> IO (String, a) hCapture handles action = withTempFile "capture" prepareAndRun where prepareAndRun :: Handle -> IO (String, a) prepareAndRun tmpHandle = go handles where go [] = do a <- action mapM_ hFlush handles hSeek tmpHandle AbsoluteSeek 0 str <- hGetContents tmpHandle str `deepseq` return (str, a) go (h:hs) = goBracket go tmpHandle h hs goBracket :: ([Handle] -> IO a) -> Handle -> Handle -> [Handle] -> IO a goBracket go tmpHandle h hs = do buffering <- hGetBuffering h let redirect = do old <- hDuplicate h hDuplicateTo tmpHandle h return old restore old = do hDuplicateTo old h hSetBuffering h buffering hClose old E.bracket redirect restore (\_ -> go hs)