-- | A module for 'FilePath' operations exposing "System.FilePath" plus some additional operations. -- -- /Windows note:/ The extension methods ('<.>', 'takeExtension' etc) use the Posix variants since on -- Windows @\"\/\/\*\" '<.>' \"txt\"@ produces @\"\/\/\*\\\\.txt\"@ -- (which is bad for 'Development.Shake.FilePattern' values). module Development.Shake.FilePath( module System.FilePath, module System.FilePath.Posix, dropDirectory1, takeDirectory1, replaceDirectory1, makeRelativeEx, normaliseEx, toNative, toStandard, exe ) where import System.Directory (canonicalizePath) import System.Info.Extra import Data.List.Extra import qualified System.FilePath as Native import System.FilePath hiding (splitExtension, takeExtension, replaceExtension, dropExtension, addExtension ,hasExtension, (<.>), splitExtensions, takeExtensions, dropExtensions ) import System.FilePath.Posix (splitExtension, takeExtension, replaceExtension, dropExtension, addExtension ,hasExtension, (<.>), splitExtensions, takeExtensions, dropExtensions ) -- | Drop the first directory from a 'FilePath'. Should only be used on -- relative paths. -- -- > dropDirectory1 "aaa/bbb" == "bbb" -- > dropDirectory1 "aaa/" == "" -- > dropDirectory1 "aaa" == "" -- > dropDirectory1 "" == "" dropDirectory1 :: FilePath -> FilePath dropDirectory1 = drop1 . dropWhile (not . isPathSeparator) -- | Take the first component of a 'FilePath'. Should only be used on -- relative paths. -- -- > takeDirectory1 "aaa/bbb" == "aaa" -- > takeDirectory1 "aaa/" == "aaa" -- > takeDirectory1 "aaa" == "aaa" takeDirectory1 :: FilePath -> FilePath takeDirectory1 = takeWhile (not . isPathSeparator) -- | Replace the first component of a 'FilePath'. Should only be used on -- relative paths. -- -- > replaceDirectory1 "root/file.ext" "directory" == "directory/file.ext" -- > replaceDirectory1 "root/foo/bar/file.ext" "directory" == "directory/foo/bar/file.ext" replaceDirectory1 :: FilePath -> String -> FilePath replaceDirectory1 x dir = dir dropDirectory1 x -- | Make a path relative. Returns Nothing only when the given paths are on -- different drives. This will try the pure function makeRelative first. If that -- fails, the paths are canonicalised (removing any indirection and symlinks) -- and a relative path is derived from there. -- -- > > -- Given that "/root/a/" is not a symlink -- > > makeRelativeEx "/root/a/" "/root/b/file.out" -- > Just "../b/file.out" -- > -- > > -- Given that "/root/c/" is a symlink to "/root/e/f/g/" -- > > makeRelativeEx "/root/c/" "/root/b/file.out" -- > Just "../../../b/file.out" -- > -- > > -- On Windows -- > > makeRelativeEx "C:\\foo" "D:\\foo\\bar" -- > Nothing -- makeRelativeEx :: FilePath -> FilePath -> IO (Maybe FilePath) makeRelativeEx pathA pathB | isRelative makeRelativePathAPathB = pure (Just makeRelativePathAPathB) | otherwise = do a' <- canonicalizePath pathA b' <- canonicalizePath pathB if takeDrive a' /= takeDrive b' then pure Nothing else Just <$> makeRelativeEx' a' b' where makeRelativePathAPathB = makeRelative pathA pathB makeRelativeEx' :: FilePath -> FilePath -> IO FilePath makeRelativeEx' a b = do let rel = makeRelative a b parent = takeDirectory a if isRelative rel then pure rel else if a /= parent then do parentToB <- makeRelativeEx' parent b pure (".." parentToB) -- Impossible: makeRelative should have succeeded in finding -- a relative path once `a == "/"`. else error $ "Error calculating relative path from \"" ++ pathA ++ "\" to \"" ++ show pathB ++ "\"" -- | Normalise a 'FilePath', applying the rules: -- -- * All 'pathSeparators' become 'pathSeparator' (@\/@ on Linux, @\\@ on Windows) -- -- * @foo\/bar\/..\/baz@ becomes @foo\/baz@ (not universally true in the presence of symlinks) -- -- * @foo\/.\/bar@ becomes @foo\/bar@ -- -- * @foo\/\/bar@ becomes @foo\/bar@ -- -- This function is not based on the 'normalise' function from the @filepath@ library, as that function -- is quite broken. normaliseEx :: FilePath -> FilePath normaliseEx xs | a:b:xs <- xs, isWindows && sep a && sep b = '/' : f ('/':xs) -- account for UNC paths being double // | otherwise = f xs where sep = Native.isPathSeparator f o = toNative $ deslash o $ (++"/") $ concatMap ('/':) $ reverse $ g 0 $ reverse $ split o deslash o x | x == "/" = case (pre,pos) of (True,True) -> "/" (True,False) -> "/." (False,True) -> "./" (False,False) -> "." | otherwise = (if pre then id else tail) $ (if pos then id else init) x where pre = sep $ head $ o ++ " " pos = sep $ last $ " " ++ o g i [] = replicate i ".." g i ("..":xs) = g (i+1) xs g i (".":xs) = g i xs g 0 (x:xs) = x : g 0 xs g i (_:xs) = g (i-1) xs -- equivalent to eliminating ../x split xs = if null ys then [] else a : split b where (a,b) = break sep ys ys = dropWhile sep xs -- | Convert to native path separators, namely @\\@ on Windows. toNative :: FilePath -> FilePath toNative = if isWindows then map (\x -> if x == '/' then '\\' else x) else id -- | Convert all path separators to @/@, even on Windows. toStandard :: FilePath -> FilePath toStandard = if isWindows then map (\x -> if x == '\\' then '/' else x) else id -- | The extension of executables, @\"exe\"@ on Windows and @\"\"@ otherwise. exe :: String exe = if isWindows then "exe" else ""