{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE IncoherentInstances #-} module Data.Path.Internal where import Data.Monoid ((<>)) import Data.Typeable import GHC.Generics import Data.Maybe (isJust, fromMaybe) import Data.String (IsString(..)) import Data.List (intercalate, foldl', stripPrefix) import Data.Data import Data.Text (Text) import qualified Data.Text as T import Data.Validity type AbsPath = Path Absolute type RelPath = Path Relative data Path rel = Path { pathPieces :: [PathPiece] , pathLastPiece :: LastPathPiece , pathExtensions :: [Extension] } deriving (Show, Eq, Generic, Data, Typeable) data Absolute = Absolute deriving (Generic, Data, Typeable) data Relative = Relative deriving (Generic, Data, Typeable) -- Choose nicer ways of printing if the context allows the compiler to figure -- out what kind of path it is. instance Show (Path Relative) where show = toRelFilePath instance Show (Path Absolute) where show = toAbsFilePath -- | ONLY for @OverloadedStrings@ -- This instance instance is unsafe and should only be used at own risk, -- for literals instance IsString (Path Absolute) where fromString = unsafeAbsPathError -- | ONLY for @OverloadedStrings@ -- This instance instance is unsafe and should only be used at own risk, -- for literals instance IsString (Path Relative) where fromString = unsafeRelPathError instance Validity (Path rel) where isValid Path{..} = isValid pathPieces && isValid pathLastPiece && isValid pathExtensions && (not (T.null lt) || (null pathPieces && null pathExtensions)) where (LastPathPiece lt) = pathLastPiece newtype PathPiece = PathPiece Text deriving (Eq, Generic, Data, Typeable) instance Show PathPiece where show (PathPiece t) = T.unpack t instance Validity PathPiece where isValid (PathPiece t) = not (T.null t) && not (containsSeparator t) -- | ONLY for @OverloadedStrings@ -- This instance instance is unsafe and should only be used at own risk, -- for literals instance IsString PathPiece where fromString = unsafePathPieceError newtype LastPathPiece = LastPathPiece Text deriving (Eq, Generic, Data, Typeable) instance Show LastPathPiece where show (LastPathPiece t) = T.unpack t instance Validity LastPathPiece where isValid (LastPathPiece t) = not (containsSeparator t) && not (containsExtension t) -- | ONLY for @OverloadedStrings@ -- This instance instance is unsafe and should only be used at own risk, -- for literals instance IsString LastPathPiece where fromString = unsafeLastPieceError newtype Extension = Extension Text deriving (Eq, Generic, Data, Typeable) instance Show Extension where show (Extension t) = T.unpack t -- | ONLY for @OverloadedStrings@ -- This instance instance is unsafe and should only be used at own risk, -- for literals instance IsString Extension where fromString = unsafeExtError instance Validity Extension where isValid (Extension t) = not (T.null t) && not (containsExtension t) && not (containsSeparator t) pathSeparator :: Char pathSeparator = '/' pathSeparators :: [Char] pathSeparators = [pathSeparator] -- | Check if a given character is a valid path separator -- -- >>> isPathSeparator pathSeparator -- True -- >>> all isPathSeparator pathSeparators -- True isPathSeparator :: Char -> Bool isPathSeparator = (== pathSeparator) extensionSeparator :: Char extensionSeparator = '.' extensionSeparators :: [Char] extensionSeparators = [extensionSeparator] -- | Check if a given character is a valid extension separator -- -- >>> isExtensionSeparator extensionSeparator -- True -- >>> all isExtensionSeparator extensionSeparators -- True isExtensionSeparator :: Char -> Bool isExtensionSeparator = (== extensionSeparator) containsSatisfied :: (Char -> Bool) -> Text -> Bool containsSatisfied func = isJust . T.find func containsSeparator :: Text -> Bool containsSeparator = containsSatisfied isPathSeparator containsExtension :: Text -> Bool containsExtension = containsSatisfied isExtensionSeparator -- | Construct a relative path from a 'FilePath', failing if -- the given 'FilePath' does not represent a valid relative path. -- -- >>> relpath "file" -- Just file -- >>> relpath "/file" -- Nothing -- >>> relpath "." -- Just . -- >>> relpath "/" -- Nothing -- >>> relpath "" -- Nothing relpath :: FilePath -> Maybe RelPath relpath [] = Nothing relpath fp@(c:rest) | c == extensionSeparator && null rest = Just emptyPath | c == pathSeparator = Nothing | last fp == extensionSeparator = Nothing | otherwise = do let rawPieces = filter (not . T.null) $ T.split (== pathSeparator) $ T.pack fp (pieces, lastRawPiece) <- unsnoc $ map PathPiece rawPieces let (lastPiece, exts) = splitPiece lastRawPiece return $ Path pieces lastPiece exts -- | Construct an absolute path from a 'FilePath', failing if -- the given 'FilePath' does not represent a valid absolute path. -- -- >>> abspath "/file" -- Just /file -- >>> abspath "file" -- Nothing -- >>> abspath "/" -- Just / -- >>> abspath "." -- Nothing -- >>> abspath "" -- Nothing abspath :: FilePath -> Maybe AbsPath abspath [] = Nothing abspath (c:fp) | c == pathSeparator && null fp = Just emptyPath | c == pathSeparator = unsafePathTypeCoerse <$> relpath fp | otherwise = Nothing -- | Construct a path piece safely -- -- >>> pathpiece "file" -- Just file -- >>> pathpiece "with.dot" -- Just with.dot -- >>> pathpiece "with/slash" -- Nothing pathpiece :: String -> Maybe PathPiece pathpiece = constructValid . PathPiece . T.pack -- | Construct a last path piece safely -- -- >>> lastpiece "file" -- Just file -- >>> lastpiece "with.dot" -- Nothing lastpiece :: String -> Maybe LastPathPiece lastpiece = constructValid . LastPathPiece . T.pack -- | Construct an extension safely -- -- >>> ext "extension" -- Just extension -- >>> ext ".ext" -- Nothing -- >>> ext "" -- Nothing ext :: String -> Maybe Extension ext = constructValid . Extension . T.pack -- | Ground a filepath on an absolute path. -- This will try to parse the given @FilePath@ as an absolute path and take it -- if that works. Otherwise it will try to parse it an a relative path and -- append it to the given @AbsPath@ -- -- >>> ground "/home/user" "relative/path" -- Just /home/user/relative/path -- >>> ground "/home/user" "/absolute/path" -- Just /absolute/path -- >>> ground "/home/user" "." -- Just /home/user -- >>> ground "/home/user" "/" -- Just / -- >>> ground "/" "." -- Just / -- >>> ground "/anything" "" -- Nothing ground :: AbsPath -> FilePath -> Maybe AbsPath ground ap fp = case abspath fp of Just a -> Just a Nothing -> case relpath fp of Just r -> Just $ ap r Nothing -> Nothing -- | Construct a relative path, throwing an 'error' if 'relpath' would fail. unsafeRelPathError :: FilePath -> RelPath unsafeRelPathError fp = constructValidUnsafe . fromMaybe (error $ "Invalid path: " ++ fp) . relpath $ fp -- | Construct an absolute path, throwing an 'error' if 'abspath' would fail. unsafeAbsPathError :: FilePath -> AbsPath unsafeAbsPathError fp = constructValidUnsafe . fromMaybe (error $ "Invalid path: " ++ fp) . abspath $ fp -- | Construct an extension, throwing an 'error' if 'pathpiece' would fail. unsafePathPieceError :: String -> PathPiece unsafePathPieceError s = constructValidUnsafe . fromMaybe (error $ "Invalid path piece: " ++ s) . pathpiece $ s -- | Construct an extension, throwing an 'error' if 'lastpiece' would fail. unsafeLastPieceError :: String -> LastPathPiece unsafeLastPieceError s = constructValidUnsafe . fromMaybe (error $ "Invalid last path piece: " ++ s) . lastpiece $ s -- | Construct an extension, throwing an 'error' if 'ext' would fail. unsafeExtError :: String -> Extension unsafeExtError e = constructValidUnsafe . fromMaybe (error $ "Invalid extension: " ++ e) . ext $ e -- | Render a relative filepath to a 'FilePath' toRelFilePath :: RelPath -> FilePath toRelFilePath (Path [] (LastPathPiece "") []) = [extensionSeparator] toRelFilePath Path{..} = intercalate [pathSeparator] (map renderPiece pathPieces ++ [renderLastPiece pathLastPiece]) ++ renderExtensions pathExtensions -- | Render an absolute filepath to a 'FilePath' toAbsFilePath :: AbsPath -> FilePath toAbsFilePath (Path [] (LastPathPiece "") []) = [pathSeparator] toAbsFilePath p = (pathSeparator:) . toRelFilePath . unsafePathTypeCoerse $ p -- | Take the last extension of a filepath -- -- >>> takeExtension ("/directory/path.ext" :: AbsPath) -- Just ext -- >>> takeExtension ("file.tar.gz" :: RelPath) -- Just gz -- >>> takeExtension ("file" :: RelPath) -- Nothing -- -- Replaces @System.FilePath.takeExtension@ takeExtension :: Path rel -> Maybe Extension takeExtension (Path _ _ es) = lastMay es -- | Take all extensions of a given path in the form of a list -- -- >>> takeExtensions ("/directory/path.ext" :: AbsPath) -- [ext] -- >>> takeExtensions ("file.tar.gz" :: RelPath) -- [tar,gz] -- -- Replaces @System.FilePath.takeExtensions@ takeExtensions :: Path rel -> [Extension] takeExtensions (Path _ _ es) = es -- | Replace the last extension of a path, exactly -- -- This will fail if the given path has no extension -- -- >>> replaceExtensionExact "dir/file.ext1.ext2" "ext3" :: Maybe RelPath -- Just dir/file.ext1.ext3 -- >>> replaceExtensionExact "dir/file.ext1" "ext2" :: Maybe RelPath -- Just dir/file.ext2 -- >>> replaceExtensionExact "dir/file" "ext" :: Maybe RelPath -- Nothing -- >>> replaceExtensionExact "/dir/file.ext1.ext2" "ext3" :: Maybe AbsPath -- Just /dir/file.ext1.ext3 -- >>> replaceExtensionExact "/dir/file.ext1" "ext2" :: Maybe AbsPath -- Just /dir/file.ext2 -- >>> replaceExtensionExact "/dir/file" "ext" :: Maybe AbsPath -- Nothing -- >>> replaceExtensionExact "." "ext" :: Maybe RelPath -- Nothing -- >>> replaceExtensionExact "/" "ext" :: Maybe AbsPath -- Nothing replaceExtensionExact :: Path rel -> Extension -> Maybe (Path rel) replaceExtensionExact path extension = do path' <- dropExtensionExact path return $ path' <.> extension -- | Replace the last extension of a path -- -- This will first remove one extension and then add the given extension. -- -- > replaceExtension path extension = dropExtension path <.> extension -- -- >>> replaceExtension "dir/file.ext1.ext2" "ext3" :: RelPath -- dir/file.ext1.ext3 -- >>> replaceExtension "dir/file.ext1" "ext2" :: RelPath -- dir/file.ext2 -- >>> replaceExtension "dir/file" "ext" :: RelPath -- dir/file.ext -- >>> replaceExtension "/dir/file.ext1.ext2" "ext3" :: AbsPath -- /dir/file.ext1.ext3 -- >>> replaceExtension "/dir/file.ext1" "ext2" :: AbsPath -- /dir/file.ext2 -- >>> replaceExtension "/dir/file" "ext" :: AbsPath -- /dir/file.ext -- >>> replaceExtension "." "ext" :: RelPath -- . -- >>> replaceExtension "/" "ext" :: AbsPath -- / -- -- Replaces @System.FilePath.replaceExtension@ replaceExtension :: Path rel -> Extension -> Path rel replaceExtension path extension = dropExtension path <.> extension -- | Replace the last extension of a path (equivalent to 'replaceExtension') -- -- >>> "dir/file.ext1.ext2" -<.> "ext3" :: RelPath -- dir/file.ext1.ext3 -- >>> "dir/file.ext1" -<.> "ext2" :: RelPath -- dir/file.ext2 -- >>> "dir/file" -<.> "ext" :: RelPath -- dir/file.ext -- >>> "/dir/file.ext1.ext2" -<.> "ext3" :: AbsPath -- /dir/file.ext1.ext3 -- >>> "/dir/file.ext1" -<.> "ext2" :: AbsPath -- /dir/file.ext2 -- >>> "/dir/file" -<.> "ext" :: AbsPath -- /dir/file.ext -- >>> "." -<.> "ext" :: RelPath -- . -- >>> "/" -<.> "ext" :: AbsPath -- / -- -- Replaces @System.FilePath.(-<.>)@ (-<.>) :: Path rel -> Extension -> Path rel (-<.>) = replaceExtension -- | Replace all the extensions of a path with the given extension -- -- >>> replaceExtensions "dir/file.ext1.ext2" "ext3" :: RelPath -- dir/file.ext3 -- >>> replaceExtensions "dir/file.ext1" "ext3" :: RelPath -- dir/file.ext3 -- >>> replaceExtensions "dir/file" "ext3" :: RelPath -- dir/file.ext3 -- >>> replaceExtensions "/dir/file.ext1.ext2" "ext3" :: AbsPath -- /dir/file.ext3 -- >>> replaceExtensions "/dir/file.ext1" "ext3" :: AbsPath -- /dir/file.ext3 -- >>> replaceExtensions "/dir/file" "ext3" :: AbsPath -- /dir/file.ext3 -- >>> replaceExtensions "." "ext" :: RelPath -- . -- >>> replaceExtensions "/" "ext" :: AbsPath -- / -- TODO(syd) exact version replaceExtensions :: Path rel -> Extension -> Path rel replaceExtensions p e = replaceExtensionss p [e] -- | Replace all the extensions of a path with the given list of extensions -- -- >>> replaceExtensionss "dir/file.ext1.ext2" ["ext3", "ext4"] :: RelPath -- dir/file.ext3.ext4 -- >>> replaceExtensionss "dir/file.ext1" ["ext3", "ext4"] :: RelPath -- dir/file.ext3.ext4 -- >>> replaceExtensionss "dir/file" ["ext3", "ext4"] :: RelPath -- dir/file.ext3.ext4 -- >>> replaceExtensionss "/dir/file.ext1.ext2" ["ext3", "ext4"] :: AbsPath -- /dir/file.ext3.ext4 -- >>> replaceExtensionss "/dir/file.ext1" ["ext3", "ext4"] :: AbsPath -- /dir/file.ext3.ext4 -- >>> replaceExtensionss "/dir/file" ["ext3", "ext4"] :: AbsPath -- /dir/file.ext3.ext4 -- >>> replaceExtensionss "." ["ext1", "ext2"] :: RelPath -- . -- >>> replaceExtensionss "/" ["ext1", "ext2"] :: AbsPath -- / replaceExtensionss :: Path rel -> [Extension] -> Path rel replaceExtensionss p@(Path ps lp _) es | isEmptyPath p = emptyPath | otherwise = (Path ps lp es) -- | Drop the last extension of a path, exactly -- -- This will fail if the given path has no extensions -- -- >>> dropExtensionExact "dir/file.ext1.ext2" :: Maybe RelPath -- Just dir/file.ext1 -- >>> dropExtensionExact "dir/file.ext" :: Maybe RelPath -- Just dir/file -- >>> dropExtensionExact "dir/file" :: Maybe RelPath -- Nothing -- >>> dropExtensionExact "/dir/file.ext1.ext2" :: Maybe AbsPath -- Just /dir/file.ext1 -- >>> dropExtensionExact "/dir/file.ext" :: Maybe AbsPath -- Just /dir/file -- >>> dropExtensionExact "/dir/file" :: Maybe AbsPath -- Nothing -- >>> dropExtensionExact "." :: Maybe RelPath -- Nothing -- >>> dropExtensionExact "/" :: Maybe AbsPath -- Nothing dropExtensionExact :: Path rel -> Maybe (Path rel) dropExtensionExact path@(Path _ _ es) = do is <- initMay es return $ path { pathExtensions = is } -- | Drop the last extension of a path -- -- >>> dropExtension "dir/file.ext1.ext2" :: RelPath -- dir/file.ext1 -- >>> dropExtension "dir/file.ext" :: RelPath -- dir/file -- >>> dropExtension "dir/file" :: RelPath -- dir/file -- >>> dropExtension "/dir/file.ext1.ext2" :: AbsPath -- /dir/file.ext1 -- >>> dropExtension "/dir/file.ext" :: AbsPath -- /dir/file -- >>> dropExtension "/dir/file" :: AbsPath -- /dir/file -- >>> dropExtension "." :: RelPath -- . -- >>> dropExtension "/" :: AbsPath -- / -- -- Replaces @System.FilePath.dropExtension@ dropExtension :: Path rel -> Path rel dropExtension path = path { pathExtensions = reverse . drop 1 . reverse $ pathExtensions path } -- | Drop all extensions of a path -- -- >>> dropExtensions "dir/file.ext1.ext2" :: RelPath -- dir/file -- >>> dropExtensions "dir/file.ext" :: RelPath -- dir/file -- >>> dropExtensions "dir/file" :: RelPath -- dir/file -- >>> dropExtensions "/dir/file.ext1.ext2" :: AbsPath -- /dir/file -- >>> dropExtensions "/dir/file.ext" :: AbsPath -- /dir/file -- >>> dropExtensions "/dir/file" :: AbsPath -- /dir/file -- >>> dropExtensions "." :: RelPath -- . -- >>> dropExtensions "/" :: AbsPath -- / -- -- Replaces @System.FilePath.dropExtensions@ -- TODO(syd) exact version dropExtensions :: Path rel -> Path rel dropExtensions (Path ps lp _) = Path ps lp [] -- | Add an extension to a path -- -- >>> addExtension "/directory/path" "ext" :: AbsPath -- /directory/path.ext -- >>> addExtension "directory/path" "ext" :: RelPath -- directory/path.ext -- -- This will not override the extension if there already is an extension. -- It will only add the given extension on top of it -- -- >>> addExtension "/directory/path.ext1" "ext2" :: AbsPath -- /directory/path.ext1.ext2 -- >>> addExtension "directory/path.ext1" "ext2" :: RelPath -- directory/path.ext1.ext2 -- -- This will not add an extension if the path is empty. -- -- >>> addExtension "." "ext" :: RelPath -- . -- >>> addExtension "/" "ext" :: AbsPath -- / -- -- Replaces @System.FilePath.addExtension@ addExtension :: Path rel -> Extension -> Path rel addExtension path extension | isEmptyPath path = path | otherwise = path { pathExtensions = pathExtensions path ++ [extension] } -- | Add an extension to a path (equivalent to 'addExtension') -- -- >>> "/directory/path" <.> "ext" :: AbsPath -- /directory/path.ext -- >>> "directory/path" <.> "ext" :: RelPath -- directory/path.ext -- >>> "/directory/path.ext1" <.> "ext2" :: AbsPath -- /directory/path.ext1.ext2 -- >>> "directory/path.ext1" <.> "ext2" :: RelPath -- directory/path.ext1.ext2 -- >>> "." <.> "ext" :: RelPath -- . -- >>> "/" <.> "ext" :: AbsPath -- / -- -- Replaces @System.FilePath.(<.>)@ (<.>) :: Path rel -> Extension -> Path rel (<.>) = addExtension -- | Add a list of extensions to a path -- -- >>> addExtensions "/directory/path" ["ext1", "ext2"] :: AbsPath -- /directory/path.ext1.ext2 -- >>> addExtensions "directory/path" ["ext1", "ext2"] :: RelPath -- directory/path.ext1.ext2 -- -- >>> addExtensions "/directory/path.ext1" ["ext2", "ext3"] :: AbsPath -- /directory/path.ext1.ext2.ext3 -- >>> addExtensions "directory/path.ext1" ["ext2", "ext3"] :: RelPath -- directory/path.ext1.ext2.ext3 -- -- >>> addExtensions "." ["ext1", "ext2"] :: RelPath -- . -- >>> addExtensions "/" ["ext1", "ext2"] :: AbsPath -- / -- -- This operation is an identity function if the given list of extensions -- is empty. addExtensions :: Path rel -> [Extension] -> Path rel addExtensions = foldl' addExtension -- | Drop the given extension from a FilePath. -- Fails if the FilePath does not have the given extension. -- -- >>> stripExtension "foo.x.hs.o" "o" :: Maybe RelPath -- Just foo.x.hs -- >>> stripExtension "foo.x.hs.o" "hs" :: Maybe RelPath -- Nothing -- >>> stripExtension "a.b.c.d" "d" :: Maybe RelPath -- Just a.b.c -- >>> stripExtension "foo.bar" "baz" :: Maybe RelPath -- Nothing -- >>> stripExtension "foobar" "bar" :: Maybe RelPath -- Nothing -- -- Replaces @System.FilePath.stripExtension@ stripExtension :: Path rel -> Extension -> Maybe (Path rel) stripExtension p e = stripExtensions p [e] -- | Drop the given extensions from a FilePath. -- Fails if the FilePath does not have all of the given extensions. -- -- >>> stripExtensions "foo.x.hs.o" ["hs", "o"] :: Maybe RelPath -- Just foo.x -- >>> stripExtensions "foo.x.hs.o" ["o", "hs"] :: Maybe RelPath -- Nothing -- >>> stripExtensions "a.b.c.d" ["c", "d"] :: Maybe RelPath -- Just a.b -- >>> stripExtensions "foo.bar" ["baz", "quux"] :: Maybe RelPath -- Nothing -- >>> stripExtensions "foobar" ["bar"] :: Maybe RelPath -- Nothing stripExtensions :: Path rel -> [Extension] -> Maybe (Path rel) stripExtensions (Path ps lp es) esq = (Path ps lp . reverse) <$> stripPrefix (reverse esq) (reverse es) -- | Split off the extensions from a path -- -- >>> splitExtension ("dir/file.ext1.ext2" :: RelPath) -- Just (dir/file.ext1,ext2) -- >>> splitExtension ("dir/file.ext" :: RelPath) -- Just (dir/file,ext) -- >>> splitExtension ("dir/file" :: RelPath) -- Nothing -- >>> splitExtension ("/dir/file.ext1.ext2" :: AbsPath) -- Just (/dir/file.ext1,ext2) -- >>> splitExtension ("/dir/file.ext" :: AbsPath) -- Just (/dir/file,ext) -- >>> splitExtension ("/dir/file" :: AbsPath) -- Nothing -- >>> splitExtension ("." :: RelPath) -- Nothing -- >>> splitExtension ("/" :: AbsPath) -- Nothing -- -- Replaces @System.FilePath.splitExtension@ splitExtension :: Path rel -> Maybe (Path rel, Extension) splitExtension p = (,) (dropExtension p) <$> takeExtension p -- | Split off the extensions from a path -- -- >>> splitExtensions ("dir/file.ext1.ext2" :: RelPath) -- (dir/file,[ext1,ext2]) -- >>> splitExtensions ("dir/file.ext" :: RelPath) -- (dir/file,[ext]) -- >>> splitExtensions ("dir/file" :: RelPath) -- (dir/file,[]) -- >>> splitExtensions ("/dir/file.ext1.ext2" :: AbsPath) -- (/dir/file,[ext1,ext2]) -- >>> splitExtensions ("/dir/file.ext" :: AbsPath) -- (/dir/file,[ext]) -- >>> splitExtensions ("/dir/file" :: AbsPath) -- (/dir/file,[]) -- >>> splitExtensions ("." :: RelPath) -- (.,[]) -- >>> splitExtensions ("/" :: AbsPath) -- (/,[]) splitExtensions :: Path rel -> (Path rel, [Extension]) splitExtensions p = (dropExtensions p, takeExtensions p) -- | Check whether the given filepath has any extensions -- -- >>> hasExtension ("/directory/path.ext" :: AbsPath) -- True -- >>> hasExtension ("/directory/path" :: AbsPath) -- False -- -- Replaces @System.FilePath.hasExtension@ hasExtension :: Path rel -> Bool hasExtension = not . null . takeExtensions -- | Split a path into all but the last piece and the last piece and the -- extensions -- -- >>> splitFileName ("/directory/file.ext" :: AbsPath) -- (/directory,file.ext) -- >>> splitFileName ("file/bob.txt" :: RelPath) -- (file,bob.txt) -- >>> splitFileName ("file" :: RelPath) -- (.,file) -- >>> splitFileName ("dir.ext/file.ext" :: RelPath) -- (dir.ext,file.ext) splitFileName :: Path rel -> (Path rel, RelPath) splitFileName p = (dropFileName p, takeFileName p) -- | Take the last piece and the extensions, exactly. -- -- This will evaluate to 'Nothing' if the given path is empty -- -- >>> takeFileNameExact ("/directory/file.ext" :: AbsPath) -- Just file.ext -- >>> takeFileNameExact ("file/bob.txt" :: RelPath) -- Just bob.txt -- >>> takeFileNameExact ("file" :: RelPath) -- Just file -- >>> takeFileNameExact ("dir.ext/file.ext" :: RelPath) -- Just file.ext -- >>> takeFileNameExact ("." :: RelPath) -- Nothing -- >>> takeFileNameExact ("/" :: AbsPath) -- Nothing -- -- Replaces @System.FilePath.takeFileName@ takeFileNameExact :: Path rel -> Maybe RelPath takeFileNameExact p@(Path _ lp es) | isEmptyPath p = Nothing | otherwise = Just $ Path [] lp es -- | Take the last piece and the extensions. -- -- This will evaluate to the empty (relative) path if the given path is empty. -- -- >>> takeFileName ("/directory/file.ext" :: AbsPath) -- file.ext -- >>> takeFileName ("file/bob.txt" :: RelPath) -- bob.txt -- >>> takeFileName ("file" :: RelPath) -- file -- >>> takeFileName ("dir.ext/file.ext" :: RelPath) -- file.ext -- >>> takeFileName ("." :: RelPath) -- . -- >>> takeFileName ("/" :: AbsPath) -- . -- -- Replaces @System.FilePath.takeFileName@ takeFileName :: Path rel -> RelPath takeFileName p = case takeFileNameExact p of Nothing -> emptyPath Just r -> r -- | Replace the last piece of a path with the given last piece. -- -- >>> replaceFileNameExact "/directory/other.txt" "file.ext" :: Maybe AbsPath -- Just /directory/file.ext -- >>> replaceFileNameExact "." "file.ext" :: Maybe RelPath -- Just file.ext -- >>> replaceFileNameExact "/" "file.ext" :: Maybe AbsPath -- Just /file.ext -- -- If the given path piece is degenerate, this is what happens: -- -- >>> replaceFileNameExact "/directory/other.txt" "..." :: Maybe AbsPath -- Nothing replaceFileNameExact :: Path rel -> PathPiece -> Maybe (Path rel) replaceFileNameExact (Path ps _ _) p = let (lp, es) = splitPiece p in if isEmptyLastPathPiece lp && (not (null es) || not (null ps)) then Nothing else Just $ Path ps lp es -- | Replace the last piece of a path with the given last piece. -- -- >>> replaceFileName "/directory/other.txt" "file.ext" :: AbsPath -- /directory/file.ext -- >>> replaceFileName "." "file.ext" :: RelPath -- file.ext -- >>> replaceFileName "/" "file.ext" :: AbsPath -- /file.ext -- -- If the given path piece is degenerate, this is what happens: -- -- >>> replaceFileName "/directory/other.txt" "..." :: AbsPath -- /directory replaceFileName :: Path rel -> PathPiece -> Path rel replaceFileName path p = case replaceFileNameExact path p of Nothing -> dropFileName path Just rs -> rs -- | Drop the last piece of a path, exactly -- -- >>> dropFileNameExact ("directory/file.ext" :: RelPath) -- Just directory -- >>> dropFileNameExact ("/directory/file.ext" :: AbsPath) -- Just /directory -- -- This evaluates to Nothing when given an empty path -- -- >>> dropFileNameExact ("/" :: AbsPath) -- Nothing -- >>> dropFileNameExact ("." :: RelPath) -- Nothing dropFileNameExact :: Path rel -> Maybe (Path rel) dropFileNameExact (Path psc _ _) = case unsnoc psc of Nothing -> Nothing Just (ps, p) -> let (lp, es) = splitPiece p in if isEmptyLastPathPiece lp then Nothing -- TODO(syd) fixme: really ugly else Just $ Path ps lp es -- | Drop the last piece of a path -- -- >>> dropFileName ("directory/file.ext" :: RelPath) -- directory -- >>> dropFileName ("/directory/file.ext" :: AbsPath) -- /directory -- -- This evaluates to an empty path when given an empty path -- -- >>> dropFileName ("/" :: AbsPath) -- / -- >>> dropFileName ("." :: RelPath) -- . -- -- Replaces @System.FilePath.dropFileName@ and @System.FilePath.takeDirectory@ dropFileName :: Path rel -> Path rel dropFileName p = case dropFileNameExact p of Nothing -> emptyPath Just rs -> rs -- | Take the last piece (no extensions) -- -- >>> takeBaseNameExact ("file.ext" :: RelPath) -- Just file -- >>> takeBaseNameExact ("dir/and/file.ext" :: RelPath) -- Just file -- -- This will evaluate to Nothing when given an empty path: -- -- >>> takeBaseNameExact ("." :: RelPath) -- Nothing -- >>> takeBaseNameExact ("/" :: AbsPath) -- Nothing takeBaseNameExact :: Path rel -> Maybe LastPathPiece takeBaseNameExact p@(Path _ lp _) | isEmptyPath p = Nothing | otherwise = Just lp -- | Take the last piece (no extensions) -- -- >>> takeBaseName ("file.ext" :: RelPath) -- file -- >>> takeBaseName ("dir/and/file.ext" :: RelPath) -- file -- -- This will evaluate to an empty last path piece when given an empty path: -- -- Replaces @System.FilePath.takeBaseName@ takeBaseName :: Path rel -> LastPathPiece takeBaseName p = case takeBaseNameExact p of Nothing -> emptyLastPathPiece Just rs -> rs -- | Replace the last piece exactly: fails on empty last piece -- -- >>> replaceBaseNameExact "file.ext" "piece" :: Maybe RelPath -- Just piece.ext -- >>> replaceBaseNameExact "." "thing" :: Maybe RelPath -- Just thing -- >>> replaceBaseNameExact "/" "thing" :: Maybe AbsPath -- Just /thing -- >>> replaceBaseNameExact "/directory/file" "" :: Maybe AbsPath -- Nothing replaceBaseNameExact :: Path rel -> LastPathPiece -> Maybe (Path rel) replaceBaseNameExact (Path ps _ es) lp | isEmptyLastPathPiece lp = Nothing | otherwise = Just $ Path ps lp es -- | Replace the last piece -- -- >>> replaceBaseName "file.ext" "piece" :: RelPath -- piece.ext -- >>> replaceBaseName "." "thing" :: RelPath -- thing -- >>> replaceBaseName "/" "thing" :: AbsPath -- /thing -- >>> replaceBaseName "/directory/file" "" :: AbsPath -- /directory -- -- Replaces @System.FilePath.replaceBaseName@ replaceBaseName :: Path rel -> LastPathPiece -> Path rel replaceBaseName p@(Path ps _ es) lp | isEmptyLastPathPiece lp = dropFileName p | otherwise = Path ps lp es -- | Replace everthing but the last piece, exactly -- -- >>> replaceDirectoryExact ("/dir/and/file" :: AbsPath) ("other/directory" :: RelPath) -- Just other/directory/file -- -- This will evaluate to 'Nothing' if the first argument is an empty path. -- -- >>> replaceDirectoryExact ("." :: RelPath) ("a/directory" :: RelPath) -- Nothing -- >>> replaceDirectoryExact ("/" :: AbsPath) ("a/directory" :: RelPath) -- Nothing -- -- -- This will evaluate to 'Nothing' if the second argument is an empty path. -- -- >>> replaceDirectoryExact ("dir/file" :: RelPath) ("." :: RelPath) -- Nothing -- >>> replaceDirectoryExact ("dir/file" :: RelPath) ("/" :: AbsPath) -- Nothing replaceDirectoryExact :: Path r -> Path s -> Maybe (Path s) replaceDirectoryExact p@(Path _ lp es) q@(Path ps' lp' es') | isEmptyPath p = Nothing | isEmptyPath q = Nothing | otherwise = let p = combineLastAndExtensions lp' es' in Just $ Path (ps' ++ [p]) lp es -- | Replace everthing but the last piece -- -- >>> replaceDirectory ("/dir/and/file" :: AbsPath) ("other/directory" :: RelPath) -- other/directory/file -- >>> replaceDirectory ("." :: RelPath) ("a/directory" :: RelPath) -- a/directory -- >>> replaceDirectory ("/" :: AbsPath) ("a/directory" :: RelPath) -- a/directory -- >>> replaceDirectory ("dir/file" :: RelPath) ("." :: RelPath) -- file -- >>> replaceDirectory ("dir/file" :: RelPath) ("/" :: AbsPath) -- /file -- -- Replaces @System.FilePath.replaceDirectory@ replaceDirectory :: Path r -> Path s -> Path s replaceDirectory p@(Path _ lp es) q@(Path ps' lp' es') | isEmptyPath p = q | isEmptyPath q = Path [] lp es | otherwise = let p = combineLastAndExtensions lp' es' in Path (ps' ++ [p]) lp es -- | Combine two paths, exactly -- -- If the first path has extensions, they will be appended to the last -- pathpiece before concatenation -- -- >>> combineExact "/directory/path" "another/path.ext" :: Maybe AbsPath -- Just /directory/path/another/path.ext -- >>> combineExact "directory/path" "another/path.ext" :: Maybe RelPath -- Just directory/path/another/path.ext -- >>> combineExact "/file.ext1.ext2" "other/file.ext3" :: Maybe AbsPath -- Just /file.ext1.ext2/other/file.ext3 -- >>> combineExact "file.ext1.ext2" "other/file.ext3" :: Maybe RelPath -- Just file.ext1.ext2/other/file.ext3 -- -- This evaluates to 'Nothing' if any of the given paths are empty -- -- >>> combineExact "." "file.ext" :: Maybe RelPath -- Nothing -- >>> combineExact "/" "file.ext" :: Maybe AbsPath -- Nothing combineExact :: Path rel -> RelPath -> Maybe (Path rel) combineExact p1 p2 | isEmptyPath p1 || isEmptyPath p2 = Nothing | otherwise = Just Path { pathPieces = pathPieces p1 ++ [combineLastAndExtensions (pathLastPiece p1) (pathExtensions p1)] ++ pathPieces p2 , pathLastPiece = pathLastPiece p2 , pathExtensions = pathExtensions p2 } -- | Combine two paths -- -- If the first path has extensions, they will be appended to the last -- pathpiece before concatenation -- -- >>> combine "/directory/path" "another/path.ext" :: AbsPath -- /directory/path/another/path.ext -- >>> combine "directory/path" "another/path.ext" :: RelPath -- directory/path/another/path.ext -- >>> combine "/file.ext1.ext2" "other/file.ext3" :: AbsPath -- /file.ext1.ext2/other/file.ext3 -- >>> combine "file.ext1.ext2" "other/file.ext3" :: RelPath -- file.ext1.ext2/other/file.ext3 -- -- This treats empty paths as identities to the operation. -- -- >>> combine "file.ext" "." :: RelPath -- file.ext -- >>> combine "." "file.ext" :: RelPath -- file.ext -- >>> combine "/" "file.ext" :: AbsPath -- /file.ext -- >>> combine "." "." :: RelPath -- . -- >>> combine "/" "." :: AbsPath -- / -- -- Replaces @System.FilePath.combine@ combine :: Path rel -> RelPath -> Path rel combine p1 p2 | isEmptyPath p1 && isEmptyPath p2 = emptyPath | isEmptyPath p2 = p1 | isEmptyPath p1 = unsafePathTypeCoerse p2 | otherwise = Path { pathPieces = pathPieces p1 ++ [combineLastAndExtensions (pathLastPiece p1) (pathExtensions p1)] ++ pathPieces p2 , pathLastPiece = pathLastPiece p2 , pathExtensions = pathExtensions p2 } -- | Combine two paths -- -- equivalent to 'combine' -- -- >>> "/directory/path" "another/path.ext" :: AbsPath -- /directory/path/another/path.ext -- >>> "directory/path" "another/path.ext" :: RelPath -- directory/path/another/path.ext -- >>> "/file.ext1.ext2" "other/file.ext3" :: AbsPath -- /file.ext1.ext2/other/file.ext3 -- >>> "file.ext1.ext2" "other/file.ext3" :: RelPath -- file.ext1.ext2/other/file.ext3 -- >>> "." "file.ext" :: RelPath -- file.ext -- >>> "/" "file.ext" :: AbsPath -- /file.ext -- -- Replaces @System.FilePath.()@ () :: Path rel -> RelPath -> Path rel () = combine -- | Split a path up into pieces -- -- >>> splitPath ("/a/full/absolute/directory/path" :: AbsPath) -- [a,full,absolute,directory,path] splitPath :: Path rel -> [PathPiece] splitPath (Path ps lp es) = ps ++ filter isValid [combineLastAndExtensions lp es] -- | Join path pieces back into a path -- -- >>> joinPath ["a", "full", "absolute", "directory", "path"] :: Maybe AbsPath -- Just /a/full/absolute/directory/path -- >>> joinPath [] :: Maybe RelPath -- Just . -- >>> joinPath [] :: Maybe AbsPath -- Just / -- >>> joinPath [".", "."] :: Maybe RelPath -- Nothing joinPath :: [PathPiece] -> Maybe (Path rel) joinPath ps = case unsnoc ps of Nothing -> Just $ emptyPath Just (ips, p) -> let (lp, es) = splitPiece p in constructValid $ Path ips lp es --- [ UTILS ] --- initMay :: [a] -> Maybe [a] initMay [] = Nothing initMay as = Just $ reverse $ tail $ reverse as lastMay :: [a] -> Maybe a lastMay [] = Nothing lastMay as = Just $ head $ reverse as unsnoc :: [a] -> Maybe ([a], a) unsnoc as = (,) <$> initMay as <*> lastMay as uncons :: [a] -> Maybe (a, [a]) uncons [] = Nothing uncons (a:as) = Just (a, as) isEmptyLastPathPiece :: LastPathPiece -> Bool isEmptyLastPathPiece = (== emptyLastPathPiece) emptyLastPathPiece :: LastPathPiece emptyLastPathPiece = (LastPathPiece T.empty) emptyPath :: Path rel emptyPath = (Path [] emptyLastPathPiece []) isEmptyPath :: Path rel -> Bool isEmptyPath p = p == emptyPath renderPiece :: PathPiece -> String renderPiece (PathPiece p) = T.unpack p renderLastPiece :: LastPathPiece -> String renderLastPiece (LastPathPiece p) = T.unpack p renderExtension :: Extension -> String renderExtension (Extension e) = T.unpack e renderExtensions :: [Extension] -> String renderExtensions [] = [] renderExtensions es = [extensionSeparator] ++ intercalate [extensionSeparator] (map renderExtension es) combineLastAndExtensions :: LastPathPiece -> [Extension] -> PathPiece combineLastAndExtensions (LastPathPiece lpp) es = PathPiece $ lpp <> T.pack (renderExtensions es) splitPiece :: PathPiece -> (LastPathPiece, [Extension]) splitPiece (PathPiece t) = let rawExts = filter (not . T.null) $ T.split (== extensionSeparator) t in case uncons rawExts of Nothing -> (emptyLastPathPiece, []) Just (lastPieceStr, safeExts) -> let lastPiece = LastPathPiece lastPieceStr exts = map Extension safeExts in (lastPiece, exts) unsafePathTypeCoerse :: Path rel -> Path rel' unsafePathTypeCoerse (Path pieces lastPiece exts) = Path pieces lastPiece exts