-- This template expects CPP definitions for: -- PLATFORM_NAME = Posix | Windows -- IS_WINDOWS = False | True -- | This library provides a well-typed representation of paths in a filesystem -- directory tree. -- -- __Note__: This module is for working with PLATFORM_NAME style paths. Importing -- "Path" is usually better. -- -- A path is represented by a number of path components separated by a path -- separator which is a @/@ on POSIX systems and can be a @/@ or @\\@ on Windows. -- The root of the tree is represented by a @/@ on POSIX and a drive letter -- followed by a @/@ or @\\@ on Windows (e.g. @C:\\@). Paths can be absolute -- or relative. An absolute path always starts from the root of the tree (e.g. -- @\/x/y@) whereas a relative path never starts with the root (e.g. @x/y@). -- Just like we represent the notion of an absolute root by "@/@", the same way -- we represent the notion of a relative root by "@.@". The relative root denotes -- the directory which contains the first component of a relative path. {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE PatternGuards #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleInstances #-} module Path.PLATFORM_NAME (-- * Types Path ,Abs ,Rel ,File ,Dir -- * Exceptions ,PathException(..) -- * QuasiQuoters -- | Using the following requires the QuasiQuotes language extension. -- -- __For Windows users__, the QuasiQuoters are especially beneficial because they -- prevent Haskell from treating @\\@ as an escape character. -- This makes Windows paths easier to write. -- -- @ -- [absfile|C:\\chris\\foo.txt|] -- @ ,absdir ,reldir ,absfile ,relfile -- * Operations ,() ,stripProperPrefix ,isProperPrefixOf ,parent ,filename ,dirname ,addExtension ,splitExtension ,fileExtension ,replaceExtension -- * Parsing ,parseAbsDir ,parseRelDir ,parseAbsFile ,parseRelFile -- * Conversion ,toFilePath ,fromAbsDir ,fromRelDir ,fromAbsFile ,fromRelFile -- * TemplateHaskell constructors -- | These require the TemplateHaskell language extension. ,mkAbsDir ,mkRelDir ,mkAbsFile ,mkRelFile -- * Deprecated ,PathParseException ,stripDir ,isParentOf ,addFileExtension ,(<.>) ,setFileExtension ,(-<.>) ) where import Control.Exception (Exception(..)) import Control.Monad (liftM, when) import Control.Monad.Catch (MonadThrow(..)) import Data.Aeson (FromJSON (..), FromJSONKey(..)) import qualified Data.Aeson.Types as Aeson import Data.Data import qualified Data.Text as T import Data.List import Data.Maybe import Language.Haskell.TH import Language.Haskell.TH.Syntax (lift) import Language.Haskell.TH.Quote (QuasiQuoter(..)) import Path.Internal import qualified System.FilePath.PLATFORM_NAME as FilePath -------------------------------------------------------------------------------- -- Types -- | An absolute path. data Abs deriving (Typeable) -- | A relative path; one without a root. Note that a @..@ path component to -- represent the parent directory is not allowed by this library. data Rel deriving (Typeable) -- | A file path. data File deriving (Typeable) -- | A directory path. data Dir deriving (Typeable) instance FromJSON (Path Abs File) where parseJSON = parseJSONWith parseAbsFile {-# INLINE parseJSON #-} instance FromJSON (Path Rel File) where parseJSON = parseJSONWith parseRelFile {-# INLINE parseJSON #-} instance FromJSON (Path Abs Dir) where parseJSON = parseJSONWith parseAbsDir {-# INLINE parseJSON #-} instance FromJSON (Path Rel Dir) where parseJSON = parseJSONWith parseRelDir {-# INLINE parseJSON #-} parseJSONWith :: (Show e, FromJSON a) => (a -> Either e b) -> Aeson.Value -> Aeson.Parser b parseJSONWith f x = do fp <- parseJSON x case f fp of Right p -> return p Left e -> fail (show e) {-# INLINE parseJSONWith #-} instance FromJSONKey (Path Abs File) where fromJSONKey = fromJSONKeyWith parseAbsFile {-# INLINE fromJSONKey #-} instance FromJSONKey (Path Rel File) where fromJSONKey = fromJSONKeyWith parseRelFile {-# INLINE fromJSONKey #-} instance FromJSONKey (Path Abs Dir) where fromJSONKey = fromJSONKeyWith parseAbsDir {-# INLINE fromJSONKey #-} instance FromJSONKey (Path Rel Dir) where fromJSONKey = fromJSONKeyWith parseRelDir {-# INLINE fromJSONKey #-} fromJSONKeyWith :: (Show e) => (String -> Either e b) -> Aeson.FromJSONKeyFunction b fromJSONKeyWith f = Aeson.FromJSONKeyTextParser $ \t -> case f (T.unpack t) of Left e -> fail (show e) Right rf -> pure rf {-# INLINE fromJSONKeyWith #-} -- | Exceptions that can occur during path operations. -- -- @since 0.6.0 data PathException = InvalidAbsDir FilePath | InvalidRelDir FilePath | InvalidAbsFile FilePath | InvalidRelFile FilePath | NotAProperPrefix FilePath FilePath | HasNoExtension FilePath | InvalidExtension String deriving (Show,Eq,Typeable) instance Exception PathException where displayException (InvalidExtension ext) = concat [ "Invalid extension [" , ext , "]. A valid extension starts with a '.' followed by one or more " , "characters other than '.', and it must be a valid filename, " , "notably it cannot include a path separator." ] displayException x = show x -------------------------------------------------------------------------------- -- QuasiQuoters qq :: (String -> Q Exp) -> QuasiQuoter qq quoteExp' = QuasiQuoter { quoteExp = quoteExp' , quotePat = \_ -> fail "illegal QuasiQuote (allowed as expression only, used as a pattern)" , quoteType = \_ -> fail "illegal QuasiQuote (allowed as expression only, used as a type)" , quoteDec = \_ -> fail "illegal QuasiQuote (allowed as expression only, used as a declaration)" } -- | Construct a 'Path' 'Abs' 'Dir' using QuasiQuotes. -- -- @ -- [absdir|/|] -- -- [absdir|\/home\/chris|] -- @ -- -- Remember: due to the nature of absolute paths a path like @[absdir|\/home\/chris|]@ -- may compile on your platform, but it may not compile on another -- platform (Windows). -- -- @since 0.5.13 absdir :: QuasiQuoter absdir = qq mkAbsDir -- | Construct a 'Path' 'Rel' 'Dir' using QuasiQuotes. -- -- @ -- [absdir|\/home|]\<\/>[reldir|chris|] -- @ -- -- @since 0.5.13 reldir :: QuasiQuoter reldir = qq mkRelDir -- | Construct a 'Path' 'Abs' 'File' using QuasiQuotes. -- -- @ -- [absfile|\/home\/chris\/foo.txt|] -- @ -- -- Remember: due to the nature of absolute paths a path like @[absdir|\/home\/chris\/foo.txt|]@ -- may compile on your platform, but it may not compile on another -- platform (Windows). -- -- @since 0.5.13 absfile :: QuasiQuoter absfile = qq mkAbsFile -- | Construct a 'Path' 'Rel' 'File' using QuasiQuotes. -- -- @ -- [absdir|\/home\/chris|]\<\/>[relfile|foo.txt|] -- @ -- -- @since 0.5.13 relfile :: QuasiQuoter relfile = qq mkRelFile -------------------------------------------------------------------------------- -- Operations -- | Append two paths. -- -- The following cases are valid and the equalities hold: -- -- @$(mkAbsDir x) \<\/> $(mkRelDir y) = $(mkAbsDir (x ++ \"/\" ++ y))@ -- -- @$(mkAbsDir x) \<\/> $(mkRelFile y) = $(mkAbsFile (x ++ \"/\" ++ y))@ -- -- @$(mkRelDir x) \<\/> $(mkRelDir y) = $(mkRelDir (x ++ \"/\" ++ y))@ -- -- @$(mkRelDir x) \<\/> $(mkRelFile y) = $(mkRelFile (x ++ \"/\" ++ y))@ -- -- The following are proven not possible to express: -- -- @$(mkAbsFile …) \<\/> x@ -- -- @$(mkRelFile …) \<\/> x@ -- -- @x \<\/> $(mkAbsFile …)@ -- -- @x \<\/> $(mkAbsDir …)@ -- infixr 5 () :: Path b Dir -> Path Rel t -> Path b t () (Path a) (Path b) = Path (a ++ b) -- | If the directory in the first argument is a proper prefix of the path in -- the second argument strip it from the second argument, generating a path -- relative to the directory. -- Throws 'NotAProperPrefix' if the directory is not a proper prefix of the -- path. -- -- The following properties hold: -- -- @stripProperPrefix x (x \<\/> y) = y@ -- -- Cases which are proven not possible: -- -- @stripProperPrefix (a :: Path Abs …) (b :: Path Rel …)@ -- -- @stripProperPrefix (a :: Path Rel …) (b :: Path Abs …)@ -- -- In other words the bases must match. -- -- @since 0.6.0 stripProperPrefix :: MonadThrow m => Path b Dir -> Path b t -> m (Path Rel t) stripProperPrefix (Path p) (Path l) = case stripPrefix p l of Nothing -> throwM (NotAProperPrefix p l) Just "" -> throwM (NotAProperPrefix p l) Just ok -> return (Path ok) -- | Determines if the path in the first parameter is a proper prefix of the -- path in the second parameter. -- -- The following properties hold: -- -- @not (x \`isProperPrefixOf\` x)@ -- -- @x \`isProperPrefixOf\` (x \<\/\> y)@ -- -- @since 0.6.0 isProperPrefixOf :: Path b Dir -> Path b t -> Bool isProperPrefixOf p l = isJust (stripProperPrefix p l) -- | Take the parent path component from a path. -- -- The following properties hold: -- -- @ -- parent (x \<\/> y) == x -- parent \"\/x\" == \"\/\" -- parent \"x\" == \".\" -- @ -- -- On the root (absolute or relative), getting the parent is idempotent: -- -- @ -- parent \"\/\" = \"\/\" -- parent \"\.\" = \"\.\" -- @ -- parent :: Path b t -> Path b Dir parent (Path "") = Path "" parent (Path fp) | FilePath.isDrive fp = Path fp parent (Path fp) = Path $ normalizeDir $ FilePath.takeDirectory $ FilePath.dropTrailingPathSeparator fp -- | Extract the file part of a path. -- -- The following properties hold: -- -- @filename (p \<\/> a) == filename a@ -- filename :: Path b File -> Path Rel File filename (Path l) = Path (FilePath.takeFileName l) -- | Extract the last directory name of a path. -- -- The following properties hold: -- -- @dirname $(mkRelDir ".") == $(mkRelDir ".")@ -- -- @dirname (p \<\/> a) == dirname a@ -- dirname :: Path b Dir -> Path Rel Dir dirname (Path "") = Path "" dirname (Path l) | FilePath.isDrive l = Path "" dirname (Path l) = Path (last (FilePath.splitPath l)) -- | 'splitExtension' is the inverse of 'addExtension'. It splits the given -- file path into a valid filename and a valid extension. -- -- >>> splitExtension $(mkRelFile "name.foo" ) == Just ($(mkRelFile "name" ), ".foo" ) -- >>> splitExtension $(mkRelFile "name.foo." ) == Just ($(mkRelFile "name" ), ".foo." ) -- >>> splitExtension $(mkRelFile "name.foo.." ) == Just ($(mkRelFile "name" ), ".foo..") -- >>> splitExtension $(mkRelFile "name.bar.foo" ) == Just ($(mkRelFile "name.bar"), ".foo" ) -- >>> splitExtension $(mkRelFile ".name.foo" ) == Just ($(mkRelFile ".name" ), ".foo" ) -- >>> splitExtension $(mkRelFile "name..foo" ) == Just ($(mkRelFile "name." ), ".foo" ) -- >>> splitExtension $(mkRelFile "....foo" ) == Just ($(mkRelFile "..." ), ".foo" ) -- -- Throws 'HasNoExtension' exception if the filename does not have an extension -- or in other words it cannot be split into a valid filename and a valid -- extension. The following cases throw an exception, please note that "." and -- ".." are not valid filenames: -- -- >>> splitExtension $(mkRelFile "name" ) -- >>> splitExtension $(mkRelFile "name." ) -- >>> splitExtension $(mkRelFile "name.." ) -- >>> splitExtension $(mkRelFile ".name" ) -- >>> splitExtension $(mkRelFile "..name" ) -- >>> splitExtension $(mkRelFile "...name") -- -- 'splitExtension' and 'addExtension' are inverses of each other, the -- following laws hold: -- -- @ -- uncurry addExtension . swap >=> splitExtension == return -- splitExtension >=> uncurry addExtension . swap == return -- @ -- -- @since 0.7.0 splitExtension :: MonadThrow m => Path b File -> m (Path b File, String) splitExtension (Path fpath) = if nameDot == [] || ext == [] then throwM $ HasNoExtension fpath else let fname = init nameDot in if fname == [] || fname == "." || fname == ".." then throwM $ HasNoExtension fpath else return ( Path (normalizeDrive drv ++ dir ++ fname) , FilePath.extSeparator : ext ) where -- trailing separators are ignored for the split and considered part of the -- second component in the split. splitLast isSep str = let rstr = reverse str notSep = not . isSep name = (dropWhile notSep . dropWhile isSep) rstr trailingSeps = takeWhile isSep rstr xtn = (takeWhile notSep . dropWhile isSep) rstr in (reverse name, reverse xtn ++ trailingSeps) normalizeDrive | IS_WINDOWS = normalizeTrailingSeps | otherwise = id (drv, pth) = FilePath.splitDrive fpath (dir, file) = splitLast FilePath.isPathSeparator pth (nameDot, ext) = splitLast FilePath.isExtSeparator file -- | Get extension from given file path. Throws 'HasNoExtension' exception if -- the file does not have an extension. The following laws hold: -- -- @ -- flip addExtension file >=> fileExtension == return -- fileExtension == (fmap snd) . splitExtension -- @ -- -- @since 0.5.11 fileExtension :: MonadThrow m => Path b File -> m String fileExtension = (liftM snd) . splitExtension -- | Add extension to given file path. -- -- >>> addExtension ".foo" $(mkRelFile "name" ) == Just $(mkRelFile "name.foo" ) -- >>> addExtension ".foo." $(mkRelFile "name" ) == Just $(mkRelFile "name.foo." ) -- >>> addExtension ".foo.." $(mkRelFile "name" ) == Just $(mkRelFile "name.foo.." ) -- >>> addExtension ".foo" $(mkRelFile "name.bar" ) == Just $(mkRelFile "name.bar.foo") -- >>> addExtension ".foo" $(mkRelFile ".name" ) == Just $(mkRelFile ".name.foo" ) -- >>> addExtension ".foo" $(mkRelFile "name." ) == Just $(mkRelFile "name..foo" ) -- >>> addExtension ".foo" $(mkRelFile "..." ) == Just $(mkRelFile "....foo" ) -- -- Throws an 'InvalidExtension' exception if the extension is not valid. A -- valid extension starts with a @.@ followed by one or more characters not -- including @.@ followed by zero or more @.@ in trailing position. Moreover, -- an extension must be a valid filename, notably it cannot include path -- separators. Particularly, @.foo.bar@ is an invalid extension, instead you -- have to first set @.foo@ and then @.bar@ individually. Some examples of -- invalid extensions are: -- -- >>> addExtension "foo" $(mkRelFile "name") -- >>> addExtension "..foo" $(mkRelFile "name") -- >>> addExtension ".foo.bar" $(mkRelFile "name") -- >>> addExtension ".foo/bar" $(mkRelFile "name") -- -- @since 0.7.0 addExtension :: MonadThrow m => String -- ^ Extension to add -> Path b File -- ^ Old file name -> m (Path b File) -- ^ New file name with the desired extension added at the end addExtension ext (Path path) = do validateExtension ext return $ Path (path ++ ext) where validateExtension ex@(sep:xs) = do -- has to start with a "." when (not $ FilePath.isExtSeparator sep) $ throwM $ InvalidExtension ex -- just a "." is not a valid extension when (xs == []) $ throwM $ InvalidExtension ex -- cannot have path separators when (any FilePath.isPathSeparator xs) $ throwM $ InvalidExtension ex -- All "."s is not a valid extension let ys = dropWhile FilePath.isExtSeparator (reverse xs) when (ys == []) $ throwM $ InvalidExtension ex -- Cannot have "."s except in trailing position when (any FilePath.isExtSeparator ys) $ throwM $ InvalidExtension ex -- must be valid as a filename _ <- parseRelFile ex return () validateExtension ex = throwM $ InvalidExtension ex -- | Add extension to given file path. Throws if the -- resulting filename does not parse. -- -- >>> addFileExtension "txt $(mkRelFile "foo") -- "foo.txt" -- >>> addFileExtension "symbols" $(mkRelFile "Data.List") -- "Data.List.symbols" -- >>> addFileExtension ".symbols" $(mkRelFile "Data.List") -- "Data.List.symbols" -- >>> addFileExtension "symbols" $(mkRelFile "Data.List.") -- "Data.List..symbols" -- >>> addFileExtension ".symbols" $(mkRelFile "Data.List.") -- "Data.List..symbols" -- >>> addFileExtension "evil/" $(mkRelFile "Data.List") -- *** Exception: InvalidRelFile "Data.List.evil/" -- -- @since 0.6.1 {-# DEPRECATED addFileExtension "Please use addExtension instead." #-} addFileExtension :: MonadThrow m => String -- ^ Extension to add -> Path b File -- ^ Old file name -> m (Path b File) -- ^ New file name with the desired extension added at the end addFileExtension ext (Path path) = if FilePath.isAbsolute path then liftM coercePath (parseAbsFile (FilePath.addExtension path ext)) else liftM coercePath (parseRelFile (FilePath.addExtension path ext)) where coercePath :: Path a b -> Path a' b' coercePath (Path a) = Path a -- | A synonym for 'addFileExtension' in the form of an infix operator. -- See more examples there. -- -- >>> $(mkRelFile "Data.List") <.> "symbols" -- "Data.List.symbols" -- >>> $(mkRelFile "Data.List") <.> "evil/" -- *** Exception: InvalidRelFile "Data.List.evil/" -- -- @since 0.6.1 infixr 7 <.> {-# DEPRECATED (<.>) "Please use addExtension instead." #-} (<.>) :: MonadThrow m => Path b File -- ^ Old file name -> String -- ^ Extension to add -> m (Path b File) -- ^ New file name with the desired extension added at the end (<.>) = flip addFileExtension -- | If the file has an extension replace it with the given extension otherwise -- add the new extension to it. Throws an 'InvalidExtension' exception if the -- new extension is not a valid extension (see 'fileExtension' for validity -- rules). -- -- The following law holds: -- -- @(fileExtension >=> flip replaceExtension file) file == return file@ -- -- @since 0.7.0 replaceExtension :: MonadThrow m => String -- ^ Extension to set -> Path b File -- ^ Old file name -> m (Path b File) -- ^ New file name with the desired extension replaceExtension ext path = addExtension ext (maybe path fst $ splitExtension path) -- | Replace\/add extension to given file path. Throws if the -- resulting filename does not parse. -- -- @since 0.5.11 {-# DEPRECATED setFileExtension "Please use replaceExtension instead." #-} setFileExtension :: MonadThrow m => String -- ^ Extension to set -> Path b File -- ^ Old file name -> m (Path b File) -- ^ New file name with the desired extension setFileExtension ext (Path path) = if FilePath.isAbsolute path then liftM coercePath (parseAbsFile (FilePath.replaceExtension path ext)) else liftM coercePath (parseRelFile (FilePath.replaceExtension path ext)) where coercePath :: Path a b -> Path a' b' coercePath (Path a) = Path a -- | A synonym for 'setFileExtension' in the form of an operator. -- -- @since 0.6.0 infixr 7 -<.> {-# DEPRECATED (-<.>) "Please use replaceExtension instead." #-} (-<.>) :: MonadThrow m => Path b File -- ^ Old file name -> String -- ^ Extension to set -> m (Path b File) -- ^ New file name with the desired extension (-<.>) = flip setFileExtension -------------------------------------------------------------------------------- -- Parsers -- | Convert an absolute 'FilePath' to a normalized absolute dir 'Path'. -- -- Throws: 'InvalidAbsDir' when the supplied path: -- -- * is not an absolute path -- * contains a @..@ path component representing the parent directory -- * is not a valid path (See 'FilePath.isValid') -- parseAbsDir :: MonadThrow m => FilePath -> m (Path Abs Dir) parseAbsDir filepath = if FilePath.isAbsolute filepath && not (hasParentDir filepath) && FilePath.isValid filepath then return (Path (normalizeDir filepath)) else throwM (InvalidAbsDir filepath) -- | Convert a relative 'FilePath' to a normalized relative dir 'Path'. -- -- Throws: 'InvalidRelDir' when the supplied path: -- -- * is not a relative path -- * is @""@ -- * contains a @..@ path component representing the parent directory -- * is not a valid path (See 'FilePath.isValid') -- * is all path separators -- parseRelDir :: MonadThrow m => FilePath -> m (Path Rel Dir) parseRelDir filepath = if not (FilePath.isAbsolute filepath) && not (hasParentDir filepath) && not (null filepath) && not (all FilePath.isPathSeparator filepath) && FilePath.isValid filepath then return (Path (normalizeDir filepath)) else throwM (InvalidRelDir filepath) -- | Convert an absolute 'FilePath' to a normalized absolute file 'Path'. -- -- Throws: 'InvalidAbsFile' when the supplied path: -- -- * is not an absolute path -- * is a directory path i.e. -- -- * has a trailing path separator -- * is @.@ or ends in @/.@ -- -- * contains a @..@ path component representing the parent directory -- * is not a valid path (See 'FilePath.isValid') -- parseAbsFile :: MonadThrow m => FilePath -> m (Path Abs File) parseAbsFile filepath = case validAbsFile filepath of True | normalized <- normalizeFilePath filepath , validAbsFile normalized -> return (Path normalized) _ -> throwM (InvalidAbsFile filepath) -- | Is the string a valid absolute file? validAbsFile :: FilePath -> Bool validAbsFile filepath = FilePath.isAbsolute filepath && not (FilePath.hasTrailingPathSeparator filepath) && not (hasParentDir filepath) && FilePath.isValid filepath -- | Convert a relative 'FilePath' to a normalized relative file 'Path'. -- -- Throws: 'InvalidRelFile' when the supplied path: -- -- * is not a relative path -- * is @""@ -- * is a directory path i.e. -- -- * has a trailing path separator -- * is @.@ or ends in @/.@ -- -- * contains a @..@ path component representing the parent directory -- * is not a valid path (See 'FilePath.isValid') -- parseRelFile :: MonadThrow m => FilePath -> m (Path Rel File) parseRelFile filepath = case validRelFile filepath of True | normalized <- normalizeFilePath filepath , validRelFile normalized -> return (Path normalized) _ -> throwM (InvalidRelFile filepath) -- | Is the string a valid relative file? validRelFile :: FilePath -> Bool validRelFile filepath = not (FilePath.isAbsolute filepath || FilePath.hasTrailingPathSeparator filepath) && not (null filepath) && not (hasParentDir filepath) && filepath /= "." && FilePath.isValid filepath -------------------------------------------------------------------------------- -- Conversion -- | Convert absolute path to directory to 'FilePath' type. fromAbsDir :: Path Abs Dir -> FilePath fromAbsDir = toFilePath -- | Convert relative path to directory to 'FilePath' type. fromRelDir :: Path Rel Dir -> FilePath fromRelDir = toFilePath -- | Convert absolute path to file to 'FilePath' type. fromAbsFile :: Path Abs File -> FilePath fromAbsFile = toFilePath -- | Convert relative path to file to 'FilePath' type. fromRelFile :: Path Rel File -> FilePath fromRelFile = toFilePath -------------------------------------------------------------------------------- -- Constructors -- | Make a 'Path' 'Abs' 'Dir'. -- -- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@) -- may compile on your platform, but it may not compile on another -- platform (Windows). mkAbsDir :: FilePath -> Q Exp mkAbsDir = either (error . show) lift . parseAbsDir -- | Make a 'Path' 'Rel' 'Dir'. mkRelDir :: FilePath -> Q Exp mkRelDir = either (error . show) lift . parseRelDir -- | Make a 'Path' 'Abs' 'File'. -- -- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@) -- may compile on your platform, but it may not compile on another -- platform (Windows). mkAbsFile :: FilePath -> Q Exp mkAbsFile = either (error . show) lift . parseAbsFile -- | Make a 'Path' 'Rel' 'File'. mkRelFile :: FilePath -> Q Exp mkRelFile = either (error . show) lift . parseRelFile -------------------------------------------------------------------------------- -- Internal functions -- | Normalizes directory path with platform-specific rules. normalizeDir :: FilePath -> FilePath normalizeDir = normalizeRelDir . FilePath.addTrailingPathSeparator . normalizeFilePath where -- Represent a "." in relative dir path as "" internally so that it -- composes without having to renormalize the path. normalizeRelDir p | p == relRootFP = "" | otherwise = p -- | Replaces consecutive path seps with single sep and replaces alt sep with standard sep. normalizeAllSeps :: FilePath -> FilePath normalizeAllSeps = foldr normSeps [] where normSeps ch [] = [ch] normSeps ch path@(p0:_) | FilePath.isPathSeparator ch && FilePath.isPathSeparator p0 = path | FilePath.isPathSeparator ch = FilePath.pathSeparator:path | otherwise = ch:path -- | Normalizes seps in whole path, but if there are 2+ seps at the beginning, -- they are normalized to exactly 2 to preserve UNC and Unicode prefixed paths. normalizeWindowsSeps :: FilePath -> FilePath normalizeWindowsSeps path = normLeadingSeps ++ normalizeAllSeps rest where (leadingSeps, rest) = span FilePath.isPathSeparator path normLeadingSeps = replicate (min 2 (length leadingSeps)) FilePath.pathSeparator -- | Normalizes seps only at the beginning of a path. normalizeLeadingSeps :: FilePath -> FilePath normalizeLeadingSeps path = normLeadingSep ++ rest where (leadingSeps, rest) = span FilePath.isPathSeparator path normLeadingSep = replicate (min 1 (length leadingSeps)) FilePath.pathSeparator -- | Normalizes seps only at the end of a path. normalizeTrailingSeps :: FilePath -> FilePath normalizeTrailingSeps = reverse . normalizeLeadingSeps . reverse -- | Applies platform-specific sep normalization following @FilePath.normalise@. normalizeFilePath :: FilePath -> FilePath normalizeFilePath | IS_WINDOWS = normalizeWindowsSeps . FilePath.normalise | otherwise = normalizeLeadingSeps . FilePath.normalise -------------------------------------------------------------------------------- -- Deprecated {-# DEPRECATED PathParseException "Please use PathException instead." #-} -- | Same as 'PathException'. type PathParseException = PathException {-# DEPRECATED stripDir "Please use stripProperPrefix instead." #-} -- | Same as 'stripProperPrefix'. stripDir :: MonadThrow m => Path b Dir -> Path b t -> m (Path Rel t) stripDir = stripProperPrefix {-# DEPRECATED isParentOf "Please use isProperPrefixOf instead." #-} -- | Same as 'isProperPrefixOf'. isParentOf :: Path b Dir -> Path b t -> Bool isParentOf = isProperPrefixOf