packages feed

HSH 1.2.6 → 2.1.3

raw patch · 6 files changed

Files

HSH.cabal view
@@ -1,10 +1,11 @@ Name: HSH-Version: 1.2.6+Version: 2.1.3 License: LGPL Maintainer: John Goerzen <jgoerzen@complete.org> Author: John Goerzen Stability: Beta-Copyright: Copyright (c) 2006-2008 John Goerzen+Copyright: Copyright (c) 2006-2017 John Goerzen+Category: system license-file: COPYRIGHT extra-source-files: COPYING homepage: http://software.complete.org/hsh@@ -13,18 +14,39 @@  Haskell programs. With HSH, it is possible to easily run shell  commands, capture their output or provide their input, and pipe them  to and from other shell commands and arbitrary Haskell functions at will.-Exposed-Modules: HSH, HSH.Command, HSH.ShellEquivs-Extensions: ExistentialQuantification, OverlappingInstances,-    UndecidableInstances, FlexibleContexts-Build-Depends: base, unix, mtl, regex-compat, MissingH>=1.0.0,- hslogger, filepath, regex-base, regex-posix, directory, bytestring-GHC-Options: -O2 -threaded -Wall-Category: System+  Category: System++Cabal-Version: >=1.2.3 Build-type: Simple -Executable: runtests-Buildable: False-Main-Is: runtests.hs-HS-Source-Dirs: testsrc-Extensions: ExistentialQuantification, OverlappingInstances,- UndecidableInstances, FlexibleContexts+flag buildtests+  description: Build the executable to run unit tests+  default: False++library+  Exposed-Modules: HSH, HSH.Command, HSH.ShellEquivs, HSH.Channel+  Extensions: ExistentialQuantification, OverlappingInstances,+    UndecidableInstances, FlexibleContexts, CPP+  Build-Depends: base >= 4 && < 5, mtl, process, regex-compat, MissingH>=1.0.0,+    hslogger, filepath, regex-base, regex-posix, directory,+    bytestring+  if !os(windows)+    Build-Depends: unix+  GHC-Options: -O2 -Wall++Executable runtests+  if flag(buildtests)+    Buildable: True+    Build-Depends: base >= 4 && < 5, mtl, process, regex-compat,+      MissingH>=1.0.0,+      hslogger, filepath, regex-base, regex-posix, directory,+      bytestring, HUnit, testpack+    if !os(windows)+      Build-Depends: unix+  else+    Buildable: False+  Main-Is: runtests.hs+  HS-Source-Dirs: testsrc, .+  Extensions: ExistentialQuantification, OverlappingInstances,+    UndecidableInstances, FlexibleContexts, CPP+  GHC-Options: -O2 -threaded
HSH.hs view
@@ -85,9 +85,8 @@  * A Haskell function.  This function will accept input representing    its standard input and generate output to go to stdout.  Function    types that are supported natively include @(String -> String)@,-   @(String -> IO String)@, @([String] -> [String])@, -   @([String] -> IO [String])@.  Those that accept a @[String]@ type will-   have each string in the list representing a single line.+   @(String -> IO String)@, plus many more involving ByteStrings and functions+   that take no input.  See 'HSH.Command.ShellCommand' for more.  Pipes can be constructed by using the -|- operator, as illustrated above. It is quite possible to pipe data between Haskell functions and
+ HSH/Channel.hs view
@@ -0,0 +1,84 @@+{-# OPTIONS_GHC -XFlexibleInstances -XTypeSynonymInstances #-}++{- Channel basics for HSH+Copyright (C) 2004-2008 John Goerzen <jgoerzen@complete.org>+Please see the COPYRIGHT file+-}++{- |+   Module     : HSH.Channel+   Copyright  : Copyright (C) 2006-2009 John Goerzen+   License    : GNU LGPL, version 2.1 or above++   Maintainer : John Goerzen <jgoerzen@complete.org>+   Stability  : provisional+   Portability: portable++Copyright (c) 2006-2009 John Goerzen, jgoerzen\@complete.org+-}++module HSH.Channel (Channel(..),+                    chanAsString,+                    chanAsBSL,+                    chanAsBS,+                    chanToHandle,+                    Channelizable(..)+                   ) where++import qualified Data.ByteString.Lazy as BSL+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy.Char8 as BSLC+import System.IO+import Control.Concurrent++{- | The main type for communicating between commands.  All are expected to+be lazy. -}+data Channel = ChanString String+             | ChanBSL BSL.ByteString+             | ChanHandle Handle++chanAsString :: Channel -> IO String+chanAsString (ChanString s) = return s+chanAsString (ChanBSL s) = return . bsl2str $ s+chanAsString (ChanHandle h) = hGetContents h++chanAsBSL :: Channel -> IO BSL.ByteString+chanAsBSL (ChanString s) = return . str2bsl $ s+chanAsBSL (ChanBSL s) = return s+chanAsBSL (ChanHandle h) = BSL.hGetContents h++chanAsBS :: Channel -> IO BS.ByteString+chanAsBS c = do r <- chanAsBSL c+                let contents = BSL.toChunks r+                return . BS.concat $ contents++{- | Writes the Channel to the given Handle. If the first parameter is True,+     do this in a separate thread and close the handle afterwards.+-}+chanToHandle :: Bool -> Channel -> Handle -> IO ()+chanToHandle close c h = if close then forkIO (dumpChanToHandle c h >> hClose h) >> return ()+                                  else dumpChanToHandle c h+   where dumpChanToHandle (ChanString s) h = hPutStr h s+         dumpChanToHandle (ChanBSL s) h = BSL.hPut h s+         dumpChanToHandle (ChanHandle srchdl) desthdl+              = BSL.hGetContents srchdl >>= BSL.hPut desthdl++class Channelizable a where+    toChannel :: a -> Channel+instance Channelizable String where+    toChannel = ChanString+instance Channelizable BSL.ByteString where+    toChannel = ChanBSL+instance Channelizable Handle where+    toChannel = ChanHandle+instance Channelizable BS.ByteString where+    toChannel bs = ChanBSL . BSL.fromChunks $ [bs]+++                    +str2bsl :: String -> BSL.ByteString+str2bsl = BSLC.pack++bsl2str :: BSL.ByteString -> String+bsl2str = BSLC.unpack+
HSH/Command.hs view
@@ -1,4 +1,4 @@-{-# OPTIONS_GHC -XFlexibleInstances -XTypeSynonymInstances #-}+{-# OPTIONS_GHC -XFlexibleInstances -XTypeSynonymInstances -XStandaloneDeriving #-}  {- Commands for HSH Copyright (C) 2004-2008 John Goerzen <jgoerzen@complete.org>@@ -7,17 +7,18 @@  {- |    Module     : HSH.Command-   Copyright  : Copyright (C) 2006-2008 John Goerzen+   Copyright  : Copyright (C) 2006-2009 John Goerzen    License    : GNU LGPL, version 2.1 or above     Maintainer : John Goerzen <jgoerzen@complete.org>    Stability  : provisional    Portability: portable -Copyright (c) 2006-2007 John Goerzen, jgoerzen\@complete.org+Copyright (c) 2006-2009 John Goerzen, jgoerzen\@complete.org -} -module HSH.Command (ShellCommand(..),+module HSH.Command (Environment,+                    ShellCommand(..),                     PipeCommand(..),                     (-|-),                     RunResult,@@ -25,75 +26,139 @@                     runIO,                     runSL,                     InvokeResult,+                    checkResults,                     tryEC,                     catchEC,+                    setenv,+                    unsetenv                    ) where  -- import System.IO.HVIO -- import System.IO.Utils+import Prelude hiding (catch) import System.IO import System.Exit-import System.Posix.Types-import System.Posix.IO-import System.Posix.Process import System.Log.Logger-import System.IO.Error+import System.IO.Error (isUserError, ioeGetErrorString) import Data.Maybe.Utils import Data.Maybe import Data.List.Utils(uniq)-import Control.Exception(evaluate)-import System.Posix.Env+import Control.Exception(try, evaluate, SomeException, catch) import Text.Regex.Posix import Control.Monad(when) import Data.String.Utils(rstrip)+import Control.Concurrent+import System.Process+import System.Environment(getEnvironment) import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString as BS+import HSH.Channel  d, dr :: String -> IO () d = debugM "HSH.Command" dr = debugM "HSH.Command.Run"+em = errorM "HSH.Command"  {- | Result type for shell commands.  The String is the text description of the command, not its output. -}-type InvokeResult = (String, IO ProcessStatus)+type InvokeResult = (String, IO ExitCode) +{- | Type for the environment. -}+type Environment = Maybe [(String, String)]+ {- | A shell command is something we can invoke, pipe to, pipe from, or pipe in both directions.  All commands that can be run as shell commands must define these methods.  Minimum implementation is 'fdInvoke'.++Some pre-defined instances include:++ * A simple bare string, which is passed to the shell for execution. The shell+   will then typically expand wildcards, parse parameters, etc.++ * A @(String, [String])@ tuple.  The first item in the tuple gives+   the name of a program to run, and the second gives its arguments.+   The shell is never involved.  This is ideal for passing filenames,+   since there is no security risk involving special shell characters.++ * A @Handle -> Handle -> IO ()@ function, which reads from the first+   handle and write to the second.++ * Various functions.  These functions will accept input representing+   its standard input and output will go to standard output.  ++Some pre-defined instance functions include:++ * @(String -> String)@, @(String -> IO String)@, plus the same definitions+   for ByteStrings.++ * @([String] -> [String])@, @([String] -> IO [String])@, where each @String@+   in the list represents a single line++ * @(() -> String)@, @(() -> IO String)@, for commands that explicitly+   read no input.  Useful with closures.  Useful when you want to avoid+   reading stdin because something else already is.  These have the unit as+   part of the function because otherwise we would have conflicts with things+   such as bare Strings, which represent a command name.+ -} class (Show a) => ShellCommand a where     {- | Invoke a command. -}     fdInvoke :: a               -- ^ The command-             -> Fd              -- ^ fd to pass to it as stdin-             -> Fd              -- ^ fd to pass to it as stdout-             -> [Fd]            -- ^ Fds to close in the child.  Will not harm Fds this child needs for stdin and stdout.-             -> (IO ())           -- ^ Action to run post-fork in child (or in main process if it doesn't fork)-             -> IO [InvokeResult]           -- ^ Returns an action that, when evaluated, waits for the process to finish and returns an exit code.+             -> Environment     -- ^ The environment+             -> Channel         -- ^ Where to read input from+             -> IO (Channel, [InvokeResult]) -- ^ Returns an action that, when evaluated, waits for the process to finish and returns an exit code. +instance Show (Handle -> Handle -> IO ()) where+    show _ = "(Handle -> Handle -> IO ())"+instance Show (Channel -> IO Channel) where+    show _ = "(Channel -> IO Channel)" instance Show (String -> String) where     show _ = "(String -> String)"+instance Show (() -> String) where+    show _ = "(() -> String)" instance Show (String -> IO String) where     show _ = "(String -> IO String)"+instance Show (() -> IO String) where+    show _ = "(() -> IO String)" instance Show (BSL.ByteString -> BSL.ByteString) where     show _ = "(Data.ByteString.Lazy.ByteString -> Data.ByteString.Lazy.ByteString)"+instance Show (() -> BSL.ByteString) where+    show _ = "(() -> Data.ByteString.Lazy.ByteString)" instance Show (BSL.ByteString -> IO BSL.ByteString) where     show _ = "(Data.ByteString.Lazy.ByteString -> IO Data.ByteString.Lazy.ByteString)"+instance Show (() -> IO BSL.ByteString) where+    show _ =  "(() -> IO BSL.ByteString)" instance Show (BS.ByteString -> BS.ByteString) where     show _ = "(Data.ByteString.ByteString -> Data.ByteString.ByteString)"+instance Show (() -> BS.ByteString) where+    show _ = "(() -> Data.ByteString.ByteString)" instance Show (BS.ByteString -> IO BS.ByteString) where     show _ = "(Data.ByteString.ByteString -> IO Data.ByteString.ByteString)"+instance Show (() -> IO BS.ByteString) where+    show _ = "(() -> IO Data.ByteString.ByteString)"  instance ShellCommand (String -> IO String) where-    fdInvoke = genericStringlikeIO hGetContents hPutStr+    fdInvoke = genericStringlikeIO chanAsString +{- | A user function that takes no input, and generates output.  We will deal+with it using hPutStr to send the output on. -}+instance ShellCommand (() -> IO String) where+    fdInvoke = genericStringlikeO+ instance ShellCommand (BSL.ByteString -> IO BSL.ByteString) where-    fdInvoke = genericStringlikeIO BSL.hGetContents BSL.hPut+    fdInvoke = genericStringlikeIO chanAsBSL +instance ShellCommand (() -> IO BSL.ByteString) where+    fdInvoke = genericStringlikeO+ instance ShellCommand (BS.ByteString -> IO BS.ByteString) where-    fdInvoke = genericStringlikeIO BS.hGetContents BS.hPut+    fdInvoke = genericStringlikeIO chanAsBS +instance ShellCommand (() -> IO BS.ByteString) where+    fdInvoke = genericStringlikeO+ {- | An instance of 'ShellCommand' for a pure Haskell function mapping String to String.  Implement in terms of (String -> IO String) for simplicity. -}@@ -103,63 +168,77 @@             where iofunc :: String -> IO String                   iofunc = return . func +instance ShellCommand (() -> String) where+    fdInvoke func =+        fdInvoke iofunc+            where iofunc :: () -> IO String+                  iofunc = return . func+ instance ShellCommand (BSL.ByteString -> BSL.ByteString) where     fdInvoke func =         fdInvoke iofunc             where iofunc :: BSL.ByteString -> IO BSL.ByteString                   iofunc = return . func +instance ShellCommand (() -> BSL.ByteString) where+    fdInvoke func =+        fdInvoke iofunc+            where iofunc :: () -> IO BSL.ByteString+                  iofunc = return . func+ instance ShellCommand (BS.ByteString -> BS.ByteString) where     fdInvoke func =         fdInvoke iofunc             where iofunc :: BS.ByteString -> IO BS.ByteString                   iofunc = return . func -genericStringlikeIO :: (Show (a -> IO a)) => -                       (Handle -> IO a) -                    -> (Handle -> a -> IO ()) -                    -> (a -> IO a) -                    -> Fd-                    -> Fd -                    -> [Fd] -                    -> (IO ()) -                    -> IO [InvokeResult]-genericStringlikeIO getcontentsfunc hputstrfunc func fstdin fstdout childclosefds childfunc =-        do -- d $ "SIOSF: Before fork"-           p <- try (forkProcess childstuff)-           pid <- case p of-                    Right x -> return x-                    Left x -> fail $ "Error in fork for func: " ++ show x-           -- d $ "SIOSFP: New func pid " ++ show pid-           return $ seq pid pid-           return [(show func,-                   getProcessStatus True False pid >>=-                                    (return . forceMaybe))]-        where childstuff = do closefds childclosefds [fstdin, fstdout]-                              d $ "SIOSFC Input is on " ++ show fstdin-                              hr <- fdToHandle fstdin-                              d $ "SIOSFC Output is on " ++ show fstdout-                              hw <- fdToHandle fstdout-                              hSetBuffering hw LineBuffering-                              d $ "SIOSFC Running child func"-                              childfunc-                              d $ "SIOSFC Running func in child"-                              contents <- getcontentsfunc hr-                              d $ "SIOSFC Contents read"-                              result <- func contents-                              d $ "SIOSFC Func applied"-                              hputstrfunc hw result-                              d $ "SIOSFC Func done, closing handles."-                              hClose hr-                              hClose hw-                              d $ "SIOSFC Child exiting."-                              -- It hung here without the exitImmediately-                              --exitImmediately ExitSuccess+instance ShellCommand (() -> BS.ByteString) where+    fdInvoke func =+        fdInvoke iofunc+            where iofunc :: () -> IO BS.ByteString+                  iofunc = return . func +instance ShellCommand (Channel -> IO Channel) where+    fdInvoke func _ cstdin =+        runInHandler (show func) (func cstdin)++{-+instance ShellCommand (Handle -> Handle -> IO ()) where+    fdInvoke func cstdin cstdout =+        runInHandler (show func) (func hstdin hstdout)+-}++genericStringlikeIO :: (Show (a -> IO a), Channelizable a) =>+                       (Channel -> IO a)+                    -> (a -> IO a)+                    -> Environment+                    -> Channel+                    -> IO (Channel, [InvokeResult])+genericStringlikeIO dechanfunc userfunc _ cstdin =+    do contents <- dechanfunc cstdin+       runInHandler (show userfunc) (realfunc contents)+    where realfunc contents = do r <- userfunc contents+                                 return (toChannel r)++genericStringlikeO :: (Show (() -> IO a), Channelizable a) =>+                      (() -> IO a)+                   -> Environment+                   -> Channel+                   -> IO (Channel, [InvokeResult])+genericStringlikeO userfunc _ _ =+    runInHandler (show userfunc) realfunc+        where realfunc :: IO Channel+              realfunc = do r <- userfunc ()+                            return (toChannel r)+ instance Show ([String] -> [String]) where     show _ = "([String] -> [String])"+instance Show (() -> [String]) where+    show _ = "(() -> [String])" instance Show ([String] -> IO [String]) where     show _ = "([String] -> IO [String])"+instance Show (() -> IO [String]) where+    show _ = "(() -> IO [String])"  {- | An instance of 'ShellCommand' for a pure Haskell function mapping [String] to [String].@@ -172,94 +251,129 @@ instance ShellCommand ([String] -> [String]) where     fdInvoke func = fdInvoke (unlines . func . lines) +instance ShellCommand (() -> [String]) where+    fdInvoke func = fdInvoke (unlines . func)+ {- | The same for an IO function -} instance ShellCommand ([String] -> IO [String]) where     fdInvoke func = fdInvoke iofunc         where iofunc input = do r <- func (lines input)                                 return (unlines r) +instance ShellCommand (() -> IO [String]) where+    fdInvoke func = fdInvoke iofunc+        where iofunc :: (() -> IO String)+              iofunc () = do r <- func ()+                             return (unlines r) + {- | An instance of 'ShellCommand' for an external command.  The first String is the command to run, and the list of Strings represents the arguments to the program, if any. -} instance ShellCommand (String, [String]) where-    fdInvoke pc@(cmd, args) fstdin fstdout childclosefds childfunc =-        do d $ "S Before fork for " ++ show pc-           p <- try (forkProcess childstuff)-           pid <- case p of-                    Right x -> return x-                    Left x -> fail $ "Error in fork: " ++ show x-           d $ "SP New pid " ++ show pid ++ " for " ++ show pc-           return $ seq pid pid-           return [(show (cmd, args),-                   getProcessStatus True False pid >>=-                                        (return . forceMaybe))]--        where-              childstuff = do d $ "SC preparing to redir"-                              d $ "SC input is on " ++ show fstdin-                              d $ "SC output is on " ++ show fstdout-                              redir fstdin stdInput-                              redir fstdout stdOutput-                              closefds childclosefds [fstdin, fstdout, 0, 1]-                              childfunc-                              dr ("RUN: " ++ cmd ++ " " ++ (show args))-                              executeFile cmd True args Nothing+    fdInvoke (fp, args) = genericCommand (RawCommand fp args)  {- | An instance of 'ShellCommand' for an external command.  The String is split using words to the command to run, and the arguments, if any. -} instance ShellCommand String where-    fdInvoke cmdline ifd ofd closefd forkfunc =-        do esh <- getEnv "SHELL"-           let sh = case esh of-                      Nothing -> "/bin/sh"-                      Just x -> x-           fdInvoke (sh, ["-c", cmdline]) ifd ofd closefd forkfunc+    fdInvoke cmd = genericCommand (ShellCommand cmd) -redir :: Fd -> Fd -> IO ()-redir fromfd tofd-    | fromfd == tofd = do d $ "ignoring identical redir " ++ show fromfd-                          return ()-    | otherwise = do d $ "running dupTo " ++ show (fromfd, tofd)-                     dupTo fromfd tofd-                     closeFd fromfd+{- | How to we handle and external command. -}+genericCommand :: CmdSpec +               -> Environment+               -> Channel+               -> IO (Channel, [InvokeResult]) -closefds :: [Fd]                   -- ^ List of Fds to possibly close-         -> [Fd]                   -- ^ List of Fds to not touch, ever-         -> IO ()-closefds inpclosefds noclosefds =-    do d $ "closefds " ++ show uclosefds ++ " " ++ show noclosefds-       mapM_ closeit . filter (\x -> not (x `elem` noclosefds)) $ uclosefds-    where closeit fd = do d $ "Closing fd " ++ show fd-                          closeFd fd-          uclosefds = uniq inpclosefds+-- Handling external command when stdin channel is a Handle+genericCommand c environ (ChanHandle ih) =+    let cp = CreateProcess {cmdspec = c,+                            cwd = Nothing,+                            env = environ,+                            std_in = UseHandle ih,+                            std_out = CreatePipe,+                            std_err = Inherit,+                            close_fds = True+#if MIN_VERSION_process(1,1,0)+-- Or use GHC version as a proxy:  __GLASGOW_HASKELL__ >= 720+			    -- Added field in process 1.1.0.0:+			    , create_group = False+#endif+#if MIN_VERSION_process(1,2,0)+			    , delegate_ctlc = False+#endif+#if MIN_VERSION_process(1,3,0)+          , detach_console = False+          , create_new_console = False+          , new_session = False+#endif+#if MIN_VERSION_process(1,4,0)+          , child_group = Nothing+          , child_user = Nothing+#endif+#if MIN_VERSION_process(1,5,0)+          , use_process_jobs = False+#endif+			   }+    in do (_, oh', _, ph) <- createProcess cp+          let oh = fromJust oh'+          return (ChanHandle oh, [(printCmdSpec c, waitForProcess ph)])+genericCommand cspec environ ichan = +    let cp = CreateProcess {cmdspec = cspec,+                            cwd = Nothing,+                            env = environ,+                            std_in = CreatePipe,+                            std_out = CreatePipe,+                            std_err = Inherit,+                            close_fds = True+#if MIN_VERSION_process(1,1,0)+			    -- Added field in process 1.1.0.0:+			    , create_group = False+#endif+#if MIN_VERSION_process(1,2,0)+			    , delegate_ctlc = False+#endif+#if MIN_VERSION_process(1,3,0)+          , detach_console = False+          , create_new_console = False+          , new_session = False+#endif+#if MIN_VERSION_process(1,4,0)+          , child_group = Nothing+          , child_user = Nothing+#endif+#if MIN_VERSION_process(1,5,0)+          , use_process_jobs = False+#endif+			   }+    in do (ih', oh', _, ph) <- createProcess cp+          let ih = fromJust ih'+          let oh = fromJust oh'+          chanToHandle True ichan ih+          return (ChanHandle oh, [(printCmdSpec cspec, waitForProcess ph)]) -data (ShellCommand a, ShellCommand b) => PipeCommand a b = PipeCommand a b-   deriving Show+printCmdSpec :: CmdSpec -> String+printCmdSpec (ShellCommand s) = s+printCmdSpec (RawCommand fp args) = show (fp, args) +------------------------------------------------------------+-- Pipes+------------------------------------------------------------++data PipeCommand a b = (ShellCommand a, ShellCommand b) => PipeCommand a b++deriving instance Show (PipeCommand a b)+ {- | An instance of 'ShellCommand' represeting a pipeline. -} instance (ShellCommand a, ShellCommand b) => ShellCommand (PipeCommand a b) where-    fdInvoke pc@(PipeCommand cmd1 cmd2) fstdin fstdout childclosefds forkfunc =-        do d $ "*** Handling pipe: " ++ show pc-           (reader, writer) <- createPipe-           let allfdstoclose = reader : writer : fstdin : fstdout : childclosefds-           d $ "pipd fdInvoke: New pipe endpoints: " ++ show (reader, writer)-           res1 <- fdInvoke cmd1 fstdin writer allfdstoclose forkfunc-           res2 <- fdInvoke cmd2 reader fstdout allfdstoclose forkfunc-           d $ "pipe fdInvoke: Parent closing " ++ show [reader, writer]-           mapM_ closeFd [reader, writer]--           d $ "*** Done handling pipe " ++ show pc-           return $ res1 ++ res2+    fdInvoke (PipeCommand cmd1 cmd2) env ichan =+        do (chan1, res1) <- fdInvoke cmd1 env ichan+           (chan2, res2) <- fdInvoke cmd2 env chan1+           return (chan2, res1 ++ res2)  {- | Pipe the output of the first command into the input of the second. -} (-|-) :: (ShellCommand a, ShellCommand b) => a -> b -> PipeCommand a b (-|-) = PipeCommand -{- | Function to use when there is nothing for the child to do -}-nullChildFunc :: IO ()-nullChildFunc = return ()- {- | Different ways to get data from 'run'.   * IO () runs, throws an exception on error, and sends stdout to stdout@@ -270,16 +384,23 @@  * IO [String] is same as IO String, but returns the results as lines.    Note: this output is not lazy. - * IO ProcessStatus runs and returns a ProcessStatus with the exit+ * IO ExitCode runs and returns an ExitCode with the exit    information.  stdout is sent to stdout.  Exceptions are not thrown. - * IO (String, ProcessStatus) is like IO ProcessStatus, but also+ * IO (String, ExitCode) is like IO ExitCode, but also    includes a description of the last command in the pipe to have-   an error (or the last command, if there was no error)+   an error (or the last command, if there was no error). - * IO ByteString and IO (ByteString, ProcessStatus) are similar to their-   String counterparts.+ * IO ByteString and are similar to their String counterparts. + * IO (String, IO (String, ExitCode)) returns a String read lazily+   and an IO action that, when evaluated, finishes up the process and+   results in its exit status.  This command returns immediately.++ * IO (IO (String, ExitCode)) sends stdout to stdout but returns+   immediately.  It forks off the child but does not wait for it to finish.+   You can use 'checkResults' to wait for the finish.+  * IO Int returns the exit code from a program directly.  If a signal    caused the command to be reaped, returns 128 + SIGNUM. @@ -297,21 +418,20 @@ instance RunResult (IO ()) where     run cmd = run cmd >>= checkResults -instance RunResult (IO (String, ProcessStatus)) where+instance RunResult (IO (String, ExitCode)) where     run cmd =-        do r <- fdInvoke cmd stdInput stdOutput [] nullChildFunc+        do (ochan, r) <- fdInvoke cmd Nothing (ChanHandle stdin)+           chanToHandle False ochan stdout            processResults r -instance RunResult (IO ProcessStatus) where-    run cmd = ((run cmd)::IO (String, ProcessStatus)) >>= return . snd+instance RunResult (IO ExitCode) where+    run cmd = ((run cmd)::IO (String, ExitCode)) >>= return . snd  instance RunResult (IO Int) where     run cmd = do rc <- run cmd                  case rc of-                   Exited (ExitSuccess) -> return 0-                   Exited (ExitFailure x) -> return x-                   Terminated x -> return (128 + (fromIntegral x))-                   Stopped x -> return (128 + (fromIntegral x))+                   ExitSuccess -> return 0+                   ExitFailure x -> return x  instance RunResult (IO Bool) where     run cmd = do rc <- run cmd@@ -322,95 +442,109 @@                  return (lines r)  instance RunResult (IO String) where-    run cmd = genericStringlikeResult hGetContents (\c -> evaluate (length c))+    run cmd = genericStringlikeResult chanAsString (\c -> evaluate (length c))               cmd  instance RunResult (IO BSL.ByteString) where-    run cmd = genericStringlikeResult BSL.hGetContents +    run cmd = genericStringlikeResult chanAsBSL               (\c -> evaluate (BSL.length c))               cmd  instance RunResult (IO BS.ByteString) where-    run cmd = genericStringlikeResult BS.hGetContents+    run cmd = genericStringlikeResult chanAsBS               (\c -> evaluate (BS.length c))               cmd +instance RunResult (IO (String, IO (String, ExitCode))) where+    run cmd = intermediateStringlikeResult chanAsString cmd++instance RunResult (IO (BSL.ByteString, IO (String, ExitCode))) where+    run cmd = intermediateStringlikeResult chanAsBSL cmd++instance RunResult (IO (BS.ByteString, IO (String, ExitCode))) where+    run cmd = intermediateStringlikeResult chanAsBS cmd++instance RunResult (IO (IO (String, ExitCode))) where+    run cmd = do (ochan, r) <- fdInvoke cmd Nothing (ChanHandle stdin)+                 chanToHandle False ochan stdout+                 return (processResults r)++intermediateStringlikeResult :: ShellCommand b =>+                                (Channel -> IO a)+                             -> b+                             -> IO (a, IO (String, ExitCode))+intermediateStringlikeResult chanfunc cmd =+        do (ochan, r) <- fdInvoke cmd Nothing (ChanHandle stdin)+           c <- chanfunc ochan+           return (c, processResults r)+ genericStringlikeResult :: ShellCommand b => -                           (Handle -> IO a)+                           (Channel -> IO a)                         -> (a -> IO c)                         -> b                          -> IO a-genericStringlikeResult hgetcontentsfunc evalfunc cmd =-        do (pread, pwrite) <- createPipe-           -- d $ "runS: new pipe endpoints: " ++ show [pread, pwrite]-           -- d "runS 1"-           r <- fdInvoke cmd stdInput pwrite [pread, pwrite] nullChildFunc-           -- d $ "runS 2 closing " ++ show pwrite-           closeFd pwrite-           -- d "runS 3"-           hread <- fdToHandle pread-           -- d "runS 4"-           c <- hgetcontentsfunc hread-           -- d "runS 5"+genericStringlikeResult chanfunc evalfunc cmd =+        do (c, r) <- intermediateStringlikeResult chanfunc cmd            evalfunc c            --evaluate (length c)            -- d "runS 6"-           hClose hread            -- d "runS 7"-           processResults r >>= checkResults+           r >>= checkResults            -- d "runS 8"            return c  {- | Evaluates the result codes and returns an overall status -}-processResults :: [InvokeResult] -> IO (String, ProcessStatus)+processResults :: [InvokeResult] -> IO (String, ExitCode) processResults r =     do rc <- mapM procresult r        case catMaybes rc of-         [] -> return (fst (last r), Exited (ExitSuccess))+         [] -> return (fst (last r), ExitSuccess)          x -> return (last x)-    where procresult :: InvokeResult -> IO (Maybe (String, ProcessStatus))+    where procresult :: InvokeResult -> IO (Maybe (String, ExitCode))           procresult (cmd, action) =               do rc <- action                  return $ case rc of-                   Exited (ExitSuccess) -> Nothing+                   ExitSuccess -> Nothing                    x -> Just (cmd, x)  {- | Evaluates result codes and raises an error for any bad ones it finds. -}-checkResults :: (String, ProcessStatus) -> IO ()+checkResults :: (String, ExitCode) -> IO () checkResults (cmd, ps) =        case ps of-         Exited (ExitSuccess) -> return ()-         Exited (ExitFailure x) ->+         ExitSuccess -> return ()+         ExitFailure x ->              fail $ cmd ++ ": exited with code " ++ show x+{- FIXME: generate these again           Terminated sig ->              fail $ cmd ++ ": terminated by signal " ++ show sig          Stopped sig ->              fail $ cmd ++ ": stopped by signal " ++ show sig+-}  {- | Handle an exception derived from a program exiting abnormally -}-tryEC :: IO a -> IO (Either ProcessStatus a)+tryEC :: IO a -> IO (Either ExitCode a) tryEC action =-    do r <- try action+    do r <-  Control.Exception.try action        case r of          Left ioe ->           if isUserError ioe then               case (ioeGetErrorString ioe =~~ pat) of                 Nothing -> ioError ioe -- not ours; re-raise it-                Just e -> return . Left . proc $ e+                Just e -> return . Left . procit $ e           else ioError ioe      -- not ours; re-raise it          Right result -> return (Right result)     where pat = ": exited with code [0-9]+$|: terminated by signal ([0-9]+)$|: stopped by signal [0-9]+"-          proc :: String -> ProcessStatus-          proc e-              | e =~ "^: exited" = Exited (ExitFailure (str2ec e))-              | e =~ "^: terminated by signal" = Terminated (str2ec e)-              | e =~ "^: stopped by signal" = Stopped (str2ec e)+          procit :: String -> ExitCode+          procit e+              | e =~ "^: exited" = ExitFailure (str2ec e)+--              | e =~ "^: terminated by signal" = Terminated (str2ec e)+--              | e =~ "^: stopped by signal" = Stopped (str2ec e)               | otherwise = error "Internal error in tryEC"           str2ec e =               read (e =~ "[0-9]+$")  {- | Catch an exception derived from a program exiting abnormally -}-catchEC :: IO a -> (ProcessStatus -> IO a) -> IO a+catchEC :: IO a -> (ExitCode -> IO a) -> IO a catchEC action handler =     do r <- tryEC action        case r of@@ -442,3 +576,105 @@     do r <- run cmd        when (r == []) $ fail $ "runSL: no output received from " ++ show cmd        return (rstrip . head $ r)+++{- | Convenience function to wrap a child thread.  Kicks off the thread, handles+running the code, traps execptions, the works.++Note that if func is lazy, such as a getContents sort of thing,+the exception may go uncaught here.++NOTE: expects func to be lazy!+ -}+runInHandler :: String           -- ^ Description of this function+            -> (IO Channel)     -- ^ The action to run in the thread+            -> IO (Channel, [InvokeResult])+runInHandler descrip func =+    catch (realfunc) (exchandler)+    where realfunc = do r <- func+                        return (r, [(descrip, return ExitSuccess)])+          exchandler :: SomeException -> IO (Channel, [InvokeResult])+          exchandler e = do em $ "runInHandler/" ++ descrip ++ ": " ++ show e+                            return (ChanString "", [(descrip, return (ExitFailure 1))])+++------------------------------------------------------------+-- Environment+------------------------------------------------------------+{- | An environment variable filter function.++This is a low-level interface; see 'setenv' and 'unsetenv' for more convenient+interfaces. -}+type EnvironFilter = [(String, String)] -> [(String, String)]++instance Show EnvironFilter where+    show _ = "EnvironFilter"+++{- | A command that carries environment variable information with it.++This is a low-level interface; see 'setenv' and 'unsetenv' for more+convenient interfaces. -}+data EnvironCommand a = (ShellCommand a) => EnvironCommand EnvironFilter a++deriving instance Show (EnvironCommand a)++instance (ShellCommand a) => ShellCommand (EnvironCommand a) where+    fdInvoke (EnvironCommand efilter cmd) Nothing ichan =+        do -- No incoming environment; initialize from system default.+           e <- getEnvironment+           fdInvoke cmd (Just (efilter e)) ichan+    fdInvoke (EnvironCommand efilter cmd) (Just ienv) ichan =+        fdInvoke cmd (Just (efilter ienv)) ichan++{- | Sets an environment variable, replacing an existing one if it exists.++Here's a sample ghci session to illustrate.  First, let's see the defaults for+some variables:++> Prelude HSH> runIO $ "echo $TERM, $LANG"+> xterm, en_US.UTF-8++Now, let's set one:++> Prelude HSH> runIO $ setenv [("TERM", "foo")] $ "echo $TERM, $LANG"+> foo, en_US.UTF-8++Or two:++> Prelude HSH> runIO $ setenv [("TERM", "foo")] $ setenv [("LANG", "de_DE.UTF-8")] $ "echo $TERM, $LANG"+> foo, de_DE.UTF-8++We could also do it easier, like this:++> Prelude HSH> runIO $ setenv [("TERM", "foo"), ("LANG", "de_DE.UTF-8")] $ "echo $TERM, $LANG"+> foo, de_DE.UTF-8++It can be combined with unsetenv:++> Prelude HSH> runIO $ setenv [("TERM", "foo")] $ unsetenv ["LANG"] $ "echo $TERM, $LANG"+> foo,++And used with pipes:++> Prelude HSH> runIO $ setenv [("TERM", "foo")] $ "echo $TERM, $LANG" -|- "tr a-z A-Z"+> FOO, EN_US.UTF-8++See also 'unsetenv'.+-}+setenv :: (ShellCommand cmd) => [(String, String)] -> cmd -> EnvironCommand cmd+setenv items cmd =+    EnvironCommand efilter cmd+    where efilter ienv = foldr efilter' ienv items+          efilter' (key, val) ienv = +              (key, val) : (filter (\(k, _) -> k /= key) ienv)++{- | Removes an environment variable if it exists; does nothing otherwise.++See also 'setenv', which has a more extensive example.+-}+unsetenv :: (ShellCommand cmd) => [String] -> cmd -> EnvironCommand cmd+unsetenv keys cmd =+    EnvironCommand efilter cmd+    where efilter ienv = foldr efilter' ienv keys+          efilter' key = filter (\(k, _) -> k /= key)
HSH/ShellEquivs.hs view
@@ -1,18 +1,19 @@+{-# LANGUAGE ScopedTypeVariables #-} {- Shell Equivalents-Copyright (C) 2004-2008 John Goerzen <jgoerzen@complete.org>+Copyright (C) 2004-2009 John Goerzen <jgoerzen@complete.org> Please see the COPYRIGHT file -}  {- |    Module     : HSH.ShellEquivs-   Copyright  : Copyright (C) 2008 John Goerzen+   Copyright  : Copyright (C) 2009 John Goerzen    License    : GNU LGPL, version 2.1 or above     Maintainer : John Goerzen <jgoerzen@complete.org>    Stability  : provisional    Portability: portable -Copyright (c) 2006-2008 John Goerzen, jgoerzen\@complete.org+Copyright (c) 2006-2009 John Goerzen, jgoerzen\@complete.org  This module provides shell-like commands.  Most, but not all, are designed to be used directly as part of a HSH pipeline.  All may be used outside@@ -20,21 +21,32 @@  -} +{-# LANGUAGE ScopedTypeVariables #-}++#if !(defined(mingw32_HOST_OS) || defined(mingw32_TARGET_OS) || defined(__MINGW32__))+#define __HSH_POSIX__+#else+#define __HSH_WINDOWS__+#endif+ module HSH.ShellEquivs(                        abspath,                        appendTo,                        basename,                        bracketCD,                        catFrom,-                       catFromBS,+                       catBytes,+                       catBytesFrom,                        catTo,-                       catToBS,+#ifdef __HSH_POSIX__+                       catToFIFO,+#endif                        cd,                        cut,                        cutR,                        dirname,+                       discard,                        echo,-                       echoBS,                        exit,                        glob,                        grep,@@ -47,19 +59,25 @@                        mkdir,                        numberLines,                        pwd,+#ifdef __HSH_POSIX__                        readlink,                        readlinkabs,+#endif                        rev,                        revW,+                       HSH.Command.setenv,                        space,                        unspace,                        tac,                        tee,-                       teeBS,+#ifdef __HSH_POSIX__+                       teeFIFO,+#endif                        tr,                        trd,                        wcW,                        wcL,+                       HSH.Command.unsetenv,                        uniq,                       ) where @@ -68,19 +86,38 @@ import Text.Regex (matchRegex, mkRegex) import Text.Printf (printf) import Control.Monad (foldM)-import System.Directory hiding (createDirectory)+import System.Directory hiding (createDirectory, isSymbolicLink)+import qualified Control.Exception as E  -- import System.FilePath (splitPath)++#ifdef __HSH_POSIX__ import System.Posix.Files (getFileStatus, isSymbolicLink, readSymbolicLink) import System.Posix.User (getEffectiveUserName, getUserEntryForName, homeDirectory) import System.Posix.Directory (createDirectory) import System.Posix.Types (FileMode())+import System.Posix.IO+import System.Posix.Error+#endif+ import System.Path (absNormPath, bracketCWD) import System.Exit+import System.IO+import System.Process+import qualified System.Directory as SD import qualified System.Path.Glob as Glob (glob) import qualified Data.ByteString.Lazy as BSL+import qualified Data.ByteString as BS+import System.IO.Unsafe(unsafeInterleaveIO)+import HSH.Channel+import HSH.Command(setenv, unsetenv)  {- | Return the absolute path of the arg.  Raises an error if the-computation is impossible. -}+computation is impossible. This is a thin wrapper around+System.Path.absNormPath.  Unix/Linux users note:+System.Path.absNormPath is known to produce odd results when+a tilde expansion is requested; you might prefer 'glob' to this+function if you know your input is free of wildcards. See+https://github.com/jgoerzen/hsh/issues/7 for details. -} abspath :: FilePath -> IO FilePath abspath inp =     do p <- pwd@@ -108,46 +145,91 @@ {- | Load the specified files and display them, one at a time.  The special file @-@ means to display the input.  If it is not given,-no input is read.+no input is processed at all. -Unlike the shell cat, @-@ may be given twice.  However, if it is, you-will be forcing Haskell to buffer the input.+@-@ may be given a maximum of one time. -Note: buffering behavior here is untested. +See also 'catBytes' . -}+catFrom :: [FilePath] -> Channel -> IO Channel+catFrom fplist ichan =+    do r <- foldM foldfunc BSL.empty fplist+       return (toChannel r)+    where foldfunc accum fp =+                  case fp of+                    "-" -> do c <- chanAsBSL ichan+                              return (BSL.append accum c)+                    fn -> do c <- BSL.readFile fn+                             return (BSL.append accum c) -See also 'catFromBS'. -}-catFrom :: [FilePath] -> String -> IO String-catFrom = genericCatFrom readFile (++) ""+{- | Copy data from input to output, optionally with a fixed+maximum size, in bytes.  Processes data using ByteStrings internally,+so be aware of any possible UTF-8 conversions. -{- | Lazy ByteString version of 'catFrom'.  This may have performance-benefits. -}-catFromBS :: [FilePath] -> BSL.ByteString -> IO BSL.ByteString-catFromBS = genericCatFrom BSL.readFile BSL.append BSL.empty+You may wish to use @hSetBuffering h (BlockBuffering Nothing)@ prior to calling+this function for optimal performance. -genericCatFrom :: (FilePath -> IO a) -> (a -> a -> a) -> a ->  [FilePath] -> a -> IO a-genericCatFrom readfilefunc appendfunc empty fplist inp =-    do r <- foldM foldfunc empty fplist-       return r-    where foldfunc accum fp =-                  case fp of-                    "-" -> return (appendfunc accum inp)-                    fn -> do c <- readfilefunc fn-                             return (appendfunc accum c)+See also 'catFrom', 'catBytesFrom' -}+catBytes :: (Maybe Integer)    -- ^ Maximum amount of data to transfer+          -> Channel             -- ^ Handle for input+          -> IO Channel+catBytes count hr = catBytesFrom hr count hr +{- | Generic version of 'catBytes'; reads data from specified Channel, and+ignores stdin.+-}++catBytesFrom :: Channel          -- ^ Handle to read from+             -> (Maybe Integer)  -- ^ Maximum amount of data to transfer+             -> Channel          -- ^ Handle for input (ignored)+             -> IO Channel+catBytesFrom (ChanHandle hr) count cignore =+    case count of+         Nothing -> return (ChanHandle hr)+         Just m -> do c <- BSL.hGet hr (fromIntegral m)+                      return (ChanBSL c)+catBytesFrom cinput count cignore =+    case count of+      Nothing -> return cinput+      Just m -> do r <- chanAsBSL cinput+                   return (ChanBSL (BSL.take (fromIntegral m) r))+ {- | Takes input, writes it to the specified file, and does not pass it on.-     The return value is the empty string.  See also 'catToBS', 'tee'.  -}-catTo :: FilePath -> String -> IO String-catTo fp inp =-    do writeFile fp inp-       return ""+     The return value is the empty string.  See also 'catToBS', +     'catToFIFO' -}+catTo :: FilePath -> Channel -> IO Channel+catTo fp ichan =+    do ofile <- openFile fp WriteMode+       chanToHandle True ichan ofile+       return (ChanString "") -{- | Like 'catTo', but operates in a lazy ByteString.  This could be a-performance benefit. -}-catToBS :: FilePath -> BSL.ByteString -> IO BSL.ByteString-catToBS fp inp =-    do BSL.writeFile fp inp-       return (BSL.empty)+#ifdef __HSH_POSIX__ +{- | Like 'catTo', but opens the destination in ReadWriteMode instead of+ReadOnlyMode.  Due to an oddity of the Haskell IO system, this is required+when writing to a named pipe (FIFO) even if you will never read from it.++This call will BLOCK all threads on open until a reader connects.++This is provided in addition to 'catTo' because you may want to cat to+something that you do not have permission to read from.++This function is only available on POSIX platforms.++See also 'catTo' -}+catToFIFO :: FilePath -> Channel -> IO Channel+catToFIFO fp ichan =+    do h <- fifoOpen fp+       chanToHandle True ichan h+       return (ChanString "")++fifoOpen :: FilePath -> IO Handle+fifoOpen fp = +    do fd <- throwErrnoPathIf (< 0) "HSH fifoOpen" fp $ +             openFd fp WriteOnly Nothing defaultFileFlags+       fdToHandle fd++#endif+ {- | Like 'catTo', but appends to the file. -} appendTo :: FilePath -> String -> IO String appendTo fp inp =@@ -172,6 +254,13 @@ cut :: Integer -> Char -> String -> String cut pos = cutR [pos] +{- | Read all input and produce no output.  Discards input completely. -}+discard :: Channel -> IO Channel+discard inh =+    do c <- chanAsBSL inh+       E.evaluate (BSL.length c)+       return (ChanString "")+ {- | Split a list by a given character and select ranges of the resultant lists.  > cutR [2..4] ' ' "foo bar baz quux foobar" -> "baz quux foobar"@@ -188,13 +277,11 @@  The input to this function is never read. -See also 'echoBS'. -}-echo :: String -> String -> String-echo inp _ = inp+You can pass this thing a String, a ByteString, or even a Handle. -{- | ByteString.Lazy version of 'echo'. -}-echoBS :: BSL.ByteString -> BSL.ByteString -> BSL.ByteString-echoBS inp _ = inp+See also 'echoBS'. -}+echo :: Channelizable a => a -> Channel -> IO Channel+echo inp _ = return . toChannel $ inp  {- | Search for the regexp in the lines.  Return those that match. -} egrep :: String -> [String] -> [String]@@ -235,8 +322,9 @@ Non-tilde expansion is done by the MissingH module System.Path.Glob. -} glob :: FilePath -> IO [FilePath] glob inp@('~':remainder) =-    catch expanduser (\_ -> Glob.glob inp)+    E.catch expanduser (\(e::E.SomeException) -> Glob.glob rest)     where (username, rest) = span (/= '/') remainder+#ifdef __HSH_POSIX__           expanduser =               do lookupuser <-                      if username /= ""@@ -244,6 +332,9 @@                         else getEffectiveUserName                  ue <- getUserEntryForName lookupuser                  Glob.glob (homeDirectory ue ++ rest)+#else+          expanduser = fail "non-posix; will be caught above"+#endif glob x = Glob.glob x  {- | Search for the string in the lines.  Return those that match.@@ -262,11 +353,18 @@ joinLines :: [String] -> [String] joinLines = return . concat +#ifdef __HSH_POSIX__ {- | Creates the given directory.  A value of 0o755 for mode would be typical. -An alias for System.Posix.Directory.createDirectory. -}+An alias for System.Posix.Directory.createDirectory.++The second argument will be ignored on non-POSIX systems. -} mkdir :: FilePath -> FileMode -> IO () mkdir = createDirectory+#else+mkdir :: FilePath -> a -> IO ()+mkdir fp _ = SD.createDirectory fp+#endif  {- | Number each line of a file -} numberLines :: [String] -> [String]@@ -276,9 +374,12 @@ pwd :: IO FilePath pwd = getCurrentDirectory +#ifdef __HSH_POSIX__ {- | Return the destination that the given symlink points to. -An alias for System.Posix.Files.readSymbolicLink -}+An alias for System.Posix.Files.readSymbolicLink++This function is only available on POSIX platforms. -} readlink :: FilePath -> IO FilePath readlink fp =     do issym <- (getFileStatus fp >>= return . isSymbolicLink)@@ -286,7 +387,9 @@            then readSymbolicLink fp            else return fp -{- | As 'readlink', but turns the result into an absolute path. -}+{- | As 'readlink', but turns the result into an absolute path.++This function is only available on POSIX platforms. -} readlinkabs :: FilePath -> IO FilePath readlinkabs inp =        do issym <- (getFileStatus inp >>= return . isSymbolicLink)@@ -297,6 +400,7 @@                                   show (dirname inp)                        Just x -> return x              else abspath inp+#endif  {- | Reverse characters on each line (rev) -} rev, revW :: [String] -> [String]@@ -316,18 +420,39 @@ tac = reverse  {- | Takes input, writes it to all the specified files, and passes it on.-This function buffers the input.+This function does /NOT/ buffer input. -See also 'teeBS', 'catFrom'. -}-tee :: [FilePath] -> String -> IO String-tee [] inp = return inp-tee (x:xs) inp = writeFile x inp >> tee xs inp+See also 'catFrom'. -}+tee :: [FilePath] -> Channel -> IO Channel+tee fplist inp = teeBSGeneric (\fp -> openFile fp WriteMode) fplist inp -{- | Lazy ByteString version of 'tee'. -}-teeBS :: [FilePath] -> BSL.ByteString -> IO BSL.ByteString-teeBS [] inp = return inp-teeBS (x:xs) inp = BSL.writeFile x inp >> teeBS xs inp+#ifdef __HSH_POSIX__+{- | FIFO-safe version of 'tee'. +This call will BLOCK all threads on open until a reader connects.++This function is only available on POSIX platforms. -}+teeFIFO :: [FilePath] -> Channel -> IO Channel+teeFIFO fplist inp = teeBSGeneric fifoOpen fplist inp+#endif++teeBSGeneric :: (FilePath -> IO Handle) +             -> [FilePath] +             -> Channel -> IO Channel+teeBSGeneric openfunc fplist ichan =+    do handles <- mapM openfunc fplist+       inp <- chanAsBSL ichan+       resultChunks <- hProcChunks handles (BSL.toChunks inp)+       return (ChanBSL $ BSL.fromChunks resultChunks)+    where hProcChunks :: [Handle] -> [BS.ByteString] -> IO [BS.ByteString]+          hProcChunks handles chunks = unsafeInterleaveIO $+              case chunks of+                [] -> do mapM_ hClose handles+                         return [BS.empty]+                (x:xs) -> do mapM_ (\h -> BS.hPutStr h x) handles+                             remainder <- hProcChunks handles xs+                             return (x : remainder)+     {- | Translate a character x to y, like:  >tr 'e' 'f'@@ -349,23 +474,28 @@ uniq :: String -> String uniq = unlines . nub . lines -{- | Double space a file -}-space, unspace :: [String] -> [String]+{- | Double space a file; add an empty line between each line. -}+space :: [String] -> [String] space = intersperse "" -{- | Inverse of double space; drop empty lines -}+{- | Inverse of double 'space'; drop all empty lines. -}+unspace :: [String] -> [String] unspace = filter (not . null) -{- | Convert a string to all upper or lower case -}-lower, upper :: String -> String+{- | Convert a string to all lower case -}+lower :: String -> String lower = map toLower++{- | Convert a string to all upper case -}+upper :: String -> String upper = map toUpper -{- | Count number of lines.  wc -l -}-wcL, wcW :: [String] -> [String]+{- | Count number of lines.  Like wc -l -}+wcL :: [String] -> [String] wcL inp = [show (genericLength inp :: Integer)]  {- | Count number of words in a file (like wc -w) -}+wcW :: [String] -> [String] wcW inp = [show ((genericLength $ words $ unlines inp) :: Integer)]  {- Utility function.
+ testsrc/runtests.hs view
@@ -0,0 +1,43 @@+{- +Copyright (C) 2004-2007 John Goerzen <jgoerzen@complete.org>+Please see the COPYRIGHT file+-}++module Main where ++import Test.HUnit+import Tests+import TestUtils+import System.IO+import Text.Printf++-- main = do runTestTT tests+main = do hSetBuffering stdout LineBuffering+          hSetBuffering stderr LineBuffering+          (c, _) <- performTest reportStart reportError reportFailure () tests+          printf "\n TESTS COMPLETE\n"+          printf "Cases: %d, Tried: %d, Errors: %d, Failures: %d\n"+                 (cases c) (tried c) (errors c) (failures c)++reportStart :: ReportStart ()+reportStart st () =+    do printf "[%-4d/%-4d] START   %s\n" (tried . counts $ st)+               (cases . counts $ st)+               (showPath . path $ st)+       hFlush stdout+       return ()++reportError :: ReportProblem ()+reportError = problem "ERROR  "++reportFailure :: ReportProblem ()+reportFailure = problem "FAILURE"++problem :: String -> ReportProblem ()+problem ptype ptext st () =+    do printf "[%-4d/%-4d] %s\n       %s\n" (tried . counts $ st)+           (cases . counts $ st)+           (showPath . path $ st) ptext+       hFlush stdout+       return ()+