git-annex/Utility.hs

308 lines
9 KiB
Haskell
Raw Normal View History

{- general purpose utility functions
2010-10-27 20:53:54 +00:00
-
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
2010-10-27 20:53:54 +00:00
-
- Licensed under the GNU GPL version 3 or higher.
2010-10-10 04:18:10 +00:00
-}
2010-10-11 21:52:46 +00:00
module Utility (
2011-02-28 20:25:31 +00:00
CommandParam(..),
toCommand,
2010-10-11 21:52:46 +00:00
hGetContentsStrict,
2010-12-03 04:33:41 +00:00
readFileStrict,
2010-10-15 20:09:30 +00:00
parentDir,
absPath,
absPathFrom,
2011-04-25 17:36:39 +00:00
relPathCwdToFile,
relPathDirToFile,
boolSystem,
2011-04-28 20:08:18 +00:00
boolSystemEnv,
executeFile,
2010-11-08 21:44:08 +00:00
shellEscape,
shellUnEscape,
unsetFileMode,
readMaybe,
2011-06-30 04:42:09 +00:00
viaTmp,
withTempFile,
2011-02-01 00:14:08 +00:00
dirContains,
2011-04-02 19:50:51 +00:00
dirContents,
2011-04-09 16:34:49 +00:00
myHomeDir,
2011-04-17 04:57:29 +00:00
catchBool,
whenM,
(>>?),
unlessM,
(>>!),
2011-01-05 01:27:08 +00:00
prop_idempotent_shellEscape,
prop_idempotent_shellEscape_multiword,
prop_parentDir_basics,
2011-04-25 17:36:39 +00:00
prop_relPathDirToFile_basics
2010-10-11 21:52:46 +00:00
) where
2010-10-10 04:18:10 +00:00
import IO (bracket)
2010-10-10 04:18:10 +00:00
import System.IO
2010-10-19 05:45:45 +00:00
import System.Exit
import qualified System.Posix.Process
import System.Posix.Process hiding (executeFile)
2010-10-19 05:45:45 +00:00
import System.Posix.Signals
2010-11-08 21:44:08 +00:00
import System.Posix.Files
import System.Posix.Types
2011-04-09 16:34:49 +00:00
import System.Posix.User
2010-10-10 04:18:10 +00:00
import Data.String.Utils
2010-10-15 20:09:30 +00:00
import System.Path
import System.FilePath
2010-10-15 20:09:30 +00:00
import System.Directory
2010-11-08 21:44:08 +00:00
import Foreign (complement)
2011-02-01 00:14:08 +00:00
import Data.List
import Data.Maybe
import Control.Monad (liftM2, when, unless)
import System.Log.Logger
2010-10-10 04:18:10 +00:00
{- A type for parameters passed to a shell command. A command can
- be passed either some Params (multiple parameters can be included,
- whitespace-separated, or a single Param (for when parameters contain
- whitespace), or a File.
-}
2011-02-28 20:25:31 +00:00
data CommandParam = Params String | Param String | File FilePath
deriving (Eq, Show, Ord)
2011-02-28 20:25:31 +00:00
{- Used to pass a list of CommandParams to a function that runs
- a command and expects Strings. -}
toCommand :: [CommandParam] -> [String]
2011-05-16 18:49:28 +00:00
toCommand = (>>= unwrap)
where
unwrap (Param s) = [s]
unwrap (Params s) = filter (not . null) (split " " s)
-- Files that start with a dash are modified to avoid
2011-02-28 20:25:31 +00:00
-- the command interpreting them as options.
unwrap (File ('-':s)) = ["./-" ++ s]
unwrap (File s) = [s]
{- Run a system command, and returns True or False
- if it succeeded or failed.
-
- SIGINT(ctrl-c) is allowed to propigate and will terminate the program.
-}
2011-02-28 20:25:31 +00:00
boolSystem :: FilePath -> [CommandParam] -> IO Bool
2011-04-28 20:08:18 +00:00
boolSystem command params = boolSystemEnv command params Nothing
boolSystemEnv :: FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO Bool
boolSystemEnv command params env = do
-- Going low-level because all the high-level system functions
-- block SIGINT etc. We need to block SIGCHLD, but allow
-- SIGINT to do its default program termination.
let sigset = addSignal sigCHLD emptySignalSet
oldint <- installHandler sigINT Default Nothing
oldset <- getSignalMask
blockSignals sigset
childpid <- forkProcess $ childaction oldint oldset
mps <- getProcessStatus True False childpid
restoresignals oldint oldset
case mps of
Just (Exited ExitSuccess) -> return True
_ -> return False
where
restoresignals oldint oldset = do
_ <- installHandler sigINT oldint Nothing
setSignalMask oldset
childaction oldint oldset = do
restoresignals oldint oldset
2011-04-28 20:08:18 +00:00
executeFile command True (toCommand params) env
{- executeFile with debug logging -}
executeFile :: FilePath -> Bool -> [String] -> Maybe [(String, String)] -> IO ()
executeFile c path p e = do
debugM "Utility.executeFile" $
"Running: " ++ c ++ " " ++ show p ++ " " ++ maybe "" show e
System.Posix.Process.executeFile c path p e
{- Escapes a filename or other parameter to be safely able to be exposed to
- the shell. -}
shellEscape :: String -> String
shellEscape f = "'" ++ escaped ++ "'"
where
-- replace ' with '"'"'
escaped = join "'\"'\"'" $ split "'" f
{- Unescapes a set of shellEscaped words or filenames. -}
shellUnEscape :: String -> [String]
shellUnEscape [] = []
shellUnEscape s = word : shellUnEscape rest
where
(word, rest) = findword "" s
findword w [] = (w, "")
findword w (c:cs)
| c == ' ' = (w, cs)
| c == '\'' = inquote c w cs
| c == '"' = inquote c w cs
| otherwise = findword (w++[c]) cs
inquote _ w [] = (w, "")
inquote q w (c:cs)
| c == q = findword w cs
| otherwise = inquote q (w++[c]) cs
{- For quickcheck. -}
prop_idempotent_shellEscape :: String -> Bool
prop_idempotent_shellEscape s = [s] == (shellUnEscape . shellEscape) s
prop_idempotent_shellEscape_multiword :: [String] -> Bool
prop_idempotent_shellEscape_multiword s = s == (shellUnEscape . unwords . map shellEscape) s
2010-10-10 06:22:35 +00:00
{- A version of hgetContents that is not lazy. Ensures file is
- all read before it gets closed. -}
2010-10-31 20:00:32 +00:00
hGetContentsStrict :: Handle -> IO String
2010-10-10 06:22:35 +00:00
hGetContentsStrict h = hGetContents h >>= \s -> length s `seq` return s
2010-12-03 04:33:41 +00:00
{- A version of readFile that is not lazy. -}
readFileStrict :: FilePath -> IO String
readFileStrict f = readFile f >>= \s -> length s `seq` return s
2010-10-10 04:18:10 +00:00
{- Returns the parent directory of a path. Parent of / is "" -}
2011-01-05 01:27:08 +00:00
parentDir :: FilePath -> FilePath
2010-10-10 04:18:10 +00:00
parentDir dir =
2010-11-22 21:51:55 +00:00
if not $ null dirs
then slash ++ join s (take (length dirs - 1) dirs)
2010-10-10 04:18:10 +00:00
else ""
where
2010-11-22 21:51:55 +00:00
dirs = filter (not . null) $ split s dir
slash = if isAbsolute dir then s else ""
s = [pathSeparator]
2010-10-15 20:09:30 +00:00
2011-01-05 01:27:08 +00:00
prop_parentDir_basics :: FilePath -> Bool
prop_parentDir_basics dir
| null dir = True
| dir == "/" = parentDir dir == ""
| otherwise = p /= dir
where
p = parentDir dir
2011-02-01 00:14:08 +00:00
{- Checks if the first FilePath is, or could be said to contain the second.
- For example, "foo/" contains "foo/bar". Also, "foo", "./foo", "foo/" etc
- are all equivilant.
-}
dirContains :: FilePath -> FilePath -> Bool
dirContains a b = a == b || a' == b' || (a'++"/") `isPrefixOf` b'
where
norm p = fromMaybe "" $ absNormPath p "."
2011-02-01 00:14:08 +00:00
a' = norm a
b' = norm b
{- Converts a filename into a normalized, absolute path. -}
absPath :: FilePath -> IO FilePath
absPath file = do
cwd <- getCurrentDirectory
return $ absPathFrom cwd file
{- Converts a filename into a normalized, absolute path
- from the specified cwd. -}
absPathFrom :: FilePath -> FilePath -> FilePath
absPathFrom cwd file = fromMaybe bad $ absNormPath cwd file
2011-05-15 06:49:43 +00:00
where
bad = error $ "unable to normalize " ++ file
2011-04-25 17:36:39 +00:00
{- Constructs a relative path from the CWD to a file.
2010-10-15 20:09:30 +00:00
-
- For example, assuming CWD is /tmp/foo/bar:
2011-04-25 17:36:39 +00:00
- relPathCwdToFile "/tmp/foo" == ".."
- relPathCwdToFile "/tmp/foo/bar" == ""
2010-10-15 20:09:30 +00:00
-}
2011-04-25 17:36:39 +00:00
relPathCwdToFile :: FilePath -> IO FilePath
relPathCwdToFile f = liftM2 relPathDirToFile getCurrentDirectory (absPath f)
2010-10-15 20:09:30 +00:00
2011-04-25 17:36:39 +00:00
{- Constructs a relative path from a directory to a file.
2010-10-15 20:09:30 +00:00
-
2011-04-25 17:36:39 +00:00
- Both must be absolute, and normalized (eg with absNormpath).
-}
2011-04-25 17:36:39 +00:00
relPathDirToFile :: FilePath -> FilePath -> FilePath
relPathDirToFile from to = path
2010-10-15 20:09:30 +00:00
where
s = [pathSeparator]
pfrom = split s from
pto = split s to
2010-10-15 20:09:30 +00:00
common = map fst $ filter same $ zip pfrom pto
same (c,d) = c == d
uncommon = drop numcommon pto
2010-11-22 21:51:55 +00:00
dotdots = replicate (length pfrom - numcommon) ".."
numcommon = length common
path = join s $ dotdots ++ uncommon
2010-10-19 05:45:45 +00:00
2011-04-25 17:36:39 +00:00
prop_relPathDirToFile_basics :: FilePath -> FilePath -> Bool
prop_relPathDirToFile_basics from to
2011-01-05 01:27:08 +00:00
| from == to = null r
2011-04-25 17:36:39 +00:00
| otherwise = not (null r)
2011-01-05 01:27:08 +00:00
where
2011-04-25 17:36:39 +00:00
r = relPathDirToFile from to
2011-01-05 01:27:08 +00:00
2010-11-08 21:44:08 +00:00
{- Removes a FileMode from a file.
- For example, call with otherWriteMode to chmod o-w -}
unsetFileMode :: FilePath -> FileMode -> IO ()
unsetFileMode f m = do
s <- getFileStatus f
2010-11-22 21:51:55 +00:00
setFileMode f $ fileMode s `intersectFileModes` complement m
{- Attempts to read a value from a String. -}
readMaybe :: (Read a) => String -> Maybe a
readMaybe s = case reads s of
((x,_):_) -> Just x
_ -> Nothing
2011-06-30 04:42:09 +00:00
{- Runs an action like writeFile, writing to a tmp file first and
- then moving it into place. -}
viaTmp :: (FilePath -> String -> IO ()) -> FilePath -> String -> IO ()
viaTmp a file content = do
pid <- getProcessID
let tmpfile = file ++ ".tmp" ++ show pid
createDirectoryIfMissing True (parentDir file)
2011-06-30 04:42:09 +00:00
a tmpfile content
renameFile tmpfile file
2011-04-02 19:50:51 +00:00
{- Runs an action with a temp file, then removes the file. -}
withTempFile :: String -> (FilePath -> Handle -> IO a) -> IO a
withTempFile template action = bracket create remove use
where
create = do
tmpdir <- catch getTemporaryDirectory (const $ return ".")
openTempFile tmpdir template
remove (name, handle) = do
hClose handle
catchBool (removeFile name >> return True)
use (name, handle) = action name handle
2011-04-02 19:50:51 +00:00
{- Lists the contents of a directory.
- Unlike getDirectoryContents, paths are not relative to the directory. -}
dirContents :: FilePath -> IO [FilePath]
dirContents d = do
c <- getDirectoryContents d
return $ map (d </>) $ filter notcruft c
where
notcruft "." = False
notcruft ".." = False
notcruft _ = True
2011-04-09 16:34:49 +00:00
{- Current user's home directory. -}
myHomeDir :: IO FilePath
myHomeDir = do
uid <- getEffectiveUserID
u <- getUserEntryForID uid
return $ homeDirectory u
2011-04-17 04:57:29 +00:00
{- Catches IO errors and returns a Bool -}
catchBool :: IO Bool -> IO Bool
catchBool = flip catch (const $ return False)
{- when with a monadic conditional -}
whenM :: Monad m => m Bool -> m () -> m ()
whenM c a = c >>= flip when a
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM c a = c >>= flip unless a
(>>?) :: Monad m => m Bool -> m () -> m ()
(>>?) = whenM
(>>!) :: Monad m => m Bool -> m () -> m ()
(>>!) = unlessM
2011-07-16 19:09:57 +00:00
-- low fixity allows eg, foo bar >>! error $ "failed " ++ meep
infixr 0 >>?
infixr 0 >>!