module Shelly
(
ShIO, shelly, sub, silently, verbosely, print_commands
, setenv, getenv, getenv_def, appendPath
, cd, chdir, pwd
, echo, echo_n, echo_err, echo_n_err, inspect
, ls, ls', test_e, test_f, test_d, test_s, which, find
, path, absPath
, mv, rm_f, rm_rf, cp, cp_r, mkdir, mkdir_p
, readfile, writefile, appendfile, withTmpDir
, run, ( # ), run_, (-|-), lastStderr, setStdin
, command, command_, command1, command1_
, exit, errorExit, terror
, (<$>), (<$$>), grep, whenM, canonic
, catchany, catch_sh, catchany_sh
, MemTime(..), time
, RunFailed(..)
, (|<>), (<>|)
, toTextUnsafe, toTextWarn, fromText
, liftIO, when
) where
import Prelude hiding ( catch, readFile, FilePath )
import Data.List( isInfixOf )
import Data.Char( isAlphaNum )
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.Monad.Reader
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( runInteractiveProcess, waitForProcess, ProcessHandle )
import qualified Data.Text.Lazy as LT
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy.Builder as B
import Data.Monoid (mappend)
import Filesystem.Path.CurrentOS hiding (concat, fromText)
import Filesystem
import qualified Filesystem.Path.CurrentOS as FP
import System.PosixCompat.Files( getSymbolicLinkStatus, isSymbolicLink )
import System.Directory ( setPermissions, getPermissions, Permissions(..), getTemporaryDirectory, findExecutable )
infixr 5 <>|
infixr 5 |<>
(<>|) :: Text -> FilePath -> Text
(<>|) t fp = t `mappend` toTextUnsafe fp
(|<>) :: FilePath -> Text -> Text
(|<>) fp t = toTextUnsafe fp `mappend` t
toTextUnsafe :: FilePath -> Text
toTextUnsafe fp = LT.fromStrict $ case toText fp of
Left f -> f
Right f -> f
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
data State = State { sCode :: Int
, sStdin :: Maybe Text
, sStderr :: Text
, sDirectory :: FilePath
, sVerbose :: Bool
, sPrintCommands :: Bool
, sRun :: FilePath -> [Text] -> ShIO (Handle, Handle, Handle, ProcessHandle)
, sEnvironment :: [(String, String)] }
type ShIO a = ReaderT (IORef State) IO a
get :: ShIO State
get = do
stateVar <- ask
liftIO (readIORef stateVar)
put :: State -> ShIO ()
put state = do
stateVar <- ask
liftIO (writeIORef stateVar state)
modify :: (State -> State) -> ShIO ()
modify f = do
state <- ask
liftIO (modifyIORef state f)
gets :: (State -> a) -> ShIO a
gets f = f <$> get
runInteractiveProcess' :: FilePath -> [Text] -> ShIO (Handle, Handle, Handle, ProcessHandle)
runInteractiveProcess' cmd args = do
st <- get
liftIO $ runInteractiveProcess (unpack cmd)
(map LT.unpack args)
(Just $ unpack $ sDirectory st)
(Just $ sEnvironment st)
catchany :: IO a -> (SomeException -> IO a) -> IO a
catchany = catch
catch_sh :: (Exception e) => ShIO a -> (e -> ShIO a) -> ShIO a
catch_sh a h = do ref <- ask
liftIO $ catch (runReaderT a ref) (\e -> runReaderT (h e) ref)
catchany_sh :: ShIO a -> (SomeException -> ShIO a) -> ShIO a
catchany_sh = catch_sh
cd :: FilePath -> ShIO ()
cd dir = do dir' <- absPath dir
modify $ \st -> st { sDirectory = dir' }
chdir :: FilePath -> ShIO a -> ShIO a
chdir dir action = do
d <- pwd
cd dir
r <- action
cd d
return r
path :: FilePath -> ShIO FilePath
path = canonic
absPath :: FilePath -> ShIO FilePath
absPath p | relative p = (</> p) <$> gets sDirectory
| otherwise = return p
unpack :: FilePath -> String
unpack = encodeString
pack :: String -> FilePath
pack = decodeString
mv :: FilePath -> FilePath -> ShIO ()
mv a b = do a' <- absPath a
b' <- absPath b
liftIO $ rename a' b'
ls' :: FilePath -> ShIO [Text]
ls' fp = do
efiles <- ls fp
mapM toTextWarn efiles
ls :: FilePath -> ShIO [FilePath]
ls = path >=> liftIO . listDirectory
find :: FilePath -> ShIO [FilePath]
find dir = do bits <- ls dir
subDir <- forM bits $ \x -> do
ex <- test_d $ dir </> x
sym <- test_s $ dir </> x
if ex && not sym then find (dir </> x)
else return []
return $ map (dir </>) bits ++ concat subDir
pwd :: ShIO FilePath
pwd = gets sDirectory
echo, echo_n, echo_err, echo_n_err :: Text -> ShIO ()
echo = liftIO . TIO.putStrLn
echo_n = liftIO . (>> hFlush System.IO.stdout) . TIO.putStr
echo_err = liftIO . TIO.hPutStrLn stderr
echo_n_err = liftIO . (>> hFlush stderr) . TIO.hPutStr stderr
exit :: Int -> ShIO ()
exit 0 = liftIO $ exitWith ExitSuccess
exit n = liftIO $ exitWith (ExitFailure n)
errorExit :: Text -> ShIO ()
errorExit msg = echo msg >> exit 1
terror :: Text -> ShIO a
terror = fail . LT.unpack
inspect :: (Show s) => s -> ShIO ()
inspect = liftIO . print
mkdir :: FilePath -> ShIO ()
mkdir = absPath >=> liftIO . createDirectory False
mkdir_p :: FilePath -> ShIO ()
mkdir_p = absPath >=> liftIO . createTree
which :: FilePath -> ShIO (Maybe FilePath)
which =
liftIO . findExecutable . unpack >=> return . fmap pack
canonic :: FilePath -> ShIO FilePath
canonic = absPath >=> liftIO . canonicalizePath
whenM :: Monad m => m Bool -> m () -> m ()
whenM c a = do res <- c
when res a
test_e :: FilePath -> ShIO Bool
test_e f = do
fs <- absPath f
liftIO $ do
file <- isFile fs
if file then return True else isDirectory fs
test_f :: FilePath -> ShIO Bool
test_f = absPath >=> liftIO . isFile
test_d :: FilePath -> ShIO Bool
test_d = absPath >=> liftIO . isDirectory
test_s :: FilePath -> ShIO Bool
test_s = absPath >=> liftIO . \f -> do
stat <- getSymbolicLinkStatus (unpack f)
return $ isSymbolicLink stat
rm_rf :: FilePath -> ShIO ()
rm_rf f = absPath f >>= \f' -> do
whenM (test_d f) $ do
_<- find f' >>= mapM (\file -> liftIO_ $ fixPermissions (unpack file) `catchany` \_ -> return ())
liftIO_ $ removeTree f'
whenM (test_f f) $ rm_f 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 f = whenM (test_e f) $ absPath f >>= liftIO . removeFile
setenv :: Text -> Text -> ShIO ()
setenv k v =
let (kStr, vStr) = (LT.unpack k, LT.unpack v)
wibble env = (kStr, vStr) : filter ((/=kStr).fst) env
in modify $ \x -> x { sEnvironment = wibble $ sEnvironment x }
appendPath :: FilePath -> ShIO ()
appendPath 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 { sVerbose = False }) >> a
verbosely :: ShIO a -> ShIO a
verbosely a = sub $ modify (\x -> x { sVerbose = True }) >> a
print_commands :: ShIO a -> ShIO a
print_commands a = sub $ modify (\x -> x { sPrintCommands = True }) >> a
sub :: ShIO a -> ShIO a
sub a = do
state <- get
r <- a `catch_sh` (\(e :: SomeException) -> put state >> throw e)
put state
return r
shelly :: MonadIO m => ShIO a -> m a
shelly a = do
env <- liftIO getEnvironment
dir <- liftIO getWorkingDirectory
let def = State { sCode = 0
, sStdin = Nothing
, sStderr = LT.empty
, sVerbose = True
, sPrintCommands = False
, sRun = runInteractiveProcess'
, sEnvironment = env
, sDirectory = dir }
stref <- liftIO $ newIORef def
liftIO $ runReaderT a stref
data RunFailed = RunFailed FilePath [Text] Int Text deriving (Typeable)
instance Show RunFailed where
show (RunFailed cmd args code errs) =
"error running " ++
unpack cmd ++ " " ++ show args ++
": exit status " ++ show code ++ ":\n" ++ LT.unpack errs
instance Exception RunFailed
( # ) :: FilePath -> [Text] -> ShIO Text
cmd # args = run cmd args
run :: FilePath -> [Text] -> ShIO Text
run cmd args = fmap B.toLazyText $ runFoldLines (B.fromText "") foldBuilder cmd 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 cmd args = do
state <- get
when (sPrintCommands state) $ do
c <- toTextWarn cmd
echo $ LT.intercalate " " (c:args)
(inH,outH,errH,procH) <- sRun state cmd args
errV <- liftIO newEmptyMVar
outV <- liftIO newEmptyMVar
if sVerbose 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
case sStdin state of
Just input ->
liftIO $ TIO.hPutStr inH input >> hClose inH
Nothing -> return ()
errs <- liftIO $ takeMVar errV
outs <- liftIO $ takeMVar outV
ex <- liftIO $ waitForProcess procH
let code = case ex of
ExitSuccess -> 0
ExitFailure n -> n
put $ state {
sStdin = Nothing
, sStderr = errs
, sCode = code
}
case ex of
ExitSuccess -> return outs
ExitFailure n -> throw $ RunFailed cmd 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 <- one
setStdin res
two
data MemTime = MemTime Rational Double deriving (Read, Show, Ord, Eq)
time :: ShIO a -> ShIO (MemTime, a)
time what = sub $ do
t <- liftIO getCurrentTime
res <- what
t' <- liftIO getCurrentTime
let mt = MemTime 0 (realToFrac $ diffUTCTime t' t)
return (mt, res)
cp_r :: FilePath -> FilePath -> ShIO ()
cp_r from to = do
whenM (test_d from) $
mkdir to >> ls from >>= mapM_ (\item -> cp_r (from </> item) (to </> item))
whenM (test_f from) $ cp from to
cp :: FilePath -> FilePath -> ShIO ()
cp from to = do
from' <- absPath from
to' <- absPath to
to_dir <- test_d to
liftIO $ copyFile from' $ if to_dir then to' </> filename from else to'
class PredicateLike pattern hay where
match :: pattern -> hay -> Bool
instance PredicateLike (a -> Bool) a where
match = id
instance (Eq a) => PredicateLike [a] [a] where
match pat = (pat `isInfixOf`)
grep :: (PredicateLike pattern hay) => pattern -> [hay] -> [hay]
grep p = filter (match p)
(<$$>) :: (Functor m) => (b -> c) -> (a -> m b) -> a -> m c
f <$$> v = fmap f . v
withTmpDir :: (FilePath -> ShIO a) -> ShIO a
withTmpDir act = do
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
a <- act p`catch_sh` \(e :: SomeException) -> rm_rf p >> throw e
rm_rf p
return a
writefile :: FilePath -> Text -> ShIO ()
writefile f bits = absPath f >>= \f' -> liftIO (TIO.writeFile (unpack f') bits)
appendfile :: FilePath -> Text -> ShIO ()
appendfile f bits = absPath f >>= \f' -> liftIO (TIO.appendFile (unpack f') bits)
readfile :: FilePath -> ShIO Text
readfile =
absPath >=> fmap LT.fromStrict . liftIO . STIO.readFile . unpack