Elzair
Elzair

Reputation: 452

How do I execute a series of shell commands in Haskell and break on an error?

Suppose I have a list of Strings representing shell commands to execute.

commands = ["git clone https://github.com/test/repo.git", "git checkout origin"]

Also, suppose I have a command, execCommand that takes a string, executes it as a shell command, retrieves the exit code, stdout and stderr, and, if the exit code is nonzero, returns Just the concatenation of stdout and stderr; otherwise, it returns Nothing.

Now, how would I execute that list of commands sequentially while ensuring that subsequent commands do not execute after one command yields an error?

Below is the full code for execCommand.

import System.IO
import System.Process
import System.Exit

createCommand :: String -> FilePath -> CreateProcess
createCommand command curDir =
  (shell command){std_out = CreatePipe, std_err = CreatePipe, cwd = Just curDir}

execCommand :: String -> FilePath -> IO (Maybe String)
execCommand command curDir = do
  (_, Just hout, Just herr, procHandle) <- createProcess $ createCommand command curDir
  exitCode <- waitForProcess procHandle
  stdOut   <- hGetContents hout
  stdErr   <- hGetContents herr
  if exitCode /= ExitSuccess
     then return $ Just $ concat [stdOut, stdErr]
     else return $ Nothing

Upvotes: 3

Views: 681

Answers (3)

Elzair
Elzair

Reputation: 452

Well, I am pretty sure I figured it out. Instead of using "fancy" stuff, I fell back to good ol' recursion.

runCommands :: [String] -> FilePath -> IO (Maybe String)
runCommands [] _ = return Nothing
runCommands (command:rest) curDir = do
  result <- execCommand command curDir
  case result of
    Nothing  -> runCommands rest curDir
    Just err -> return $ Just err

Upvotes: 1

Misguided
Misguided

Reputation: 1312

Well, this might solve your problem:

{-# LANGUAGE FlexibleInstances #-}
import System.IO
import System.Process
import System.Exit
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe

createCommand :: CmdSpec -> FilePath -> CreateProcess
createCommand (ShellCommand command) curDir =
  (shell command){std_out = CreatePipe, std_err = CreatePipe, cwd = Just curDir}
createCommand (RawCommand command arguments) curDir =
  (proc command arguments){std_out = CreatePipe, std_err = CreatePipe, cwd = Just curDir}

execCommand :: CmdSpec -> FilePath -> IO ()
execCommand command curDir = do
    (_, Just hout, Just herr, procHandle) <- createProcess $ createCommand command curDir
    exitCode <- waitForProcess procHandle
    when (exitCode /= ExitSuccess) $ do
        stdOut   <- hGetContents hout
        stdErr   <- hGetContents herr
        throwIO $ stdOut ++ stdErr

instance Exception String

execList :: [(CmdSpec, FilePath)] -> MaybeT IO String
execList xs = do
    out <- liftIO $ try $ mapM_ (uncurry execCommand) xs
    case out of
        Left c -> return c
        Right _ -> mzero

Notice that this uses FlexibleInstances. This was required for making String an instance of the Exception typeclass (the problem lies in the fact that String = [Char]). You could remove the extension by creating a new type which encloses a string and making it an instance of Exception.

Upvotes: 2

Daniel Wagner
Daniel Wagner

Reputation: 153102

You can use mapM_, which has the type

mapM_ :: ( (CmdSpec, FilePath)  -> ExceptT String IO ())
      ->  [(CmdSpec, FilePath)] -> ExceptT String IO ()

and appropriate short-circuiting behavior.

Upvotes: 0

Related Questions