silently 1.2.4.1 → 1.2.5.4
raw patch · 5 files changed
Files
- CHANGELOG.md +22/−0
- README.md +51/−0
- silently.cabal +61/−29
- src/System/IO/Silently.hs +78/−66
- test/Spec.hs +10/−0
+ CHANGELOG.md view
@@ -0,0 +1,22 @@+# 1.2.5.4 November 2024++* Bump `cabal-version` to 1.18.+* README: alert of malfunction in multi-threaded settings.+* Tested with GHC 8.0 - 9.12.0.++# 1.2.5.3 August 2022++* Tested with GHC 7.0 - 9.4.1.+* Remove remnants of GHC 6.x support.+* Silence incomplete pattern matching warning, refactor code.+* Add section about limitations to README.++# 1.2.5.2 November 2021++* Tested with GHC 7.0 - 9.2.+* Silence warning caused by missing `other-modules` in cabal file.+* Add README and CHANGELOG to dist.++# 1.2.5.1 July 2019++No changelog for this and earlier versions.
+ README.md view
@@ -0,0 +1,51 @@+[](http://hackage.haskell.org/package/silently)+[](https://stackage.org/nightly/package/silently)+[](https://www.stackage.org/package/silently)+[](https://github.com/hspec/silently/actions)++# silently++Silently is a package that allows you to run an `IO` action and+prevent it from writing to `stdout`, or any other handle, by using+`silence`. Or you can capture the output for yourself using `capture`.++For example, the program+```haskell+ import System.IO.Silently++ main = do+ putStr "putStrLn: " >> putStrLn "puppies!"+ putStr "silenced: " >> silence (putStrLn "kittens!")+ putStrLn ""+ (captured, result) <- capture (putStr "wookies!" >> return 123)+ putStr "captured: " >> putStrLn captured+ putStr "returned: " >> putStrLn (show result)+```+will print:+```+ putStrLn: puppies!+ silenced:+ captured: wookies!+ returned: 123+```++## Not thread-safe++Since all threads of a process share the standard output handles `stdout` and `stderr`,+capturing output to these handle will capture the output of __all threads__,+not just the one running the action under `capture`.++In essence, this library does not work in a situation where multiple threads are writing to the handle+whose output _produced by the given action_ we want to capture.++See:+- https://github.com/hspec/silently/issues/6++## Further limitations++Capturing/silencing might not work as expected if the action uses the FFI+or conceals output under `unsafePerformIO` or similar unsafe operations.++Examples:+- FFI: https://github.com/hspec/silently/issues/3+- `unsafePerformIO`: https://github.com/bos/filemanip/issues/22
silently.cabal view
@@ -1,40 +1,66 @@+cabal-version: 1.18 name: silently-version: 1.2.4.1-cabal-version: >= 1.8+version: 1.2.5.4 build-type: Simple license: BSD3 license-file: LICENSE+author: Trystan Spangler copyright: (c) Trystan Spangler 2011-maintainer: trystan.s@comcast.net-stability:-homepage: https://github.com/trystan/silently-package-url: https://github.com/trystan/silently-bug-reports: https://github.com/trystan/silently/issues+maintainer: Simon Hengel <sol@typeful.net>, Andreas Abel+homepage: https://github.com/hspec/silently+package-url: https://github.com/hspec/silently+bug-reports: https://github.com/hspec/silently/issues synopsis: Prevent or capture writing to stdout and other handles.-description: Prevent or capture writing to stdout and other handles.-category:-author: Trystan Spangler-tested-with: GHC ==7.0+description: Prevent or capture writing to stdout, stderr, and other handles.+category: System, Testing +tested-with:+ GHC == 9.12.0+ GHC == 9.10.1+ GHC == 9.8.3+ GHC == 9.6.6+ GHC == 9.4.8+ GHC == 9.2.8+ GHC == 9.0.2+ GHC == 8.10.7+ GHC == 8.8.4+ GHC == 8.6.5+ GHC == 8.4.4+ GHC == 8.2.2+ GHC == 8.0.2++extra-doc-files:+ CHANGELOG.md+ README.md+ source-repository head type: git- location: https://github.com/trystan/silently+ location: https://github.com/hspec/silently Library+ hs-source-dirs:+ src+ default-language:+ Haskell98+ exposed-modules:+ System.IO.Silently+ build-depends:- base >=4 && <=5+ base >= 4.3 && < 5 , directory , deepseq- exposed-modules: System.IO.Silently - hs-source-dirs:- src- if os(windows) cpp-options: -DWINDOWS if os(linux) || os(osx) || os(freebsd) || os(openbsd) || os(netbsd) cpp-options: -DUNIX + ghc-options:+ -Wall+ if impl(ghc >= 8)+ ghc-options:+ -Wcompat+ -- This tests the platform specific implementation. -- -- NOTE: Cabal 1.10 can not deal with conditional (== if-else) options. This@@ -43,36 +69,42 @@ -- As a consequence we can not use Hspec for testing, as this would result in -- depending on two different versions of silently at the same time! test-suite spec-specific- main-is:- Spec.hs type: exitcode-stdio-1.0- ghc-options:- -Wall -Werror -threaded hs-source-dirs: test+ main-is:+ Spec.hs+ default-language:+ Haskell98+ ghc-options:+ -Wall -threaded build-depends:- base+ base >= 4.3 && < 5 , silently , directory , nanospec+ , temporary -- This tests the generic implementation, that should work on all platforms. test-suite spec-generic- main-is:- Spec.hs type: exitcode-stdio-1.0- ghc-options:- -Wall -threaded- -- FIXME: use -Werror- -- -Wall -Werror -threaded hs-source-dirs: src , test+ main-is:+ Spec.hs+ default-language:+ Haskell98+ ghc-options:+ -Wall -threaded+ other-modules:+ System.IO.Silently build-depends:- base+ base >= 4.3 && < 5 , deepseq , directory , nanospec+ , temporary
src/System/IO/Silently.hs view
@@ -1,32 +1,35 @@ {-# LANGUAGE CPP #-}--- | 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'.+{-# LANGUAGE ScopedTypeVariables #-} -module System.IO.Silently (- silence,- hSilence,- capture,- capture_,- hCapture,- hCapture_,-) where+-- | 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'. -import Prelude+module System.IO.Silently+ ( silence, hSilence+ , capture, capture_, hCapture, hCapture_+ ) where -#if __GLASGOW_HASKELL__ >= 612-import GHC.IO.Handle (hDuplicate, hDuplicateTo)-#else-import GHC.Handle (hDuplicate, hDuplicateTo)-#endif+import Prelude -import System.IO import qualified Control.Exception as E import Control.DeepSeq-import System.Directory (removeFile,getTemporaryDirectory)+ ( 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"+mNullDevice = Just "\\\\.\\NUL" #elif UNIX mNullDevice = Just "/dev/null" #else@@ -38,38 +41,46 @@ silence = hSilence [stdout] -- | Run an IO action while preventing all output to the given handles.-hSilence :: [Handle] -> IO a -> IO a-hSilence handles action = case mNullDevice of- Just nullDevice -> E.bracket (openFile nullDevice AppendMode)- hClose- prepareAndRun+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 - Nothing -> do- tmpDir <- getTempOrCurrentDirectory- E.bracket (openTempFile tmpDir "silence")- cleanup- (prepareAndRun . snd) - where- cleanup (tmpFile,tmpHandle) = do- hClose tmpHandle- removeFile tmpFile- prepareAndRun tmpHandle = go handles- where- go [] = action- go hs = goBracket go tmpHandle 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- -- availabel in base >= 4.4.+ -- 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.+-- 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] @@ -82,30 +93,31 @@ 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 :: [Handle] -> IO a -> IO (String, a)-hCapture handles action = do- tmpDir <- getTempOrCurrentDirectory- E.bracket (openTempFile tmpDir "capture")- cleanup- (prepareAndRun . snd)- where- cleanup (tmpFile,tmpHandle) = do- hClose tmpHandle- removeFile tmpFile- 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 hs = goBracket go tmpHandle hs+-- 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] -> IO a-goBracket go tmpHandle (h:hs) = E.bracket (do old <- hDuplicate h- hDuplicateTo tmpHandle h- return old)- (\old -> hDuplicateTo old h >> hClose old)- (\_ -> go 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)
test/Spec.hs view
@@ -4,8 +4,10 @@ import System.IO import System.IO.Silently import System.Directory+import System.IO.Temp import Control.Exception+import Control.Monad main :: IO () main = hspec spec@@ -26,3 +28,11 @@ describe "capture" $ do it "captures stdout" $ do capture (putStr "foo" >> return 23) `shouldReturn` ("foo", 23 :: Int)++ describe "hCapture" $ do+ forM_ [NoBuffering, LineBuffering, BlockBuffering Nothing] $ \buffering -> do+ it ("preserves " ++ show buffering) $ do+ withSystemTempFile "silently" $ \_file h -> do+ hSetBuffering h buffering+ _ <- hCapture [h] (return ())+ hGetBuffering h `shouldReturn` buffering