taffybar-4.0.3: test/System/Taffybar/SpecUtil.hs
{-# LANGUAGE ViewPatterns #-}
module System.Taffybar.SpecUtil
( withMockCommand
, writeScript
, withEnv
, prependPath
) where
import Control.Arrow (second)
import Control.Exception (bracket)
import Control.Monad (guard, join)
import Data.List (uncons)
import System.Directory (Permissions (..), findExecutable, getPermissions, setPermissions)
import System.Environment (lookupEnv, setEnv, unsetEnv)
import System.FilePath (isRelative, takeFileName, (</>))
import System.IO.Temp (withSystemTempDirectory)
-- | Run the given 'IO' action with the @PATH@ environment variable
-- set up so that executing the given command name will run a
-- script.
withMockCommand
:: FilePath -- ^ Name of command - should not contain slashes
-> String -- ^ Contents of script
-> IO a -- ^ Action to run with command available in search path
-> IO a
withMockCommand name content action =
withSystemTempDirectory "specutil" $ \dir -> do
writeScript (dir </> takeFileName name) content
withEnv [("PATH", prependPath dir)] action
-- | Write a text file, make it executable.
-- It ought to have a shebang line.
writeScript :: FilePath -> String -> IO ()
writeScript scriptFile content = do
content' <- patchShebangs content
writeFile scriptFile content'
p <- getPermissions scriptFile
setPermissions scriptFile (p { executable = True })
-- | Given the text of a shell script, this replaces any relative path
-- in the shebang with an absolute path, according to the current
-- environment's @PATH@ variable.
--
-- The only reason this exists is so that we can generate shell
-- scripts containing @#!/usr/bin/env bash@ and then be able to
-- execute them within a Nix build sandbox (which does not allow
-- @/usr/bin/env@).
patchShebangs :: String -> IO String
patchShebangs = patchShebangs' findExe
where
findExe = fmap join . traverse findExecutable . takeRelativeFileName
takeRelativeFileName :: FilePath -> Maybe FilePath
takeRelativeFileName fp = guard (isRelative fp) >> pure (takeFileName fp)
patchShebangs' :: Applicative m => (FilePath -> m (Maybe FilePath)) -> String -> m String
patchShebangs' replaceExe script = case parseInterpreter script of
Just (interpreter, rest) -> do
let unparse exe = "#! " ++ exe ++ rest
maybe script unparse <$> replaceExe interpreter
Nothing -> pure script
parseInterpreter :: String -> Maybe (String, String)
parseInterpreter (lines -> content) = do
(header, rest) <- uncons content
(interpreter, args) <- parseShebang header
pure (interpreter, unlines (args:rest))
where
parseShebang :: String -> Maybe (String, String)
parseShebang ('#':'!':(findInterpreter -> shebang)) =
let catArgs args = unwords ("":args)
in second catArgs <$> shebang
parseShebang _ = Nothing
findInterpreter = uncons . dropWhile ((== "env") . takeFileName) . words
-- | Run an 'IO' action with the given environment variables set up
-- according to their current value. 'Nothing' denotes an unset
-- environment variable. After the 'IO' action completes, environment
-- variables are restored to their previous state.
withEnv :: [(String, Maybe String -> Maybe String)] -> IO a -> IO a
withEnv mods = bracket setup teardown . const
where
setup = mapM (uncurry changeEnv) mods
teardown = mapM (uncurry putEnv) . reverse
changeEnv name f = do
old <- lookupEnv name
putEnv name (f old)
pure (name, old)
putEnv :: String -> Maybe String -> IO ()
putEnv name = maybe (unsetEnv name) (setEnv name)
-- | Use this as a modifier function argument of 'withEnv' to ensure
-- that the given directory is prepended to a search path variable.
prependPath :: FilePath -> Maybe String -> Maybe String
prependPath p = Just . (++ ":/usr/bin") . (p ++) . maybe "" (":" ++)