-- 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 DeriveGeneric #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleInstances #-} module Path.PLATFORM_NAME (-- * Types Path ,Abs ,Rel ,File ,Dir ,SomeBase(..) -- * 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 ,replaceProperPrefix ,parent ,filename ,dirname ,addExtension ,splitExtension ,fileExtension ,replaceExtension -- * Parsing ,parseAbsDir ,parseRelDir ,parseAbsFile ,parseRelFile ,parseSomeDir ,parseSomeFile -- * Conversion ,toFilePath ,fromAbsDir ,fromRelDir ,fromAbsFile ,fromRelFile ,fromSomeDir ,fromSomeFile -- * TemplateHaskell constructors -- | These require the TemplateHaskell language extension. ,mkAbsDir ,mkRelDir ,mkAbsFile ,mkRelFile -- * Deprecated ,PathParseException ,stripDir ,isParentOf ,addFileExtension ,(<.>) ,setFileExtension ,(-<.>) ) where import Control.Applicative (Alternative(..)) import Control.DeepSeq (NFData (..)) import Control.Exception (Exception(..)) import Control.Monad (liftM, when) import Control.Monad.Catch (MonadThrow(..)) import Data.Aeson (FromJSON (..), FromJSONKey(..), ToJSON(..)) import qualified Data.Aeson.Types as Aeson import Data.Data import qualified Data.Text as T import Data.Hashable import qualified Data.List as L import Data.Maybe import GHC.Generics (Generic) import Language.Haskell.TH import Language.Haskell.TH.Syntax (lift) import Language.Haskell.TH.Quote (QuasiQuoter(..)) import Path.Internal.PLATFORM_NAME 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 | InvalidFile FilePath | InvalidDir 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 L.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) -- | Change from one directory prefix to another. -- -- Throw 'NotAProperPrefix' if the first argument is not a proper prefix of the -- path. -- -- >>> replaceProperPrefix $(mkRelDir "foo") $(mkRelDir "bar") $(mkRelFile "foo/file.txt") == $(mkRelFile "bar/file.txt") replaceProperPrefix :: MonadThrow m => Path b Dir -> Path b' Dir -> Path b t -> m (Path b' t) replaceProperPrefix src dst fp = (dst ) <$> stripProperPrefix src fp -- | 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 -- | Path of some type. @t@ represents the type, whether file or -- directory. Pattern match to find whether the path is absolute or -- relative. data SomeBase t = Abs (Path Abs t) | Rel (Path Rel t) deriving (Typeable, Generic, Eq, Ord) instance NFData (SomeBase t) where rnf (Abs p) = rnf p rnf (Rel p) = rnf p instance Show (SomeBase t) where show = show . fromSomeBase instance ToJSON (SomeBase t) where toJSON = toJSON . fromSomeBase {-# INLINE toJSON #-} #if MIN_VERSION_aeson(0,10,0) toEncoding = toEncoding . fromSomeBase {-# INLINE toEncoding #-} #endif instance Hashable (SomeBase t) where -- See 'Hashable' 'Path' instance for details. hashWithSalt n path = hashWithSalt n (fromSomeBase path) instance FromJSON (SomeBase Dir) where parseJSON = parseJSONWith parseSomeDir {-# INLINE parseJSON #-} instance FromJSON (SomeBase File) where parseJSON = parseJSONWith parseSomeFile {-# INLINE parseJSON #-} -- | Convert a valid path to a 'FilePath'. fromSomeBase :: SomeBase t -> FilePath fromSomeBase (Abs p) = toFilePath p fromSomeBase (Rel p) = toFilePath p -- | Convert a valid directory to a 'FilePath'. fromSomeDir :: SomeBase Dir -> FilePath fromSomeDir = fromSomeBase -- | Convert a valid file to a 'FilePath'. fromSomeFile :: SomeBase File -> FilePath fromSomeFile = fromSomeBase -- | Convert an absolute or relative 'FilePath' to a normalized 'SomeBase' -- representing a directory. -- -- Throws: 'InvalidDir' when the supplied path: -- -- * contains a @..@ path component representing the parent directory -- * is not a valid path (See 'FilePath.isValid') parseSomeDir :: MonadThrow m => FilePath -> m (SomeBase Dir) parseSomeDir fp = maybe (throwM (InvalidDir fp)) pure $ (Abs <$> parseAbsDir fp) <|> (Rel <$> parseRelDir fp) -- | Convert an absolute or relative 'FilePath' to a normalized 'SomeBase' -- representing a file. -- -- Throws: 'InvalidFile' when the supplied 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') parseSomeFile :: MonadThrow m => FilePath -> m (SomeBase File) parseSomeFile fp = maybe (throwM (InvalidFile fp)) pure $ (Abs <$> parseAbsFile fp) <|> (Rel <$> parseRelFile fp) -------------------------------------------------------------------------------- -- 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