shellmate-0.2: Control/Shell/Internal.hs
{-# LANGUAGE CPP #-}
-- | Internal, hairy bits of Shellmate.
module Control.Shell.Internal (
MonadIO (..), Shell, ExitReason (..),
shell, shell_,
(|>), exit,
run, run_, genericRun, runInteractive,
withTempDirectory, withCustomTempDirectory,
withTempFile, withCustomTempFile,
try
) where
#if __GLASGOW_HASKELL__ <= 708
import Control.Applicative
#endif
import Control.Monad (ap)
import Control.Monad.IO.Class
import qualified Control.Concurrent as Conc
import qualified Control.Exception as Ex
import qualified Data.Map as M
import qualified System.Directory as Dir
import qualified System.Environment as Env
import qualified System.Exit as Exit
import qualified System.Process as Proc
import qualified System.IO as IO
import qualified System.IO.Temp as Temp
-- | A command name plus a ProcessHandle.
data Pid = Pid {pidName :: String, pidHandle :: Proc.ProcessHandle}
-- | Monad for running shell commands. If a command fails, the entire
-- computation is aborted unless @mayFail@ is used.
newtype Shell a = Shell {
unSh :: IO ([Pid], Result a)
}
data Result a = Fail !String | Next !a | Done
data ExitReason = Success | Failure !String
deriving (Show, Eq)
instance Functor Result where
fmap f (Next x) = Next (f x)
fmap _ (Fail x) = Fail x
fmap _ Done = Done
instance Monad Shell where
fail err = Shell $ return ([], Fail err)
return x = Shell $ return ([], Next x)
-- | The bind operation of the Shell monad is effectively a barrier; all
-- commands on the left hand side of a bind will complete before any
-- command on the right hand side is attempted.
-- To lazily stream data between two commands, use the @|>@ combinator.
(Shell m) >>= f = Shell $ do
(pids, x) <- m
merr <- waitPids pids
case (x, merr) of
(Fail err, _) -> return ([], Fail err)
(_, Just err) -> return ([], Fail err)
(Next x', _) -> unSh (f x')
(Done, _) -> return ([], Done)
instance MonadIO Shell where
liftIO act = Shell $ flip Ex.catch exHandler $ do
x <- act
return ([], Next x)
instance Applicative Shell where
pure = return
(<*>) = ap
instance Functor Shell where
fmap f (Shell x) = Shell (fmap (fmap (fmap f)) x)
-- | Run a Shell computation. The program's working directory and environment
-- will be restored after after the computation finishes.
shell :: Shell a -> IO (Either ExitReason a)
shell act = do
dir <- Dir.getCurrentDirectory
env <- M.fromList <$> Env.getEnvironment
(pids, res) <- unSh act
merr <- waitPids pids
Dir.setCurrentDirectory dir
resetEnv env
case merr of
Just err -> return $ Left $ Failure err
_ -> return $ resultToEither res
where
resultToEither (Next x) = Right x
resultToEither (Fail e) = Left (Failure e)
resultToEither (Done) = Left Success
resetEnv old = do
new <- M.fromList <$> Env.getEnvironment
mapM_ (Env.unsetEnv . fst) (M.toList (new M.\\ old))
mapM_ (uncurry Env.setEnv) (M.toList (M.filterWithKey (changed old) new))
changed old k v = maybe True (/= v) (M.lookup k old)
-- | Run a shell computation and discard its return value. If the computation
-- fails, print its error message to @stderr@ and exit.
shell_ :: Shell a -> IO ()
shell_ act = do
res <- shell act
case res of
Left (Failure err) -> IO.hPutStrLn IO.stderr err >> Exit.exitFailure
_ -> return ()
-- | Lazy counterpart to monadic bind. To stream data from a command 'a' to a
-- command 'b', do 'a |> b'.
(|>) :: Shell String -> (String -> Shell a) -> Shell a
(Shell m) |> f = Shell $ do
(pids, x) <- m
(pids', x') <- case x of
Fail err -> return ([], Fail err)
Next x' -> unSh (f x')
Done -> return ([], Done)
return (pids ++ pids', x')
infixl 1 |>
-- | Terminate a computation, successfully.
exit :: Shell a
exit = Shell $ return ([], Done)
-- | Create a temp directory in the standard system temp directory, do
-- something with it, then remove it.
withTempDirectory :: String -> (FilePath -> Shell a) -> Shell a
withTempDirectory template act = Shell $ do
Temp.withSystemTempDirectory template act'
where
act' fp = Ex.catch (unSh (act fp)) exHandler
-- | Create a temp directory in given directory, do something with it, then
-- remove it.
withCustomTempDirectory :: FilePath -> (FilePath -> Shell a) -> Shell a
withCustomTempDirectory dir act = Shell $ do
Temp.withTempDirectory dir "shellmate" act'
where
act' fp = Ex.catch (unSh (act fp)) exHandler
-- | Create a temp file in the standard system temp directory, do something
-- with it, then remove it.
withTempFile :: String -> (FilePath -> IO.Handle -> Shell a) -> Shell a
withTempFile template act = Shell $ do
Temp.withSystemTempFile template act'
where
act' fp h = Ex.catch (unSh (act fp h)) exHandler
-- | Create a temp file in the standard system temp directory, do something
-- with it, then remove it.
withCustomTempFile :: FilePath -> (FilePath -> IO.Handle -> Shell a) -> Shell a
withCustomTempFile dir act = Shell $ do
Temp.withTempFile dir "shellmate" act'
where
act' fp h = Ex.catch (unSh (act fp h)) exHandler
-- | Perform an action that may fail without aborting the entire computation.
-- Forces serialization. If the inner computation terminates successfully,
-- the outer computation terminates as well.
try :: Shell a -> Shell (Either String a)
try (Shell act) = Shell $ do
(pids, x) <- Ex.catch act exHandler
merr <- waitPids pids
case (merr, x) of
(Just err, _) -> return ([], Next (Left err))
(_, Next x') -> return ([], Next (Right x'))
(_, Fail err) -> return ([], Next (Left err))
(_, Done) -> return ([], Done)
-- | Wait for all processes in the given list. If a process has failed, its
-- error message is returned and the rest are killed.
waitPids :: [Pid] -> IO (Maybe String)
waitPids (p:ps) = do
exCode <- Proc.waitForProcess (pidHandle p)
case exCode of
Exit.ExitFailure ec -> do
killPids ps
return . Just $ "Command '" ++ (pidName p) ++ "' failed with error "
++" code " ++ show ec
_ -> do
waitPids ps
waitPids _ = do
return Nothing
-- | Kill all processes in the list.
killPids :: [Pid] -> IO ()
killPids = mapM_ (Proc.terminateProcess . pidHandle)
-- | General exception handler; any exception causes failure.
exHandler :: Ex.SomeException -> IO ([Pid], Result a)
exHandler x = return ([], Fail $ show x)
-- | Like 'run', but echoes the command's text output to the screen instead of
-- returning it.
run_ :: FilePath -> [String] -> String -> Shell ()
run_ p args stdin = do
exCode <- liftIO $ do
(Just inp, _, _, pid) <- runP p args Proc.CreatePipe
Proc.Inherit
Proc.Inherit
IO.hPutStr inp stdin
IO.hClose inp
Proc.waitForProcess pid
case exCode of
Exit.ExitFailure ec -> fail $ "Command '" ++ p ++ "' failed with error "
++" code " ++ show ec
_ -> return ()
-- | Run an interactive process.
runInteractive :: FilePath -> [String] -> Shell ()
runInteractive p args = do
exCode <- liftIO $ do
(_, _, _, pid) <- runP p args Proc.Inherit Proc.Inherit Proc.Inherit
Proc.waitForProcess pid
case exCode of
Exit.ExitFailure ec -> fail $ "Command '" ++ p ++ "' failed with error "
++" code " ++ show ec
_ -> return ()
-- | Execute an external command. No globbing, escaping or other external shell
-- magic is performed on either the command or arguments. The program's
-- stdout will be returned, and not echoed to the screen.
run :: FilePath -> [String] -> String -> Shell String
run p args stdin = Shell $ do
(output, _, pid) <- runHelper p args stdin Proc.Inherit
return ([Pid p pid], Next output)
-- | Like 'run', but always succeeds and returns the program's standard
-- error stream and exit code.
genericRun :: FilePath -> [String] -> String -> Shell (Int, String, String)
genericRun p args stdin = Shell $ do
(output, Just errh, pid) <- runHelper p args stdin Proc.CreatePipe
exCode <- Proc.waitForProcess pid
errstr <- liftIO $ IO.hGetContents errh
case errstr `seq` exCode of
Exit.ExitSuccess -> return ([], Next (0, output, errstr))
Exit.ExitFailure ec -> return ([], Next (ec, output, errstr))
-- | Helper for 'run' and 'runWithStderr'.
runHelper :: FilePath
-> [String]
-> String
-> Proc.StdStream
-> IO (String, Maybe IO.Handle, Proc.ProcessHandle)
runHelper p args inpstr errstream = do
(Just inp, Just out, merr, pid) <- runP p args Proc.CreatePipe
Proc.CreatePipe
errstream
let feed str = do
case splitAt 4096 str of
([], []) -> IO.hClose inp
(first, str') -> IO.hPutStr inp first >> feed str'
_ <- Conc.forkIO $ feed inpstr
output <- IO.hGetContents out
output `seq` return (output, merr, pid)
-- | Create a process. Helper for 'run' and friends.
runP :: String
-> [String]
-> Proc.StdStream
-> Proc.StdStream
-> Proc.StdStream
-> IO (Maybe IO.Handle,
Maybe IO.Handle,
Maybe IO.Handle,
Proc.ProcessHandle)
runP p args stdin stdout stderr =
Proc.createProcess cproc
where
cproc = Proc.CreateProcess {
Proc.cmdspec = Proc.RawCommand p args,
Proc.cwd = Nothing,
Proc.env = Nothing,
Proc.std_in = stdin,
Proc.std_out = stdout,
Proc.std_err = stderr,
Proc.close_fds = False,
#if MIN_VERSION_process(1,2,0)
Proc.delegate_ctlc = False,
#endif
Proc.create_group = False
}