Pipe 1.0 → 2.0
raw patch · 3 files changed
+73/−440 lines, 3 filesdep −bytestringdep −containersdep −unixdep ~processPVP ok
version bump matches the API change (PVP)
Dependencies removed: bytestring, containers, unix
Dependency ranges changed: process
API changes (from Hackage documentation)
- System.Process.Pipe: bufferSize :: Int
- System.Process.Pipe: class Sink a
- System.Process.Pipe: class Tap a
- System.Process.Pipe: exhausted :: (Tap a) => a -> IO Bool
- System.Process.Pipe: flowIn :: (Sink a) => a -> Ptr Word8 -> Int -> IO a
- System.Process.Pipe: flowOut :: (Tap a) => a -> Ptr Word8 -> Int -> IO (a, Int)
- System.Process.Pipe: stringToWord8 :: String -> [Word8]
- System.Process.Pipe: word8ToString :: [Word8] -> String
+ System.Process.Pipe: filePipe' :: [(FilePath, [String])] -> FilePath -> FilePath -> IO ()
+ System.Process.Pipe: handlePipe :: FilePath -> [(FilePath, [String])] -> Handle -> Handle -> IO ()
- System.Process.Pipe: filePipe :: [(FilePath, [String])] -> FilePath -> FilePath -> IO ()
+ System.Process.Pipe: filePipe :: FilePath -> [(FilePath, [String])] -> FilePath -> FilePath -> IO ()
- System.Process.Pipe: pipe :: (Tap t, Sink s) => FilePath -> [(FilePath, [String])] -> t -> s -> IO (t, s)
+ System.Process.Pipe: pipe :: (Handle -> a -> IO ()) -> (Handle -> IO b) -> FilePath -> [(FilePath, [String])] -> a -> IO b
- System.Process.Pipe: pipe' :: (Tap t, Sink s) => [(FilePath, [String])] -> t -> s -> IO (t, s)
+ System.Process.Pipe: pipe' :: (Handle -> a -> IO ()) -> (Handle -> IO b) -> [(FilePath, [String])] -> a -> IO b
Files
- Pipe.cabal +4/−10
- System/Process/Pipe.hs +69/−318
- System/Process/Pipe/Plumbing.hs +0/−112
Pipe.cabal view
@@ -1,7 +1,7 @@ Cabal-Version: >= 1.2 Name: Pipe -Version: 1.0 +Version: 2.0 Homepage: http://iki.fi/matti.niemenmaa/pipe/ Synopsis: Process piping library Category: System @@ -16,13 +16,7 @@ Build-Type: Simple Library - Build-Depends: base >= 3 && < 4, - process >= 1.0.1 && < 2.0, - filepath >= 1.1 && < 2, - bytestring >= 0.9 && < 1, - containers >= 0.1 && < 1 + Build-Depends: base >= 3 && < 4, + process >= 1.0.1 && < 2, + filepath >= 1.1 && < 2 Exposed-Modules: System.Process.Pipe - Other-Modules: System.Process.Pipe.Plumbing - - if !os(windows) - Build-Depends: unix >= 2.3 && < 3
System/Process/Pipe.hs view
@@ -1,6 +1,5 @@ -- File created: 2008-02-11 12:55:34 {-# LANGUAGE CPP #-} -{-# OPTIONS_GHC -fglasgow-exts -frewrite-rules #-} -- for the rewrite rule ------------------------------------------------------------------------------- -- | -- Module : System.Process.Pipe @@ -13,11 +12,12 @@ -- -- Operations for piping data through multiple processes. -- --- 'pipe' is the most general function, with 'filePipe' and 'pipe\'' provided --- for convenience purposes. For the common case of piping between 'String's, --- the 'pipeString' wrapper and the 'word8ToString' and 'stringToWord8' helpers --- are included. +-- 'pipe' is the most general function, with 'pipe\'' and 'pipeString' provided +-- for convenience purposes. -- +-- 'handlePipe', 'filePipe', and 'filePipe\'' are for the common file-to-file +-- case and behave somewhat differently. +-- -- Whenever specifying a path to a process, explicitly specifying the current -- directory is recommended for portability. That is: use \".\/foo\" instead of -- \"foo\", for instance. @@ -26,46 +26,25 @@ -- invocation fails. ------------------------------------------------------------------------------- module System.Process.Pipe - ( filePipe - , Tap(..), Sink(..), bufferSize - , pipe, pipe', pipeString - , word8ToString, stringToWord8 + ( pipe, pipe' + , pipeString + , handlePipe + , filePipe, filePipe' ) where -import Control.Monad (forM) -import Data.Char (chr, ord) +import Control.Concurrent (forkIO) +import Control.Monad (mplus) import Data.Maybe (fromJust) -import Data.Word (Word8) -import Foreign.Marshal.Alloc (allocaBytes) -import Foreign.Marshal.Array (peekArray, pokeArray) -import Foreign.Ptr (Ptr) import System.FilePath (dropFileName) import System.IO ( withBinaryFile, IOMode (ReadMode, WriteMode) - , Handle, hClose - , hGetContents, hPutStr) + , Handle, hGetContents, hPutStr) import System.Process ( CreateProcess(..), createProcess , CmdSpec (RawCommand) , StdStream (CreatePipe, Inherit, UseHandle) , ProcessHandle, waitForProcess) -import System.Process.Pipe.Plumbing - -#if mingw32_HOST_OS -import Data.ByteString (ByteString) -import qualified Data.ByteString as BS -import Foreign.Ptr (castPtr) -#else -import System.IO (hIsOpen, hGetBuf, hGetBufNonBlocking, hPutBuf) -import System.IO.Error (isFullError) -import System.Posix.Signals ( Signal, openEndedPipe - , Handler (Ignore), installHandler) -#endif - -type Proc = ( Handle, Handle, ProcessHandle) -type MProc = (Maybe Handle, Maybe Handle, ProcessHandle) - createProc :: FilePath -> StdStream -> StdStream -> (FilePath,[String]) - -> IO MProc + -> IO (Maybe Handle, Maybe Handle, ProcessHandle) createProc wdir inp out (p,args) = do let proc = CreateProcess { cmdspec = undefined @@ -84,302 +63,74 @@ #endif return (i,o,pid) --- | Pipes the contents of the first file to the second file through all the --- programs named. --- --- The working directory used is the directory component of the path to the --- first file. -filePipe :: [(FilePath,[String])] -> FilePath -> FilePath -> IO () -filePipe progs infile outfile = do - withBinaryFile outfile WriteMode $ \outhdl -> - withBinaryFile infile ReadMode $ \inhdl -> - pipe (dropFileName infile) progs inhdl outhdl - return () - --- | Pipes data from the 'Tap' to the 'Sink' through all the commands named, in --- the given working directory. --- --- Be careful! All IO is at the byte level: this means that piping even a --- String such as \"foo\" will result in the raw UTF-32 moving: the bytes (in --- my case; I believe this is implementation-dependent) in question are not the --- ASCII @[102, 111, 111]@ but rather @[102, 0, 0, 0, 111, 0, 0, 0, 111, 0, 0, --- 0]@. --- --- Note to Windows users: since 'hGetBufNonBlocking' doesn't work on Windows --- (it blocks despite its name, see --- <http://hackage.haskell.org/trac/ghc/ticket/806>), this pipeline uses a --- non-constant amount of space. The amount used is linear in the amount of --- data used at any point in the pipeline. So if you want to pipe 20 gibioctets --- of data to a program, you better make sure you have at least said amount of --- memory available. (In fact, ByteStrings are used, and their documentation --- suggests that you might want twice that, just in case.) --- --- In addition, the 'Tap' and 'Sink' classes are meant for the POSIX code: --- having to move data through the 'Ptr' 'Word8' types, 'bufferSize' bytes at a --- time, results in extra complexity. --- --- If you want to do something about the above, ideally fix the GHC ticket --- (probably nontrivial) and let me know so that I can activate the better code --- for Windows as well. Alternatively, feel free to code an implementation of --- this which works on Windows. -pipe :: (Tap t, Sink s) => FilePath -> [(FilePath,[String])] -> t -> s - -> IO (t,s) -pipe wdir progs otap osink = do - - let cp = createProc wdir CreatePipe CreatePipe - - allocaBytes bufferSize $ \buf -> do - ps <- forM progs $ \pr -> do - (i,o,pid) <- cp pr - return (fromJust i, fromJust o, pid) - --- See 'pipeline' comment below for why this needs to be done differently. -#if mingw32_HOST_OS - -- Gather up all data from the tap until it's exhausted. - let loop tap s = do - exh <- exhausted tap - if exh - then return (tap,s) - else do - (tap',sz) <- flowOut tap buf bufferSize - xs <- peekArray sz (castPtr buf) - loop tap' (s `BS.append` BS.pack xs) - - (tap, s) <- loop otap BS.empty - sink <- pipeline osink buf ps s - return (tap,sink) -#else - let loop :: (Sink s, Tap t) => t -> s -> IO (t,s) - loop tap sink = do - exh <- exhausted tap - if exh - then return (tap,sink) - else do - (tap' , sz) <- flowOut tap buf bufferSize - (sink', status) <- pipeline sink ps buf sz +pipeline :: FilePath -> StdStream -> StdStream -> [(FilePath,[String])] + -> IO (Maybe Handle, Maybe Handle, [ProcessHandle]) - exh' <- exhausted tap' - if isNeed status && exh' - then do - -- The first process wants more input, but there's no - -- more to give. Hence what we do is enter the final - -- pipeline: have the process close its stdin and deal - -- with any leftover output. - (sink'',_) <- finalPipeline sink' ps buf - return (tap',sink'') - else - loop tap' sink' - loop otap osink -#endif +pipeline _ _ _ [] = ioError.userError$ "Pipe :: null pipeline" +pipeline wdir inp out progs = f [] Nothing inp progs + where + f pids firstI i [p] = do + (i',o,pid) <- createProc wdir i out p + return (firstI `mplus` i', o, reverse (pid:pids)) -#if mingw32_HOST_OS + f pids firstI i (p:ps) = do + (i',o,pid) <- createProc wdir i CreatePipe p + f (pid:pids) (firstI `mplus` i') (UseHandle . fromJust $ o) ps --- hGetBufNonBlocking doesn't work on Windows (see --- http://hackage.haskell.org/trac/ghc/ticket/806). I can't think of a way of --- doing a robust constant-space pipeline without it. Hence we use this silly --- implementation instead, which is crap but does the job. (This is how --- pipelines worked in Windows pre-NT anyway, AFAIK. (At least in DOS.)) +-- | Pipes the input, using the given writer and reader functions, through all +-- the commands named, in the given working directory. Returns the result. -- --- Basically we just wait for the previous command to complete, gathering up --- all its output, and then give it all at once to the next one. And yes, this --- means that if some process outputs (or the input file contains) an infinite --- amount of data or more than fits in memory, you're screwed. -pipeline :: Sink s => s -> Ptr Word8 -> [Proc] -> ByteString -> IO s -pipeline sink buf ((i,o,p):ps) dat = do - BS.hPut i dat - hClose i - dat' <- BS.hGetContents o - waitForProcess p - pipeline sink buf ps dat' - -pipeline osink buf [] dat = do - let loop sink s = - if BS.null s - then return sink - else do - let (xs,ys) = BS.splitAt bufferSize s - pokeArray (castPtr buf) (BS.unpack xs) - sink' <- flowIn sink buf (BS.length xs) - loop sink' ys - loop osink dat - -#else - -data Need = Done | Need - -isNeed :: Need -> Bool -isNeed Need = True -isNeed _ = False - --- This matches more than just EPIPE but it's the best we can do. -onEPIPE :: IO a -> IO a -> IO a -a `onEPIPE` b = a `catch` \e -> if isFullError e then b else ioError e - -pipeline, shoveDown :: Sink s => s -> [Proc] -> Ptr Word8 -> Int -> IO (s,Need) -pipeline sink [] buf sz = toSink sink buf sz -pipeline sink ps@((inp, out, _):_) buf sz = do - -- Put the given data to the stdin of this process. - -- - -- We have to be careful with SIGPIPEs here. If hPutBuf fails, that's - -- because the input pipe is either closed or broken. We try to close it, - -- and if that fails then we know the pipe is broken. - -- - -- Simply piping infinite output to a process which takes only a bit of - -- input ("yes | head", for instance) triggers both of the onEPIPE cases, as - -- well as the one in finalize. - brokenPipe <- - withIgnoringSignal openEndedPipe $ - (hPutBuf inp buf sz >> return False) - `onEPIPE` (hClose inp >> return False) - `onEPIPE` return True - - -- We can't block here, lest it be the case where the program has output - -- e.g. bufferSize-1 bytes, but is waiting on input. If we block here then - -- we're waiting for more output while the process is waiting for more - -- input---deadlock! - sz' <- hGetBufNonBlocking out buf bufferSize - if sz' == 0 - -- We got no output from this process. If the process's stdin is open, - -- we request more input for it from the process above us. Otherwise - -- we block, waiting for the process to complete its computation or - -- whatever it's doing. We can do this safely now since the stdin is - -- closed: there's no way to reach the deadlock outlined above. - then do - wantsMore <- hIsOpen inp - if wantsMore && not brokenPipe - then return (sink, Need) - else blockingPipeline sink ps buf - else shoveDown sink ps buf sz' - -shoveDown sink [] buf sz = toSink sink buf sz -shoveDown sink ps@(p:rest) buf sz = do - -- Pipe the output from the process above to the next process in the - -- pipeline. - (sink', below) <- pipeline sink rest buf sz - case below of - -- The process below wants more data: go back and try a nonblocking get - -- from this process. - Need -> pipeline sink' ps buf 0 - -- The process below us says it wants nothing more ever again. Since it - -- wants nothing from us, we have nothing to do either: finish up here - -- and bubble the information up. - Done -> finalize p >> return (sink', Done) - -blockingPipeline, - finalPipeline :: Sink s => s -> [Proc] -> Ptr Word8 -> IO (s, Need) -blockingPipeline sink [] buf = toSink sink buf 0 -blockingPipeline sink ps@(p@(_,out,_):rest) buf = do - sz <- hGetBuf out buf bufferSize - if sz == 0 - -- The blocking call returned 0: this means we've hit EOF, i.e. the - -- process is done and will no longer output anything. Go down the - -- pipeline, closing the stdin of the process below us and pushing any - -- last output further down. After that, finish up here and tell the ones - -- above to do the same. - then do - (sink', _) <- finalPipeline sink rest buf - finalize p - return (sink', Done) - else shoveDown sink ps buf sz - -finalPipeline sink [] buf = toSink sink buf 0 -finalPipeline sink ps@((inp,_,_):_) buf = do - hClose inp - -- Since the stdin is closed, we can jump straight to the blocking version - -- of the pipeline: while we could go by way of the non-blocking one there's - -- no need to do so. - blockingPipeline sink ps buf - --- Some data found its way all the way down the pipeline, so we put it in the --- output handle and let the processes know that we're ready for more. +-- An exception is thrown if the list of programs is empty. -- --- The special case for 0, while handled in hPutBuf, is my little hint to the --- optimizer that it should inline these calls where the 0 is given explicitly --- above. -toSink :: Sink s => s -> Ptr Word8 -> Int -> IO (s, Need) -toSink sink _ 0 = return (sink , Need) -toSink sink buf sz = flowIn sink buf sz >>= \sink' -> return (sink', Need) - -finalize :: Proc -> IO () -finalize (i,o,p) = do - -- Once again the stdin pipe may be broken so we sidestep a possible - -- SIGPIPE. - withIgnoringSignal openEndedPipe (hClose i `onEPIPE` return ()) - - -- We close the stdout as soon as we no longer need to read anything from - -- it, so that we don't have to wait for the process to finish outputting. - -- With infinite-output processes (and possibly even just with buffered - -- output) we'd be stuck forever in waitForProcess if we don't do this. - -- - -- This is also the reason why withIgnoringSignal openEndedPipe isn't - -- wrapped around this whole thing already in the pipe function: we want the - -- processes in the pipeline to handle (or die of) SIGPIPE (which this - -- hClose causes on their end) by themselves. - hClose o - waitForProcess p - return () - -withIgnoringSignal :: Signal -> IO a -> IO a -withIgnoringSignal sig mx = do - old <- installHandler sig Ignore Nothing - x <- mx - installHandler sig old Nothing - return x - -#endif - -{-# RULES "pipe->handlePipe" pipe = handlePipe #-} - --- Smarter way of piping Handle-to-Handle -handlePipe :: FilePath -> [(FilePath,[String])] -> Handle -> Handle - -> IO (Handle, Handle) - -handlePipe _ [] inhdl outhdl = do - hGetContents inhdl >>= hPutStr outhdl - return (inhdl, outhdl) - -handlePipe wdir (p:ps) inhdl outhdl = do - let cp = createProc wdir - - f pids out [] = return (out, pids) - f pids out (pr:prs) = do - (i,_,pid) <- cp CreatePipe out pr - f (pid:pids) (UseHandle . fromJust $ i) prs - - (inp, pids) <- f [] (UseHandle outhdl) (reverse ps) - (_,_,pid) <- cp (UseHandle inhdl) inp p +-- The writer function is called in a 'forkIO'\'d thread, allowing this to be +-- lazy. +pipe :: (Handle -> a -> IO ()) -> (Handle -> IO b) + -> FilePath -> [(FilePath,[String])] + -> a -> IO b - mapM_ waitForProcess (pid:pids) +pipe writer reader wdir progs i = do + (inp, out, pids) <- pipeline wdir CreatePipe CreatePipe progs - return (inhdl, outhdl) + forkIO (writer (fromJust inp) i >> mapM_ waitForProcess pids) + reader (fromJust out) -- | A convenience function for when you don't care about the working --- directory. --- --- > pipe' = pipe "." -pipe' :: (Tap t, Sink s) => [(FilePath,[String])] -> t -> s -> IO (t,s) -pipe' = pipe "." +-- directory, 'pipe\'' uses ".". +pipe' :: (Handle -> a -> IO ()) -> (Handle -> IO b) + -> [(FilePath,[String])] + -> a -> IO b +pipe' r w = pipe r w "." -- | A convenience function for the common case of piping from a 'String' to a --- 'String'. This uses the 'word8ToString' and 'stringToWord8' functions and --- thus loses information if your 'Char's are non-ASCII. +-- 'String'. pipeString :: [(FilePath, [String])] -> String -> IO String -pipeString progs s = do - (_, out) <- pipe' progs (stringToWord8 s) ([] :: [Word8]) - return (word8ToString out) +pipeString = pipe' hPutStr hGetContents --- | A helper function which converts a @['Word8']@ to a 'String' by mapping --- 'chr' over the octets. +-- | A function for the common case of piping from a 'Handle' to a 'Handle'. -- --- In most cases, when you wish to pipe data to a String, you do not want to --- interpret the results as the raw byte pattern of 'Char's, so you use --- @['Word8']@ as the 'Sink' type. This function handles the common case of --- ASCII data simply—if you're dealing with non-ASCII data you probably need --- to handle the results in a different way. -word8ToString :: [Word8] -> String -word8ToString = map (chr.fromIntegral) +-- Note that this is not a convenient frontend for 'pipe' and is fundamentally +-- different in the following ways: +-- +-- * A null list of programs is allowed, in which case the contents of the +-- input Handle are simply written to the output Handle. +-- +-- * This function is not lazy and returns only when the writing has been +-- completed. +handlePipe :: FilePath -> [(FilePath,[String])] -> Handle -> Handle -> IO () +handlePipe _ [] inhdl outhdl = hGetContents inhdl >>= hPutStr outhdl +handlePipe wdir progs inhdl outhdl = do + (_, _, pids) <- pipeline wdir (UseHandle inhdl) (UseHandle outhdl) progs + mapM_ waitForProcess pids --- | The inverse of 'word8ToString'. Any 'Char's greater than 255 are --- truncated: once again, be careful with non-ASCII. -stringToWord8 :: String -> [Word8] -stringToWord8 = map (fromIntegral.ord) +-- | A convenience function for 'handlePipe'. Opens the given files in binary +-- mode. +filePipe :: FilePath -> [(FilePath,[String])] -> FilePath -> FilePath -> IO () +filePipe wdir progs infile outfile = do + withBinaryFile outfile WriteMode $ \outhdl -> + withBinaryFile infile ReadMode $ \inhdl -> + handlePipe wdir progs inhdl outhdl + +-- | Like 'filePipe', but the working directory used is the directory component +-- of the path to the first file. +filePipe' :: [(FilePath,[String])] -> FilePath -> FilePath -> IO () +filePipe' progs infile = filePipe (dropFileName infile) progs infile
− System/Process/Pipe/Plumbing.hs
@@ -1,112 +0,0 @@--- File created: 2008-06-20 14:51:20--module System.Process.Pipe.Plumbing- ( Tap(..), Sink(..)- , bufferSize- ) where--import qualified Data.ByteString as BS-import qualified Data.ByteString.Lazy as BL-import Data.Foldable (toList)-import Data.Sequence (Seq)-import qualified Data.Sequence as S-import Data.Word (Word8)-import Foreign.Marshal.Array (peekArray, pokeArray)-import Foreign.Ptr (Ptr, castPtr)-import Foreign.Storable (Storable, sizeOf)-import System.IO (Handle, hGetBuf, hPutBuf, hIsEOF)---- | From a 'Tap', data up to the requested amount flows into a 'Ptr'. The--- exact amount of 'Word8'\'s that flowed is returned. The requested amount is--- guaranteed to be no greater than 'bufferSize'.-class Tap a where- flowOut :: a -> Ptr Word8 -> Int -> IO (a, Int)- exhausted :: a -> IO Bool---- | To a 'Sink', the requested amount of 'Word8'\'s flows from a 'Ptr'. The--- requested amount is guaranteed to be no greater than 'bufferSize'.-class Sink a where- flowIn :: a -> Ptr Word8 -> Int -> IO a---- | The size of one chunk of data. A 'Ptr' 'Word8' given to a 'Tap' or 'Sink'--- is guaranteed to have room for this many 'Word8'\'s, but no more.-bufferSize :: Int-bufferSize = 32*1024---- Instances----------------- Handle--instance Tap Handle where flowOut h b s = hGetBuf h b s >>= return . (,) h- exhausted = hIsEOF-instance Sink Handle where flowIn h b s = hPutBuf h b s >> return h---- Storable a => [a]--instance Storable a => Tap [a] where- exhausted = return . null-- flowOut x buf sz = do- let size = sizeOf (head x)- (xs, ys) = splitAt (sz `div` size) x- -- avoid expensive call to 'length' if possible- sz' = if null ys then size * length xs else sz-- pokeArray (castPtr buf) xs- return (ys, sz')--instance Storable a => Sink [a] where- flowIn x buf sz = do- xs <- peekArray (sz `div` sizeOf (head x)) (castPtr buf)- return (x ++ xs)---- Storable a => Seq a--instance Storable a => Tap (Seq a) where- exhausted = return . S.null-- flowOut x buf sz = do- let size = sizeOf (S.index x 0)- (xs, ys) = S.splitAt (sz `div` size) x-- pokeArray (castPtr buf) (toList xs)- return (ys, size * S.length xs)--instance Storable a => Sink (Seq a) where- flowIn x buf sz = do- xs <- peekArray (sz `div` sizeOf (S.index x 0)) (castPtr buf)- return (x S.>< S.fromList xs)---- ByteString (both strict and lazy)---- We cheat and know in advance that ByteStrings contain octets and thus we--- don't need all the messing about with sizeOf.--instance Tap BS.ByteString where- exhausted = return . BS.null-- flowOut x buf sz = do- let (xs, ys) = BS.splitAt sz x-- pokeArray (castPtr buf) (BS.unpack xs)- return (ys, BS.length xs)--instance Tap BL.ByteString where- exhausted = return . BL.null-- flowOut x buf sz = do- let (xs, ys) = BL.splitAt (fromIntegral sz) x-- pokeArray (castPtr buf) (BL.unpack xs)- return (ys, fromIntegral . BL.length $ xs)--instance Sink BS.ByteString where- flowIn x buf sz = do- xs <- peekArray sz (castPtr buf)- return (x `BS.append` BS.pack xs)--instance Sink BL.ByteString where- flowIn x buf sz = do- xs <- peekArray sz (castPtr buf)- return (x `BL.append` BL.pack xs)