-- | A perhaps over-engineered set of wrappers around -- readProcessChunks to run processes with a variety of echoing -- options and responses to failure. {-# LANGUAGE FlexibleContexts, FlexibleInstances, RankNTypes, ScopedTypeVariables, TypeFamilies, TypeSynonymInstances #-} {-# OPTIONS_GHC -fno-warn-name-shadowing #-} module System.Process.Read.Monad ( -- * Run processes with various types and amounts of feedback runProcessS , runProcessQ , runProcessD , runProcessV , runProcessSF , runProcessQF , runProcessDF , runProcessVF , runProcessSE , runProcessQE , runProcessDE ) where import Control.Monad (when, unless) import Control.Monad.State (StateT(runStateT), get, put) import Control.Monad.Trans (MonadIO, liftIO) import Data.Char (ord) import qualified Data.ListLike as P import Data.Word (Word8) import Prelude hiding (print) import System.Exit (ExitCode(ExitFailure)) import System.IO (hPutStrLn, stderr) import System.Process (CreateProcess(cmdspec), CmdSpec(RawCommand, ShellCommand), showCommandForUser) import qualified System.Process.Chunks as P import qualified System.Process.ListLike as P import qualified System.Process.Read.Convenience as P -- | The state we need when running processes data RunState s = RunState { cpd :: Int -- ^ Output one dot per n characters of process output, 0 means no dots , trace :: Bool -- ^ Echo the command line before starting, and after with the result code , echo :: Bool -- ^ Echo the output as it is red, using the prefixes set below , prefixes :: Maybe (s, s) -- ^ Prepend a prefix to the echoed lines of stdout and stderr. -- Special case for Just ("", ""), which means echo unmodified output. , exnPrefixes :: Maybe (s, s) -- ^ Prefixes to use when generating output after an exception occurs. , failEcho :: Bool -- ^ Echo the process output if the result code is ExitFailure , failExit :: Bool -- ^ Throw an IOError if the result code is ExitFailure } deriving (Show) defaultRunState :: RunState s defaultRunState = RunState {cpd=0, trace=True, echo=False, failEcho=False, failExit=False, prefixes=Nothing, exnPrefixes=Nothing} -- | The monad for running processes type RunT s = StateT (RunState s) withRunState :: MonadIO m => RunState s -> RunT s m a -> m a withRunState s action = (runStateT action) s >>= return . fst modifyRunState :: MonadIO m => (RunState s -> RunState s) -> RunT s m () modifyRunState modify = get >>= put . modify charsPerDot :: MonadIO m => Int -> RunT s m () charsPerDot x = modifyRunState (\ s -> s {cpd = x}) echoCommand :: MonadIO m => Bool -> RunT s m () echoCommand x = modifyRunState (\ s -> s {trace = x}) echoOnFailure :: MonadIO m => Bool -> RunT s m () echoOnFailure x = modifyRunState (\ s -> s {failEcho = x}) exceptionOnFailure :: MonadIO m => Bool -> RunT s m () exceptionOnFailure x = modifyRunState (\ s -> s {failExit = x}) echoOutput :: MonadIO m => Bool -> RunT s m () echoOutput x = modifyRunState (\ s -> s {echo = x}) setPrefixes :: (P.ListLikePlus s c, MonadIO m) => Maybe (s, s) -> RunT s m () setPrefixes x = modifyRunState (\ s -> s {prefixes = x}) runProcessM :: forall s c m. (P.ListLikePlus s c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> s -> RunT s m [P.Chunk s] runProcessM p input = do s <- get liftIO $ do when (trace s) (hPutStrLn stderr ("-> " ++ showCommand (cmdspec p))) (out1 :: [P.Chunk s]) <- P.readProcessChunks p input (out2 :: [P.Chunk s]) <- if cpd s > 0 then P.putDots (cpd s) dot out1 else return out1 (out3 :: [P.Chunk s]) <- if echo s then doOutput (prefixes s) out2 else return out2 (out5 :: [P.Chunk s]) <- if failExit s then P.foldFailure' (\ n -> doOutput (exnPrefixes s) out3 >> error (showCommand (cmdspec p) ++ " -> ExitFailure " ++ show n)) out3 else return out3 (out6 :: [P.Chunk s]) <- (if trace s then P.foldResult (\ ec -> hPutStrLn stderr ("<- " ++ showCommand (cmdspec p) ++ ": " ++ show ec) >> return (P.Result ec)) else return) out5 (out7 :: [P.Chunk s]) <- (if failEcho s then P.foldFailure (\ n -> unless (trace s) (hPutStrLn stderr ("<- " ++ showCommand (cmdspec p) ++ ": " ++ show (ExitFailure n))) >> doOutput (prefixes s) out5 >> return (P.Result (ExitFailure n))) else return) out6 return out7 where dot :: Word8 dot = fromIntegral (ord '.') doOutput :: (P.ListLikePlus a c, Enum c) => Maybe (a, a) -> [P.Chunk a] -> IO [P.Chunk a] doOutput prefixes out = maybe (P.doOutput out) (\ (sout, serr) -> P.prefixed sout serr out) prefixes >> return out -- doOutput prefixes out = P.doOutput out >> return out s :: MonadIO m => RunT s m () s = echoCommand False c :: MonadIO m => RunT s m () c = echoCommand True v :: (Enum c, P.ListLikePlus s c, MonadIO m) => RunT s m () v = echoOutput True >> setPrefixes (Just (P.fromList (map (toEnum . fromEnum) " 1> "), P.fromList (map (toEnum . fromEnum) " 2> "))) >> echoOnFailure False d :: MonadIO m => RunT s m () d = charsPerDot 50 >> echoOutput False f :: MonadIO m => RunT s m () f = exceptionOnFailure True e :: (Enum c, P.ListLikePlus s c, MonadIO m) => RunT s m () e = echoOnFailure True >> setPrefixes (Just (P.fromList (map (toEnum . fromEnum) " 1> "), P.fromList (map (toEnum . fromEnum) " 2> "))) >> exceptionOnFailure True >> echoOutput False -- | Run silently runProcessS :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessS p input = withRunState defaultRunState (s >> runProcessM p input) -- | Command line trace only. runProcessQ :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessQ p input = withRunState defaultRunState (runProcessM p input) -- | Dot output runProcessD :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessD p input = withRunState defaultRunState (c >> d >> runProcessM p input) -- | Echo output runProcessV :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessV p input = withRunState defaultRunState (c >> v >> runProcessM p input) -- | Exception on failure runProcessSF :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessSF p input = withRunState defaultRunState (s >> f >> runProcessM p input) runProcessQF :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessQF p input = withRunState defaultRunState (c >> f >> runProcessM p input) -- | Dot output and exception on failure runProcessDF :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessDF p input = withRunState defaultRunState (c >> d >> f >> runProcessM p input) -- | Echo output and exception on failure runProcessVF :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => CreateProcess -> a -> m [P.Chunk a] runProcessVF p input = withRunState defaultRunState (c >> v >> f >> runProcessM p input) -- | Exception and echo on failure runProcessSE :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => Maybe (a, a) -> CreateProcess -> a -> m [P.Chunk a] runProcessSE prefixes p input = withRunState (defaultRunState {exnPrefixes = prefixes}) (s >> e >> runProcessM p input) -- | Exception and echo on failure runProcessQE :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => Maybe (a, a) -> CreateProcess -> a -> m [P.Chunk a] runProcessQE prefixes p input = withRunState (defaultRunState {exnPrefixes = prefixes}) (c >> e >> runProcessM p input) -- | Dot output, exception on failure, echo on failure. Note that -- runProcessVE isn't a useful option, you get the output twice. VF -- makes more sense. runProcessDE :: (P.ListLikePlus a c, Enum c, MonadIO m, c ~ Word8) => Maybe (a, a) -> CreateProcess -> a -> m [P.Chunk a] runProcessDE prefixes p input = withRunState (defaultRunState {exnPrefixes = prefixes}) (c >> d >> e >> runProcessM p input) showCommand :: CmdSpec -> String showCommand (RawCommand cmd args) = showCommandForUser cmd args showCommand (ShellCommand cmd) = cmd