Jan Stolarek
Jan Stolarek

Reputation: 1429

How to retrieve output of external program executed from Haskell?

I want to run an external program from Haskell and retrieve the contents of its output and error streams. In one of the libraries I found this code:

runProcess :: FilePath -> [String] -> IO (ExitCode, String, String)
runProcess prog args = do
  (_,o,e,p) <- runInteractiveProcess prog args Nothing Nothing
  hSetBuffering o NoBuffering
  hSetBuffering e NoBuffering
  sout  <- hGetContents o
  serr  <- hGetContents e
  ecode <- length sout `seq` waitForProcess p
  return (ecode, sout, serr)

Is this the right way to do it?

There are some things I don't understand here: why streams are set to NoBuffering? Why length sout `seq`? This feels like some kind of hack.

Also, I would like to merge output and error streams into one to get the same effect as if I did 2>&1 on the command line. If possible, I want to avoid using dedicated I/O libraries and rely on standard packages provided with GHC.

Upvotes: 11

Views: 2811

Answers (3)

Matthias Braun
Matthias Braun

Reputation: 34353

I find readProcessWithExitCode for this purpose very succinct.

Here's an example using only functions from GHC's standard libraries. The program lists the files of your home directory sorted by size, printing the process' exit code as well as the contents of the standard out and standard error streams:

import           System.Directory               ( getHomeDirectory )
import           System.Process                 ( readProcessWithExitCode )
import           System.Exit                    ( ExitCode )
import           Data.List.NonEmpty

callCmd :: NonEmpty String -> IO (ExitCode, String, String)
callCmd (cmd :| args) = readProcessWithExitCode cmd args stdIn
  where stdIn = ""

main = do
  home                       <- getHomeDirectory
  (exitCode, stdOut, stdErr) <-
    callCmd $ "ls" :| [home, "--almost-all", "-l", "-S"]
  putStrLn
    $  "Exit code: "
    ++ show exitCode
    ++ "\nOut: "
    ++ stdOut
    ++ "\nErr: "
    ++ stdErr

Upvotes: 4

danidiaz
danidiaz

Reputation: 27766

This example program uses the process, async, pipes, and pipes-bytestring packages to execute an external command and write stdout and stderr to separate files while the command runs:

import Control.Applicative
import Control.Monad
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception
import Pipes
import qualified Pipes.ByteString as P
import Pipes.Concurrent
import System.Process
import System.IO

writeToFile :: Handle -> FilePath -> IO ()
writeToFile handle path = 
    finally (withFile path WriteMode $ \hOut ->
                runEffect $ P.fromHandle handle >-> P.toHandle hOut)
            (hClose handle) 

main :: IO ()
main = do
   (_,mOut,mErr,procHandle) <- createProcess $ 
        (proc "foo" ["--help"]) { std_out = CreatePipe
                                , std_err = CreatePipe 
                                }
   let (hOut,hErr) = maybe (error "bogus handles") 
                           id
                           ((,) <$> mOut <*> mErr)
   a1 <- async $ writeToFile hOut "stdout.txt" 
   a2 <- async $ writeToFile hErr "stderr.txt" 
   waitBoth a1 a2
   return ()

And this is a variation that writes stdout and stderr interleaved to the same file:

writeToMailbox :: Handle -> Output ByteString -> IO ()
writeToMailbox handle oMailbox = 
     finally (runEffect $ P.fromHandle handle >-> toOutput oMailbox)
             (hClose handle) 

writeToFile :: Input ByteString -> FilePath -> IO ()
writeToFile iMailbox path = 
    withFile path WriteMode $ \hOut ->
         runEffect $ fromInput iMailbox >-> P.toHandle hOut

main :: IO ()
main = do
   (_,mOut,mErr,procHandle) <- createProcess $ 
        (proc "foo" ["--help"]) { std_out = CreatePipe
                                , std_err = CreatePipe 
                                }
   let (hOut,hErr) = maybe (error "bogus handles") 
                           id
                           ((,) <$> mOut <*> mErr)
   (mailBoxOut,mailBoxIn,seal) <- spawn' Unbounded
   a1 <- async $ writeToMailbox hOut mailBoxOut 
   a2 <- async $ writeToMailbox hErr mailBoxOut 
   a3 <- async $ waitBoth a1 a2 >> atomically seal 
   writeToFile mailBoxIn "combined.txt" 
   wait a3
   return ()

It uses pipes-concurrent. The threads that drain each handle write to the same mailbox. The mailbox is read by the main thread, which writes the output file while the command runs.

Upvotes: 4

haroldcarr
haroldcarr

Reputation: 1575

Use Shelly, a module for shell-like programming in Haskell:

http://hackage.haskell.org/package/shelly-1.4.1/docs/Shelly.html

Upvotes: 4

Related Questions