{-# LANGUAGE DeriveDataTypeable #-}
module B9.ShellScript
( writeSh,
renderScript,
emptyScript,
CmdVerbosity (..),
Cwd (..),
User (..),
Script (..),
)
where
import Control.Monad.Reader
import Control.Parallel.Strategies
import Data.Binary
import Data.Data
import Data.Hashable
import Data.List (intercalate)
import Data.Semigroup as Sem
import GHC.Generics (Generic)
import System.Directory
( getPermissions,
setOwnerExecutable,
setPermissions,
)
data Script
= In
FilePath
[Script]
| As
String
[Script]
| IgnoreErrors
Bool
[Script]
| Verbosity
CmdVerbosity
[Script]
| Begin [Script]
| Run
FilePath
[String]
| NoOP
deriving (Show, Read, Typeable, Data, Eq, Generic)
instance Hashable Script
instance Binary Script
instance NFData Script
instance Sem.Semigroup Script where
NoOP <> s = s
s <> NoOP = s
(Begin ss) <> (Begin ss') = Begin (ss ++ ss')
(Begin ss) <> s' = Begin (ss ++ [s'])
s <> (Begin ss') = Begin (s : ss')
s <> s' = Begin [s, s']
instance Monoid Script where
mempty = NoOP
mappend = (Sem.<>)
data Cmd
= Cmd
String
[String]
User
Cwd
Bool
CmdVerbosity
deriving (Show, Read, Typeable, Data, Eq, Generic)
instance Hashable Cmd
instance Binary Cmd
instance NFData Cmd
data CmdVerbosity
= Debug
| Verbose
| OnlyStdErr
| Quiet
deriving (Show, Read, Typeable, Data, Eq, Generic)
instance Hashable CmdVerbosity
instance Binary CmdVerbosity
instance NFData CmdVerbosity
data Cwd
= Cwd FilePath
| NoCwd
deriving (Show, Read, Typeable, Data, Eq, Generic)
instance Hashable Cwd
instance Binary Cwd
instance NFData Cwd
data User
= User String
| NoUser
deriving (Show, Read, Typeable, Data, Eq, Generic)
instance Hashable User
instance Binary User
instance NFData User
data Ctx
= Ctx
{ ctxCwd :: Cwd,
ctxUser :: User,
ctxIgnoreErrors :: Bool,
ctxVerbosity :: CmdVerbosity
}
deriving (Show, Read, Typeable, Data, Eq, Generic)
instance Hashable Ctx
instance Binary Ctx
instance NFData Ctx
writeSh :: FilePath -> Script -> IO ()
writeSh file script = do
writeFile file (toBash $ toCmds script)
getPermissions file >>= setPermissions file . setOwnerExecutable True
emptyScript :: Script -> Bool
emptyScript = null . toCmds
toCmds :: Script -> [Cmd]
toCmds s = runReader (toLLC s) (Ctx NoCwd NoUser False Debug)
where
toLLC :: Script -> Reader Ctx [Cmd]
toLLC NoOP = return []
toLLC (In d cs) = local (\ctx -> ctx {ctxCwd = Cwd d}) (toLLC (Begin cs))
toLLC (As u cs) =
local (\ctx -> ctx {ctxUser = User u}) (toLLC (Begin cs))
toLLC (IgnoreErrors b cs) =
local (\ctx -> ctx {ctxIgnoreErrors = b}) (toLLC (Begin cs))
toLLC (Verbosity v cs) =
local (\ctx -> ctx {ctxVerbosity = v}) (toLLC (Begin cs))
toLLC (Begin cs) = concat <$> mapM toLLC cs
toLLC (Run cmd args) = do
c <- reader ctxCwd
u <- reader ctxUser
i <- reader ctxIgnoreErrors
v <- reader ctxVerbosity
return [Cmd cmd args u c i v]
renderScript :: Script -> String
renderScript = toBash . toCmds
toBash :: [Cmd] -> String
toBash cmds = intercalate "\n\n" $ bashHeader ++ (cmdToBash <$> cmds)
bashHeader :: [String]
bashHeader = ["#!/bin/bash", "set -e"]
cmdToBash :: Cmd -> String
cmdToBash (Cmd cmd args user cwd ignoreErrors verbosity) =
intercalate "\n" $
disableErrorChecking
++ pushd cwdQ
++ execCmd
++ popd cwdQ
++ reenableErrorChecking
where
execCmd = [unwords (runuser ++ [cmd] ++ args ++ redirectOutput)]
where
runuser = case user of
NoUser -> []
User "root" -> []
User u -> ["runuser", "-p", "-u", u, "--"]
pushd NoCwd = []
pushd (Cwd cwdPath) = [unwords (["pushd", cwdPath] ++ redirectOutput)]
popd NoCwd = []
popd (Cwd cwdPath) =
[unwords (["popd"] ++ redirectOutput ++ ["#", cwdPath])]
disableErrorChecking = ["set +e" | ignoreErrors]
reenableErrorChecking = ["set -e" | ignoreErrors]
cwdQ = case cwd of
NoCwd -> NoCwd
Cwd d -> Cwd ("'" ++ d ++ "'")
redirectOutput = case verbosity of
Debug -> []
Verbose -> []
OnlyStdErr -> [">", "/dev/null"]
Quiet -> ["&>", "/dev/null"]