如何从Haskell中执行外部程序并获取输出?

10

我希望能够在Haskell中运行一个外部程序,并取回它的输出和错误流内容。在某个库中,我找到了以下代码:

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)

这样做是否正确?

我有几个不理解的问题:为什么流被设置为NoBuffering?为什么要执行 length sout `seq`?这感觉像某种黑客行为。

此外,我想将输出和错误流合并成一个,以获得与在命令行上执行2>&1相同的效果。如果可能的话,我希望避免使用专用的I/O库,并依赖于GHC提供的标准包。


我不确定是否可能获得与在shell中执行“2>&1”相同的效果。你有什么特别的原因想要那样做吗? - kqr
2
我认为seq的原因是强制消耗程序的完整输出。否则(由于字符串是惰性的)可能会发生死锁:子进程将等待写入其输出,而主进程将等待进程完成。但是似乎serr也可能存在同样的问题,我担心如果程序将大量输出写入其stderr,则上面的代码也可能会挂起。正确地执行这个可能并不是很简单,所以我可能更愿意寻找一些稳定的IO库来解决这个问题。 - Petr
你需要这个程序是多平台的,还是只针对特定平台? - Petr
1
process 库有一个名为 readProcessWithExitCode 的函数,它返回 stdoutstderr 作为字符串。但是,如果进程写了很多输出,如果您想在程序运行时访问输出,或者您希望 stdoutstderr 交错,那么这将不足够。 - danidiaz
我希望我的代码可以跨平台移植。我可以不使用 2>&1。同时,将输出和错误流重定向到磁盘文件中也可以让我满意。 - Jan Stolarek
4
如果您使用 runProcess 并将相同的句柄传递给 stdout 和 stderr(而不是 Nothing),它应该像 2>&1 一样工作。 - hammar
3个回答

4

这个示例程序使用了 process, async, pipes, 和 pipes-bytestring 包来执行一个外部命令,并在命令运行时将 stdoutstderr 分别写入不同的文件:

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 ()

以下是将stdoutstderr交错写入同一文件的变体:

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 ()

它使用

pipes-concurrent

每个句柄的线程都写入同一个邮箱。邮箱由主线程读取,同时在命令运行时编写输出文件。

4

2
我认为这个目的非常简洁,可以使用readProcessWithExitCode
以下是一个示例,仅使用GHC标准库中的函数。该程序按大小排序列出您的主目录文件,并打印进程的退出代码以及标准输出和标准错误流的内容:
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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接