uhc-util-0.1.6.2: src/UHC/Util/FPath.hs
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-| Library for manipulating a more structured version of FilePath.
Note: the library should use System.FilePath functionality but does not do so yet.
-}
module UHC.Util.FPath
(
-- * FPath datatype, FPATH class for overloaded construction
FPath(..), fpathSuff
, FPATH(..)
, FPathError -- (..) -- can be removed...
, emptyFPath
-- * Construction, deconstruction, predicates
-- , mkFPath
, fpathFromStr
, mkFPathFromDirsFile
, fpathToStr, fpathIsEmpty
, fpathSetBase, fpathSetSuff, fpathSetDir
, fpathUpdBase
, fpathRemoveSuff, fpathRemoveDir
, fpathIsAbsolute
, fpathAppendDir, fpathUnAppendDir
, fpathPrependDir, fpathUnPrependDir
, fpathSplitDirBy
, mkTopLevelFPath
-- * SearchPath
, SearchPath
, FileSuffixes, FileSuffixesWith
, FileSuffix, FileSuffixWith
, mkInitSearchPath, searchPathFromFPath, searchPathFromFPaths
, searchPathFromString
, searchFPathFromLoc
, searchLocationsForReadableFilesWith
, searchLocationsForReadableFiles
, searchPathForReadableFiles
, searchPathForReadableFile
, fpathEnsureExists
-- * Path as prefix
, filePathMkPrefix, filePathUnPrefix
, filePathCoalesceSeparator
, filePathMkAbsolute, filePathUnAbsolute
-- * Misc
, fpathGetModificationTime
, fpathDirSep, fpathDirSepChar
, fpathOpenOrStdin, openFPath
)
where
import Data.Maybe
import Data.Typeable
import Data.List
import Control.Monad
import System.IO
import System.Directory
import GHC.Generics
import UHC.Util.Utils
import UHC.Util.Time
-------------------------------------------------------------------------------------------
-- Making prefix and inverse, where a prefix has a tailing '/'
-------------------------------------------------------------------------------------------
-- | Construct a filepath to be a prefix (i.e. ending with '/' as last char)
filePathMkPrefix :: FilePath -> FilePath
filePathMkPrefix d@(_:_) | last d /= '/' = d ++ "/"
filePathMkPrefix d = d
-- | Remove from a filepath a possibly present '/' as last char
filePathUnPrefix :: FilePath -> FilePath
filePathUnPrefix d | isJust il && l == '/' = filePathUnPrefix i
where il = initlast d
(i,l) = fromJust il
filePathUnPrefix d = d
-- | Remove consecutive occurrences of '/'
filePathCoalesceSeparator :: FilePath -> FilePath
filePathCoalesceSeparator ('/':d@('/':_)) = filePathCoalesceSeparator d
filePathCoalesceSeparator (c:d) = c : filePathCoalesceSeparator d
filePathCoalesceSeparator d = d
-------------------------------------------------------------------------------------------
-- Making into absolute path and inverse, where absolute means a heading '/'
-------------------------------------------------------------------------------------------
-- | Make a filepath an absolute filepath by prefixing with '/'
filePathMkAbsolute :: FilePath -> FilePath
filePathMkAbsolute d@('/':_ ) = d
filePathMkAbsolute d = "/" ++ d
-- | Make a filepath an relative filepath by removing prefixed '/'-s
filePathUnAbsolute :: FilePath -> FilePath
filePathUnAbsolute d@('/':d') = filePathUnAbsolute d'
filePathUnAbsolute d = d
-------------------------------------------------------------------------------------------
-- File path
-------------------------------------------------------------------------------------------
-- | File path representation making explicit (possible) directory, base and (possible) suffix
data FPath
= FPath
{ fpathMbDir :: !(Maybe FilePath)
, fpathBase :: !String
, fpathMbSuff :: !(Maybe String)
}
deriving (Show,Eq,Ord,Typeable,Generic)
-- | Empty FPath
emptyFPath :: FPath
emptyFPath
= mkFPath ""
-- | Is FPath empty?
fpathIsEmpty :: FPath -> Bool
fpathIsEmpty fp = null (fpathBase fp)
-- | Conversion to FilePath
fpathToStr :: FPath -> FilePath
fpathToStr fpath
= let adds f = maybe f (\s -> f ++ "." ++ s) (fpathMbSuff fpath)
addd f = maybe f (\d -> d ++ fpathDirSep ++ f) (fpathMbDir fpath)
in addd . adds . fpathBase $ fpath
-------------------------------------------------------------------------------------------
-- Observations
-------------------------------------------------------------------------------------------
-- TBD. does not work under WinXX, use FilePath library
fpathIsAbsolute :: FPath -> Bool
fpathIsAbsolute fp
= case fpathMbDir fp of
Just ('/':_) -> True
_ -> False
-------------------------------------------------------------------------------------------
-- Utilities, (de)construction
-------------------------------------------------------------------------------------------
-- | Construct FPath from FilePath
fpathFromStr :: FilePath -> FPath
fpathFromStr fn
= FPath d b' s
where (d ,b) = maybe (Nothing,fn) (\(d,b) -> (Just d,b)) (splitOnLast fpathDirSepChar fn)
(b',s) = maybe (b,Nothing) (\(b,s) -> (b,Just s)) (splitOnLast '.' b )
-- | Construct FPath directory from FilePath
fpathDirFromStr :: String -> FPath
fpathDirFromStr d = emptyFPath {fpathMbDir = Just d}
{-# INLINE fpathDirFromStr #-}
-- | Get suffix, being empty equals the empty String
fpathSuff :: FPath -> String
fpathSuff = maybe "" id . fpathMbSuff
-- | Set the base
fpathSetBase :: String -> FPath -> FPath
fpathSetBase s fp
= fp {fpathBase = s}
{-# INLINE fpathSetBase #-}
-- | Modify the base
fpathUpdBase :: (String -> String) -> FPath -> FPath
fpathUpdBase u fp
= fp {fpathBase = u (fpathBase fp)}
{-# INLINE fpathUpdBase #-}
-- | Set suffix, empty String removes it
fpathSetSuff :: String -> FPath -> FPath
fpathSetSuff "" fp
= fpathRemoveSuff fp
fpathSetSuff s fp
= fp {fpathMbSuff = Just s}
-- | Set suffix, empty String leaves old suffix
fpathSetNonEmptySuff :: String -> FPath -> FPath
fpathSetNonEmptySuff "" fp
= fp
fpathSetNonEmptySuff s fp
= fp {fpathMbSuff = Just s}
-- | Set directory, empty FilePath removes it
fpathSetDir :: FilePath -> FPath -> FPath
fpathSetDir "" fp
= fpathRemoveDir fp
fpathSetDir d fp
= fp {fpathMbDir = Just d}
-- | Split FPath into given directory (prefix) and remainder, fails if not a prefix
fpathSplitDirBy :: FilePath -> FPath -> Maybe (String,String)
fpathSplitDirBy byDir fp
= do { d <- fpathMbDir fp
; dstrip <- stripPrefix byDir' d
; return (byDir',filePathUnAbsolute dstrip)
}
where byDir' = filePathUnPrefix byDir
-- | Prepend directory
fpathPrependDir :: FilePath -> FPath -> FPath
fpathPrependDir "" fp
= fp
fpathPrependDir d fp
= maybe (fpathSetDir d fp) (\fd -> fpathSetDir (d ++ fpathDirSep ++ fd) fp) (fpathMbDir fp)
-- | Remove directory (prefix), using 'fpathSplitDirBy'
fpathUnPrependDir :: FilePath -> FPath -> FPath
fpathUnPrependDir d fp
= case fpathSplitDirBy d fp of
Just (_,d) -> fpathSetDir d fp
_ -> fp
-- | Append directory (to directory part)
fpathAppendDir :: FPath -> FilePath -> FPath
fpathAppendDir fp ""
= fp
fpathAppendDir fp d
= maybe (fpathSetDir d fp) (\fd -> fpathSetDir (fd ++ fpathDirSep ++ d) fp) (fpathMbDir fp)
-- | Remove common trailing part of dir.
-- Note: does not check whether it really is a suffix.
fpathUnAppendDir :: FPath -> FilePath -> FPath
fpathUnAppendDir fp ""
= fp
fpathUnAppendDir fp d
= case fpathMbDir fp of
Just p -> fpathSetDir (filePathUnPrefix prefix) fp
where (prefix,_) = splitAt (length p - length d) p
_ -> fp
-- | Remove suffix
fpathRemoveSuff :: FPath -> FPath
fpathRemoveSuff fp
= fp {fpathMbSuff = Nothing}
{-# INLINE fpathRemoveSuff #-}
-- | Remove dir
fpathRemoveDir :: FPath -> FPath
fpathRemoveDir fp
= fp {fpathMbDir = Nothing}
{-# INLINE fpathRemoveDir #-}
mkFPathFromDirsFile :: Show s => [s] -> s -> FPath
mkFPathFromDirsFile dirs f
= fpathSetDir (concat $ intersperse fpathDirSep $ map show $ dirs) (mkFPath (show f))
-- | Make FPath from FilePath, setting the suffix when absent
mkTopLevelFPath
:: String -- ^ suffix
-> FilePath -- ^ file name
-> FPath
mkTopLevelFPath suff fn
= let fpNoSuff = mkFPath fn
in maybe (fpathSetSuff suff fpNoSuff) (const fpNoSuff) $ fpathMbSuff fpNoSuff
-------------------------------------------------------------------------------------------
-- Utils
-------------------------------------------------------------------------------------------
splitOnLast :: Char -> String -> Maybe (String,String)
splitOnLast splitch fn
= case fn of
"" -> Nothing
(f:fs) -> let rem = splitOnLast splitch fs
in if f == splitch
then maybe (Just ("",fs)) (\(p,s)->Just (f:p,s)) rem
else maybe Nothing (\(p,s)->Just (f:p,s)) rem
-------------------------------------------------------------------------------------------
-- Config, should be dealt with by FilePath utils
-------------------------------------------------------------------------------------------
fpathDirSep :: String
fpathDirSep = "/"
{-# INLINE fpathDirSep #-}
fpathDirSepChar :: Char
fpathDirSepChar = head fpathDirSep
{-# INLINE fpathDirSepChar #-}
-------------------------------------------------------------------------------------------
-- Class 'can make FPath of ...'
-------------------------------------------------------------------------------------------
-- | Construct a FPath from some type
class FPATH f where
mkFPath :: f -> FPath
instance FPATH String where
mkFPath = fpathFromStr
instance FPATH FPath where
mkFPath = id
-------------------------------------------------------------------------------------------
-- Class 'is error related to FPath'
-------------------------------------------------------------------------------------------
-- | Is error related to FPath
class FPathError e
instance FPathError String
-------------------------------------------------------------------------------------------
-- Open path for read or return stdin
-------------------------------------------------------------------------------------------
fpathOpenOrStdin :: FPath -> IO (FPath,Handle)
fpathOpenOrStdin fp
= if fpathIsEmpty fp
then return (mkFPath "<stdin>",stdin)
else do { let fn = fpathToStr fp
; h <- openFile fn ReadMode
; return (fp,h)
}
openFPath :: FPath -> IOMode -> Bool -> IO (String, Handle)
openFPath fp mode binary
| fpathIsEmpty fp = case mode of
ReadMode -> return ("<stdin>" ,stdin )
WriteMode -> return ("<stdout>",stdout)
AppendMode -> return ("<stdout>",stdout)
ReadWriteMode -> error "cannot use stdin/stdout with random access"
| otherwise = do { let fNm = fpathToStr fp
; h <- if binary
then openBinaryFile fNm mode
else openFile fNm mode
; return (fNm,h)
}
-------------------------------------------------------------------------------------------
-- Directory
-------------------------------------------------------------------------------------------
fpathEnsureExists :: FPath -> IO ()
fpathEnsureExists fp
= do { let d = fpathMbDir fp
; when (isJust d) (createDirectoryIfMissing True (fromJust d))
}
-------------------------------------------------------------------------------------------
-- Search path utils
-------------------------------------------------------------------------------------------
type SearchPath = [String]
type FileSuffix = Maybe String
-- | FileSuffix with extra payload
type FileSuffixWith x = (Maybe String, x)
type FileSuffixes = [FileSuffix]
-- | FileSuffix with extra payload
type FileSuffixesWith x = [FileSuffixWith x]
searchPathFromFPaths :: [FPath] -> SearchPath
searchPathFromFPaths fpL = nub [ d | (Just d) <- map fpathMbDir fpL ] ++ [""]
searchPathFromFPath :: FPath -> SearchPath
searchPathFromFPath fp = searchPathFromFPaths [fp]
{-# INLINE searchPathFromFPath #-}
mkInitSearchPath :: FPath -> SearchPath
mkInitSearchPath = searchPathFromFPath
{-# INLINE mkInitSearchPath #-}
searchPathFromString :: String -> [String]
searchPathFromString
= unfoldr f
where f "" = Nothing
f sp = Just (break (== ';') sp)
-- Simple function that returns a particular file under a
-- certain root dir.
searchFPathFromLoc :: FilePath -> FPath -> [(FilePath,FPath)]
searchFPathFromLoc loc fp = [(loc,fpathPrependDir loc fp)]
-- | Search for file in locations, with possible suffices
searchLocationsForReadableFilesWith
:: (loc -> FPath -> [(loc,FPath,[e])]) -- ^ get the locations for a name, possibly with errors
-> Bool -- ^ stop when first is found
-> [loc] -- ^ locations to search
-> FileSuffixesWith s -- ^ suffixes to try, with associated info
-> FPath -- ^ search for a path
-> IO [(FPath,loc,s,[e])]
searchLocationsForReadableFilesWith getfp stopAtFirst locs suffs fp
= let select stop f fps
= foldM chk [] fps
where chk r fp
= case r of
(_:_) | stop -> return r
_ -> do r' <- f fp
return (r ++ r')
tryToOpen loc (mbSuff,suffinfo) fp
= do { let fp' = maybe fp (\suff -> fpathSetNonEmptySuff suff fp) mbSuff
; fExists <- doesFileExist (fpathToStr fp')
-- ; hPutStrLn stderr (show fp ++ " - " ++ show fp')
; if fExists
then return [(fp',loc,suffinfo)]
else return []
}
tryToOpenWithSuffs suffs (loc,fp,x)
= fmap (map (tup123to1234 x)) $
case suffs of
[] -> tryToOpen loc (Nothing,panic "searchLocationsForReadableFilesWith.tryToOpenWithSuffs.tryToOpen") fp
_ -> select stopAtFirst
(\(ms,f) -> tryToOpen loc ms f)
({- (Nothing,fp) : -} zip suffs (repeat fp))
tryToOpenInDir loc
= select True (tryToOpenWithSuffs suffs) (getfp loc fp)
in select True tryToOpenInDir locs
-- | Search for file in locations, with possible suffices
searchLocationsForReadableFiles
:: (loc -> FPath -> [(loc,FPath,[e])]) -- ^ get the locations for a name, possibly with errors
-> Bool -- ^ stop when first is found
-> [loc] -- ^ locations to search
-> FileSuffixes -- ^ suffixes to try
-> FPath -- ^ search for a path
-> IO [(FPath,loc,[e])]
searchLocationsForReadableFiles getfp stopAtFirst locs suffs fp
= fmap (map tup1234to124) $ searchLocationsForReadableFilesWith getfp stopAtFirst locs (map (flip (,) ()) suffs) fp
{-
= let select stop f fps
= foldM chk [] fps
where chk r fp
= case r of
(_:_) | stop -> return r
_ -> do r' <- f fp
return (r ++ r')
tryToOpen loc mbSuff fp
= do { let fp' = maybe fp (\suff -> fpathSetNonEmptySuff suff fp) mbSuff
; fExists <- doesFileExist (fpathToStr fp')
-- ; hPutStrLn stderr (show fp ++ " - " ++ show fp')
; if fExists
then return [(fp',loc)]
else return []
}
tryToOpenWithSuffs suffs (loc,fp,x)
= fmap (map (tup12to123 x)) $
case suffs of
[] -> tryToOpen loc Nothing fp
_ -> select stopAtFirst
(\(ms,f) -> tryToOpen loc ms f)
({- (Nothing,fp) : -} zipWith (\s f -> (s,f)) suffs (repeat fp))
tryToOpenInDir loc
= select True (tryToOpenWithSuffs suffs) (getfp loc fp)
in select True tryToOpenInDir locs
-}
searchPathForReadableFiles :: Bool -> SearchPath -> FileSuffixes -> FPath -> IO [FPath]
searchPathForReadableFiles stopAtFirst locs suffs fp
= fmap (map tup123to1) $ searchLocationsForReadableFiles (\s f -> map (tup12to123 ([]::[String])) $ searchFPathFromLoc s f) stopAtFirst locs suffs fp
searchPathForReadableFile :: SearchPath -> FileSuffixes -> FPath -> IO (Maybe FPath)
searchPathForReadableFile paths suffs fp
= do fs <- searchPathForReadableFiles True paths suffs fp
return (listToMaybe fs)
-------------------------------------------------------------------------------------------
-- Get modification time, with old-time + time compatibility
-------------------------------------------------------------------------------------------
fpathGetModificationTime :: FPath -> IO UTCTime
fpathGetModificationTime fp = do let fn = fpathToStr fp
t <- getModificationTime fn
return (toUTCTime t)