module Shelly
(
ShIO, shelly, sub, silently, verbosely, escaping, print_stdout, print_commands
, run, run_, cmd, (-|-), lastStderr, setStdin
, command, command_, command1, command1_
, sshPairs, sshPairs_
, setenv, getenv, getenv_def, appendToPath
, cd, chdir, pwd
, echo, echo_n, echo_err, echo_n_err, inspect, inspect_err
, tag, trace, show_command
, ls, lsT, test_e, test_f, test_d, test_s, which
, absPath, (</>), (<.>), canonic, canonicalize, relPath, relativeTo, path
, mv, rm, rm_f, rm_rf, cp, cp_r, mkdir, mkdir_p
, readfile, writefile, appendfile, withTmpDir
, exit, errorExit, terror
, (<$>), (<$$>), whenM, unlessM
, catchany, catch_sh, finally_sh, ShellyHandler(..), catches_sh, catchany_sh
, time
, RunFailed(..)
, toTextIgnore, toTextWarn, fromText
, liftIO, when, unless, FilePath
, get, put
) where
import Shelly.Base
import Shelly.Find (find)
import Control.Monad ( when, unless )
import Control.Monad.Trans ( MonadIO )
import Control.Monad.Reader (runReaderT, ask)
import Prelude hiding ( catch, readFile, FilePath )
import Data.Char( isAlphaNum, isSpace )
import Data.Typeable
import Data.IORef
import Data.Maybe
import System.IO hiding ( readFile, FilePath )
import System.Exit
import System.Environment
import Control.Applicative
import Control.Exception hiding (handle)
import Control.Concurrent
import Data.Time.Clock( getCurrentTime, diffUTCTime )
import qualified Data.Text.Lazy.IO as TIO
import qualified Data.Text.IO as STIO
import System.Process( CmdSpec(..), StdStream(CreatePipe), CreateProcess(..), createProcess, waitForProcess, ProcessHandle )
import System.IO.Error (isPermissionError)
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as B
import qualified Data.Text as T
import Data.Monoid (mappend)
import Filesystem.Path.CurrentOS hiding (concat, fromText, (</>), (<.>))
import Filesystem hiding (canonicalizePath)
import qualified Filesystem.Path.CurrentOS as FP
import System.Directory ( setPermissions, getPermissions, Permissions(..), getTemporaryDirectory, findExecutable )
class ShellArg a where toTextArg :: a -> Text
instance ShellArg Text where toTextArg = id
instance ShellArg FilePath where toTextArg = toTextIgnore
class ShellCommand t where
cmdAll :: FilePath -> [Text] -> t
instance ShellCommand (ShIO Text) where
cmdAll fp args = run fp args
instance (s ~ Text, Show s) => ShellCommand (ShIO s) where
cmdAll fp args = run fp args
instance ShellCommand (ShIO ()) where
cmdAll fp args = run_ fp args
instance (ShellArg arg, ShellCommand result) => ShellCommand (arg -> result) where
cmdAll fp acc = \x -> cmdAll fp (acc ++ [toTextArg x])
cmd :: (ShellCommand result) => FilePath -> result
cmd fp = cmdAll fp []
class ToFilePath a where
toFilePath :: a -> FilePath
instance ToFilePath FilePath where toFilePath = id
instance ToFilePath Text where toFilePath = fromText
instance ToFilePath T.Text where toFilePath = FP.fromText
instance ToFilePath String where toFilePath = FP.fromText . T.pack
(</>) :: (ToFilePath filepath1, ToFilePath filepath2) => filepath1 -> filepath2 -> FilePath
x </> y = toFilePath x FP.</> toFilePath y
(<.>) :: (ToFilePath filepath) => filepath -> Text -> FilePath
x <.> y = toFilePath x FP.<.> LT.toStrict y
toTextWarn :: FilePath -> ShIO Text
toTextWarn efile = fmap lazy $ case toText efile of
Left f -> encodeError f >> return f
Right f -> return f
where
encodeError f = echo ("Invalid encoding for file: " `mappend` lazy f)
lazy = LT.fromStrict
fromText :: Text -> FilePath
fromText = FP.fromText . LT.toStrict
printGetContent :: Handle -> Handle -> IO Text
printGetContent rH wH =
fmap B.toLazyText $ printFoldHandleLines (B.fromText "") foldBuilder rH wH
getContent :: Handle -> IO Text
getContent h = fmap B.toLazyText $ foldHandleLines (B.fromText "") foldBuilder h
type FoldCallback a = ((a, Text) -> a)
printFoldHandleLines :: a -> FoldCallback a -> Handle -> Handle -> IO a
printFoldHandleLines start foldLine readHandle writeHandle = go start
where
go acc = do
line <- TIO.hGetLine readHandle
TIO.hPutStrLn writeHandle line >> go (foldLine (acc, line))
`catchany` \_ -> return acc
foldHandleLines :: a -> FoldCallback a -> Handle -> IO a
foldHandleLines start foldLine readHandle = go start
where
go acc = do
line <- TIO.hGetLine readHandle
go $ foldLine (acc, line)
`catchany` \_ -> return acc
tag :: ShIO a -> Text -> ShIO a
tag action msg = do
trace msg
action
put :: State -> ShIO ()
put newState = do
stateVar <- ask
liftIO (writeIORef stateVar newState)
runCommand :: FilePath -> [Text] -> ShIO (Handle, Handle, Handle, ProcessHandle)
runCommand exe args = do
st <- get
shellyProcess st $
RawCommand (unpack exe) (map LT.unpack args)
runCommandNoEscape :: FilePath -> [Text] -> ShIO (Handle, Handle, Handle, ProcessHandle)
runCommandNoEscape exe args = do
st <- get
shellyProcess st $
ShellCommand $ LT.unpack $ LT.intercalate " " (toTextIgnore exe : args)
shellyProcess :: State -> CmdSpec -> ShIO (Handle, Handle, Handle, ProcessHandle)
shellyProcess st cmdSpec = do
(Just hin, Just hout, Just herr, pHandle) <- liftIO $
createProcess $ CreateProcess {
cmdspec = cmdSpec
, cwd = Just $ unpack $ sDirectory st
, env = Just $ sEnvironment st
, std_in = CreatePipe, std_out = CreatePipe, std_err = CreatePipe
, close_fds = False
#if MIN_VERSION_process(1,1,0)
, create_group = False
#endif
}
return (hin, hout, herr, pHandle)
catch_sh :: (Exception e) => ShIO a -> (e -> ShIO a) -> ShIO a
catch_sh action handle = do
ref <- ask
liftIO $ catch (runReaderT action ref) (\e -> runReaderT (handle e) ref)
finally_sh :: ShIO a -> ShIO b -> ShIO a
finally_sh action handle = do
ref <- ask
liftIO $ finally (runReaderT action ref) (runReaderT handle ref)
data ShellyHandler a = forall e . Exception e => ShellyHandler (e -> ShIO a)
catches_sh :: ShIO a -> [ShellyHandler a] -> ShIO a
catches_sh action handlers = do
ref <- ask
let runner a = runReaderT a ref
liftIO $ catches (runner action) $ map (toHandler runner) handlers
where
toHandler :: (ShIO a -> IO a) -> ShellyHandler a -> Handler a
toHandler runner (ShellyHandler handle) = Handler (\e -> runner (handle e))
catchany_sh :: ShIO a -> (SomeException -> ShIO a) -> ShIO a
catchany_sh = catch_sh
cd :: FilePath -> ShIO ()
cd = absPath >=> \dir -> do
trace $ "cd " `mappend` toTextIgnore dir
modify $ \st -> st { sDirectory = dir }
chdir :: FilePath -> ShIO a -> ShIO a
chdir dir action = do
d <- gets sDirectory
cd dir
action `finally_sh` cd d
pack :: String -> FilePath
pack = decodeString
mv :: FilePath -> FilePath -> ShIO ()
mv a b = do a' <- absPath a
b' <- absPath b
trace $ "mv " `mappend` toTextIgnore a' `mappend` " " `mappend` toTextIgnore b'
liftIO $ rename a' b'
lsT :: FilePath -> ShIO [Text]
lsT = ls >=> mapM toTextWarn
pwd :: ShIO FilePath
pwd = gets sDirectory `tag` "pwd"
exit :: Int -> ShIO ()
exit 0 = liftIO (exitWith ExitSuccess) `tag` "exit 0"
exit n = liftIO (exitWith (ExitFailure n)) `tag` ("exit " `mappend` LT.pack (show n))
errorExit :: Text -> ShIO ()
errorExit msg = echo msg >> exit 1
terror :: Text -> ShIO a
terror = fail . LT.unpack
mkdir :: FilePath -> ShIO ()
mkdir = absPath >=> \fp -> do
trace $ "mkdir " `mappend` toTextIgnore fp
liftIO $ createDirectory False fp
mkdir_p :: FilePath -> ShIO ()
mkdir_p = absPath >=> \fp -> do
trace $ "mkdir -p " `mappend` toTextIgnore fp
liftIO $ createTree fp
which :: FilePath -> ShIO (Maybe FilePath)
which fp = do
(trace . mappend "which " . toTextIgnore) fp
(liftIO . findExecutable . unpack >=> return . fmap pack) fp
whenM :: Monad m => m Bool -> m () -> m ()
whenM c a = c >>= \res -> when res a
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM c a = c >>= \res -> unless res a
test_e :: FilePath -> ShIO Bool
test_e = absPath >=> \f -> do
liftIO $ do
file <- isFile f
if file then return True else isDirectory f
test_f :: FilePath -> ShIO Bool
test_f = absPath >=> liftIO . isFile
rm_rf :: FilePath -> ShIO ()
rm_rf = absPath >=> \f -> do
trace $ "rm -rf " `mappend` toTextIgnore f
isDir <- (test_d f)
if not isDir then whenM (test_f f) $ rm_f f
else
(liftIO_ $ removeTree f) `catch_sh` (\(e :: IOError) ->
when (isPermissionError e) $ do
find f >>= mapM_ (\file -> liftIO_ $ fixPermissions (unpack file) `catchany` \_ -> return ())
liftIO $ removeTree f
)
where fixPermissions file =
do permissions <- liftIO $ getPermissions file
let deletable = permissions { readable = True, writable = True, executable = True }
liftIO $ setPermissions file deletable
rm_f :: FilePath -> ShIO ()
rm_f = absPath >=> \f -> do
trace $ "rm -f " `mappend` toTextIgnore f
whenM (test_e f) $ canonic f >>= liftIO . removeFile
rm :: FilePath -> ShIO ()
rm = absPath >=> \f -> do
trace $ "rm" `mappend` toTextIgnore f
canonic f >>= liftIO . removeFile
setenv :: Text -> Text -> ShIO ()
setenv k v =
let (kStr, vStr) = (LT.unpack k, LT.unpack v)
wibble environment = (kStr, vStr) : filter ((/=kStr).fst) environment
in modify $ \x -> x { sEnvironment = wibble $ sEnvironment x }
appendToPath :: FilePath -> ShIO ()
appendToPath = absPath >=> \filepath -> do
tp <- toTextWarn filepath
pe <- getenv path_env
setenv path_env $ pe `mappend` ":" `mappend` tp
where
path_env = "PATH"
getenv :: Text -> ShIO Text
getenv k = getenv_def k ""
getenv_def :: Text -> Text -> ShIO Text
getenv_def k d = gets sEnvironment >>=
return . LT.pack . fromMaybe (LT.unpack d) . lookup (LT.unpack k)
silently :: ShIO a -> ShIO a
silently a = sub $ modify (\x -> x { sPrintStdout = False, sPrintCommands = False }) >> a
verbosely :: ShIO a -> ShIO a
verbosely a = sub $ modify (\x -> x { sPrintStdout = True, sPrintCommands = True }) >> a
print_stdout :: Bool -> ShIO a -> ShIO a
print_stdout shouldPrint a = sub $ modify (\x -> x { sPrintStdout = shouldPrint }) >> a
print_commands :: Bool -> ShIO a -> ShIO a
print_commands shouldPrint a = sub $ modify (\st -> st { sPrintCommands = shouldPrint }) >> a
sub :: ShIO a -> ShIO a
sub a = do
oldState <- get
modify $ \st -> st { sTrace = B.fromText "" }
a `finally_sh` (restoreState oldState)
where
restoreState oldState = do
newState <- get
put oldState { sTrace = sTrace oldState `mappend` sTrace newState }
escaping :: Bool -> ShIO a -> ShIO a
escaping shouldEscape action = sub $ do
modify $ \st -> st { sRun =
if shouldEscape
then runCommand
else runCommandNoEscape
}
action
shelly :: MonadIO m => ShIO a -> m a
shelly action = do
environment <- liftIO getEnvironment
dir <- liftIO getWorkingDirectory
let def = State { sCode = 0
, sStdin = Nothing
, sStderr = LT.empty
, sPrintStdout = True
, sPrintCommands = False
, sRun = runCommand
, sEnvironment = environment
, sTrace = B.fromText ""
, sDirectory = dir }
stref <- liftIO $ newIORef def
let caught =
action `catches_sh` [
ShellyHandler (\ex ->
case ex of
ExitSuccess -> liftIO $ throwIO ex
ExitFailure _ -> throwExplainedException ex
)
, ShellyHandler (\(ex::SomeException) -> throwExplainedException ex)
]
liftIO $ runReaderT caught stref
where
throwExplainedException :: Exception exception => exception -> ShIO a
throwExplainedException ex = get >>=
liftIO . throwIO . ReThrownException ex . errorMsg . LT.unpack . B.toLazyText . sTrace
errorMsg trc = "Ran commands: \n" `mappend` trc
data RunFailed = RunFailed FilePath [Text] Int Text deriving (Typeable)
instance Show RunFailed where
show (RunFailed exe args code errs) =
let codeMsg = case code of
127 -> ". exit code 127 usually means the command does not exist (in the PATH)"
_ -> ""
in "error running: " ++ LT.unpack (show_command exe args) ++
"\nexit status: " ++ show code ++ codeMsg ++ "\nstderr: " ++ LT.unpack errs
instance Exception RunFailed
show_command :: FilePath -> [Text] -> Text
show_command exe args =
LT.intercalate " " $ map quote (toTextIgnore exe : args)
where
quote t = if LT.any (== '\'') t then t
else if LT.any isSpace t then surround '\'' t else t
surround :: Char -> Text -> Text
surround c t = LT.cons c $ LT.snoc t c
sshPairs_ :: Text -> [(FilePath, [Text])] -> ShIO ()
sshPairs_ _ [] = return ()
sshPairs_ server cmds = sshPairs' run_ server cmds
sshPairs :: Text -> [(FilePath, [Text])] -> ShIO Text
sshPairs _ [] = return ""
sshPairs server cmds = sshPairs' run server cmds
sshPairs' :: (FilePath -> [Text] -> ShIO a) -> Text -> [(FilePath, [Text])] -> ShIO a
sshPairs' run' server actions = do
escaping False $ do
let ssh_commands = surround '\'' $ foldl1
(\memo next -> memo `mappend` " && " `mappend` next)
(map toSSH actions)
run' "ssh" $ [server, ssh_commands]
where
toSSH (exe,args) = show_command exe args
data Exception e => ReThrownException e = ReThrownException e String deriving (Typeable)
instance Exception e => Exception (ReThrownException e)
instance Exception e => Show (ReThrownException e) where
show (ReThrownException ex msg) = "\n" ++
msg ++ "\n" ++ "Exception: " ++ show ex
run :: FilePath -> [Text] -> ShIO Text
run exe args = fmap B.toLazyText $ runFoldLines (B.fromText "") foldBuilder exe args
foldBuilder :: (B.Builder, Text) -> B.Builder
foldBuilder (b, line) = b `mappend` B.fromLazyText line `mappend` B.singleton '\n'
command :: FilePath -> [Text] -> [Text] -> ShIO Text
command com args more_args = run com (args ++ more_args)
command_ :: FilePath -> [Text] -> [Text] -> ShIO ()
command_ com args more_args = run_ com (args ++ more_args)
command1 :: FilePath -> [Text] -> Text -> [Text] -> ShIO Text
command1 com args one_arg more_args = run com ([one_arg] ++ args ++ more_args)
command1_ :: FilePath -> [Text] -> Text -> [Text] -> ShIO ()
command1_ com args one_arg more_args = run_ com ([one_arg] ++ args ++ more_args)
run_ :: FilePath -> [Text] -> ShIO ()
run_ = runFoldLines () (\(_, _) -> ())
liftIO_ :: IO a -> ShIO ()
liftIO_ action = liftIO action >> return ()
runFoldLines :: a -> FoldCallback a -> FilePath -> [Text] -> ShIO a
runFoldLines start cb exe args = do
origstate <- get
let mStdin = sStdin origstate
put $ origstate { sStdin = Nothing, sCode = 0, sStderr = LT.empty }
state <- get
let cmdString = show_command exe args
when (sPrintCommands state) $ echo cmdString
trace cmdString
(inH,outH,errH,procH) <- sRun state exe args
case mStdin of
Just input ->
liftIO $ TIO.hPutStr inH input >> hClose inH
Nothing -> return ()
errV <- liftIO newEmptyMVar
outV <- liftIO newEmptyMVar
if sPrintStdout state
then do
liftIO_ $ forkIO $ printGetContent errH stderr >>= putMVar errV
liftIO_ $ forkIO $ printFoldHandleLines start cb outH stdout >>= putMVar outV
else do
liftIO_ $ forkIO $ getContent errH >>= putMVar errV
liftIO_ $ forkIO $ foldHandleLines start cb outH >>= putMVar outV
errs <- liftIO $ takeMVar errV
ex <- liftIO $ waitForProcess procH
let code = case ex of
ExitSuccess -> 0
ExitFailure n -> n
put $ state { sStderr = errs , sCode = code }
liftIO $ case ex of
ExitSuccess -> takeMVar outV
ExitFailure n -> throwIO $ RunFailed exe args n errs
lastStderr :: ShIO Text
lastStderr = gets sStderr
setStdin :: Text -> ShIO ()
setStdin input = modify $ \st -> st { sStdin = Just input }
(-|-) :: ShIO Text -> ShIO b -> ShIO b
one -|- two = do
res <- (print_stdout False) one
setStdin res
two
cp_r :: FilePath -> FilePath -> ShIO ()
cp_r from' to' = do
fromDir <- absPath from'
from_d <- (test_d fromDir)
if not from_d then cp from' to' else do
to <- absPath to'
trace $ "cp -r " `mappend` toTextIgnore fromDir `mappend` " " `mappend` toTextIgnore to
toIsDir <- test_d to
toDir <- if toIsDir
then return $ fromDir </> to
else mkdir to >> return to
when (fromDir == toDir) $ liftIO $ throwIO $ userError $ LT.unpack $ "cp_r: " `mappend`
toTextIgnore fromDir `mappend` " and " `mappend` toTextIgnore toDir `mappend` " are identical"
ls fromDir >>= mapM_ (\item -> cp_r (fromDir FP.</> filename item) (toDir FP.</> filename item))
cp :: FilePath -> FilePath -> ShIO ()
cp from' to' = do
from <- absPath from'
to <- absPath to'
trace $ "cp " `mappend` toTextIgnore from `mappend` " " `mappend` toTextIgnore to
to_dir <- test_d to
let to_loc = if to_dir then to FP.</> filename from else to
liftIO $ copyFile from to_loc `catchany` (\e -> throwIO $
ReThrownException e (extraMsg to_loc from)
)
where
extraMsg t f = "during copy from: " ++ unpack f ++ " to: " ++ unpack t
(<$$>) :: (Functor m) => (b -> c) -> (a -> m b) -> a -> m c
f <$$> v = fmap f . v
withTmpDir :: (FilePath -> ShIO a) -> ShIO a
withTmpDir act = do
trace "withTmpDir"
dir <- liftIO getTemporaryDirectory
tid <- liftIO myThreadId
(pS, handle) <- liftIO $ openTempFile dir ("tmp"++filter isAlphaNum (show tid))
let p = pack pS
liftIO $ hClose handle
rm_f p
mkdir p
act p `finally_sh` (rm_rf p)
writefile :: FilePath -> Text -> ShIO ()
writefile f' bits = absPath f' >>= \f -> do
trace $ "writefile " `mappend` toTextIgnore f
liftIO (TIO.writeFile (unpack f) bits)
appendfile :: FilePath -> Text -> ShIO ()
appendfile f' bits = absPath f' >>= \f -> do
trace $ "appendfile " `mappend` toTextIgnore f
liftIO (TIO.appendFile (unpack f) bits)
readfile :: FilePath -> ShIO Text
readfile = absPath >=> \fp -> do
trace $ "readfile " `mappend` toTextIgnore fp
(fmap LT.fromStrict . liftIO . STIO.readFile . unpack) fp
time :: ShIO a -> ShIO (Double, a)
time what = sub $ do
trace "time"
t <- liftIO getCurrentTime
res <- what
t' <- liftIO getCurrentTime
return ((realToFrac $ diffUTCTime t' t), res)