Search code examples
haskellfunctional-programmingiopipe

How can I lazily forward text to shell program and lazily get output back from it?


(This is a follow up to a previous question.)

I want to write a Haskell program that forward its standard input to grep's (or another Linux program, fwiw) standard input, and then collects grep's standard output and error to into its standard output and error.

The target is that I can have the Haskell program continuously "pre-process" the input to and "post-process" the output from a shell program.


Solution

  • A barebones version looks like this:

    main = do
        -- launch grep, grab handles for its std{in,out,err}
        (i, o, e, pid) <- runInteractiveProcess "grep" ["hello", "--line-buffered"] Nothing Nothing
        -- this likely isn't needed, but choosing your
        -- buffering consciously is a good habit to be in
        sequence_ [hSetBuffering h LineBuffering | h <- [i,o,e]]
        -- fork a thread to process each handle
        forkIO . forever $ do
            line <- hGetLine e
            hPrintf stderr "grep stderr: %s\n" line
        forkIO . forever $ do
            line <- hGetLine o
            hPrintf stdout "grep stdout: %s\n" line
        -- you could pick one of these three threads to run
        -- directly, without forking; likely the stdin
        -- one, since it's the one most likely to determine
        -- when you want to quit
        --
        -- but I like the symmetry of forking them all
        forkIO . forever $ do
            line <- hGetLine stdin
            hPrintf i "sending line to grep: %s\n" line
        -- this is where you would wait for pid, take some MVar's from
        -- the other threads saying they're done, etc.
        --
        -- your program ends when main ends, so make sure you've
        -- done so! since I don't know what conditions you want
        -- to wait for, I'll just wait forever
        forever (threadDelay 1000000)
    

    The "preprocessing" I've done here is very simple (just prepend "sending line to grep: " to each line), as is the "postprocessing" (prepend "grep stdout: " to each line), but I trust you can work out how to do more exciting processing.

    Of course there are endless variations. One I was working on just this weekend forks a thread for stderr, but because I know the exact communication protocol of the process I'm launching, I read and write its stdin and stdout in the same thread. You do need to be more careful with this approach though -- it's easy to accidentally deadlock if you and your child are both waiting for each other!