filepath-1.4.1.2: Library for manipulating FilePaths in a cross platform way.

Copyright(c) Neil Mitchell 2005-2014
LicenseBSD3
Maintainerndmitchell@gmail.com
Stabilitystable
Portabilityportable
Safe HaskellSafe
LanguageHaskell2010

System.FilePath.Windows

Contents

Description

A library for FilePath manipulations, using Windows style paths on all platforms. Importing System.FilePath is usually better.

Given the example FilePath: /directory/file.ext

We can use the following functions to extract pieces.

And we could have built an equivalent path with the following expressions:

  • "/directory" </> "file.ext".
  • "/directory/file" <.> "ext".
  • "/directory/file.txt" -<.> "ext".

Each function in this module is documented with several examples, which are also used as tests.

Here are a few examples of using the filepath functions together:

Example 1: Find the possible locations of a Haskell module Test imported from module Main:

[replaceFileName path_to_main "Test" <.> ext | ext <- ["hs","lhs"] ]

Example 2: Download a file from url and save it to disk:

do let file = makeValid url
   System.IO.createDirectoryIfMissing True (takeDirectory file)

Example 3: Compile a Haskell file, putting the .hi file under interface:

takeDirectory file </> "interface" </> (takeFileName file -<.> "hi")

References: [1] Naming Files, Paths and Namespaces (Microsoft MSDN)

Synopsis

Separator predicates

type FilePath = String Source #

File and directory names are values of type String, whose precise meaning is operating system dependent. Files can be opened, yielding a handle which can then be used to operate on the contents of that file.

pathSeparator :: Char #

The character that separates directories. In the case where more than one character is possible, pathSeparator is the 'ideal' one.

Windows: pathSeparator == '\\'
Posix:   pathSeparator ==  '/'
isPathSeparator pathSeparator

pathSeparators :: [Char] #

The list of all possible separators.

Windows: pathSeparators == ['\\', '/']
Posix:   pathSeparators == ['/']
pathSeparator `elem` pathSeparators

isPathSeparator :: Char -> Bool #

Rather than using (== pathSeparator), use this. Test if something is a path separator.

isPathSeparator a == (a `elem` pathSeparators)

searchPathSeparator :: Char #

The character that is used to separate the entries in the $PATH environment variable.

Windows: searchPathSeparator == ';'
Posix:   searchPathSeparator == ':'

isSearchPathSeparator :: Char -> Bool #

Is the character a file separator?

isSearchPathSeparator a == (a == searchPathSeparator)

extSeparator :: Char #

File extension character

extSeparator == '.'

isExtSeparator :: Char -> Bool #

Is the character an extension character?

isExtSeparator a == (a == extSeparator)

$PATH methods

splitSearchPath :: String -> [FilePath] #

Take a string, split it on the searchPathSeparator character. Blank items are ignored on Windows, and converted to . on Posix. On Windows path elements are stripped of quotes.

Follows the recommendations in http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html

Posix:   splitSearchPath "File1:File2:File3"  == ["File1","File2","File3"]
Posix:   splitSearchPath "File1::File2:File3" == ["File1",".","File2","File3"]
Windows: splitSearchPath "File1;File2;File3"  == ["File1","File2","File3"]
Windows: splitSearchPath "File1;;File2;File3" == ["File1","File2","File3"]
Windows: splitSearchPath "File1;\"File2\";File3" == ["File1","File2","File3"]

getSearchPath :: IO [FilePath] #

Get a list of FilePaths in the $PATH variable.

Extension functions

splitExtension :: FilePath -> (String, String) #

Split on the extension. addExtension is the inverse.

splitExtension "/directory/path.ext" == ("/directory/path",".ext")
uncurry (++) (splitExtension x) == x
Valid x => uncurry addExtension (splitExtension x) == x
splitExtension "file.txt" == ("file",".txt")
splitExtension "file" == ("file","")
splitExtension "file/file.txt" == ("file/file",".txt")
splitExtension "file.txt/boris" == ("file.txt/boris","")
splitExtension "file.txt/boris.ext" == ("file.txt/boris",".ext")
splitExtension "file/path.txt.bob.fred" == ("file/path.txt.bob",".fred")
splitExtension "file/path.txt/" == ("file/path.txt/","")

takeExtension :: FilePath -> String #

Get the extension of a file, returns "" for no extension, .ext otherwise.

takeExtension "/directory/path.ext" == ".ext"
takeExtension x == snd (splitExtension x)
Valid x => takeExtension (addExtension x "ext") == ".ext"
Valid x => takeExtension (replaceExtension x "ext") == ".ext"

replaceExtension :: FilePath -> String -> FilePath #

Set the extension of a file, overwriting one if already present, equivalent to -<.>.

replaceExtension "/directory/path.txt" "ext" == "/directory/path.ext"
replaceExtension "/directory/path.txt" ".ext" == "/directory/path.ext"
replaceExtension "file.txt" ".bob" == "file.bob"
replaceExtension "file.txt" "bob" == "file.bob"
replaceExtension "file" ".bob" == "file.bob"
replaceExtension "file.txt" "" == "file"
replaceExtension "file.fred.bob" "txt" == "file.fred.txt"
replaceExtension x y == addExtension (dropExtension x) y

(-<.>) :: FilePath -> String -> FilePath infixr 7 #

Remove the current extension and add another, equivalent to replaceExtension.

"/directory/path.txt" -<.> "ext" == "/directory/path.ext"
"/directory/path.txt" -<.> ".ext" == "/directory/path.ext"
"foo.o" -<.> "c" == "foo.c"

dropExtension :: FilePath -> FilePath #

Remove last extension, and the "." preceding it.

dropExtension "/directory/path.ext" == "/directory/path"
dropExtension x == fst (splitExtension x)

addExtension :: FilePath -> String -> FilePath #

Add an extension, even if there is already one there, equivalent to <.>.

addExtension "/directory/path" "ext" == "/directory/path.ext"
addExtension "file.txt" "bib" == "file.txt.bib"
addExtension "file." ".bib" == "file..bib"
addExtension "file" ".bib" == "file.bib"
addExtension "/" "x" == "/.x"
addExtension x "" == x
Valid x => takeFileName (addExtension (addTrailingPathSeparator x) "ext") == ".ext"
Windows: addExtension "\\\\share" ".txt" == "\\\\share\\.txt"

hasExtension :: FilePath -> Bool #

Does the given filename have an extension?

hasExtension "/directory/path.ext" == True
hasExtension "/directory/path" == False
null (takeExtension x) == not (hasExtension x)

(<.>) :: FilePath -> String -> FilePath infixr 7 #

Add an extension, even if there is already one there, equivalent to addExtension.

"/directory/path" <.> "ext" == "/directory/path.ext"
"/directory/path" <.> ".ext" == "/directory/path.ext"

splitExtensions :: FilePath -> (FilePath, String) #

Split on all extensions.

splitExtensions "/directory/path.ext" == ("/directory/path",".ext")
splitExtensions "file.tar.gz" == ("file",".tar.gz")
uncurry (++) (splitExtensions x) == x
Valid x => uncurry addExtension (splitExtensions x) == x
splitExtensions "file.tar.gz" == ("file",".tar.gz")

dropExtensions :: FilePath -> FilePath #

Drop all extensions.

dropExtensions "/directory/path.ext" == "/directory/path"
dropExtensions "file.tar.gz" == "file"
not $ hasExtension $ dropExtensions x
not $ any isExtSeparator $ takeFileName $ dropExtensions x

takeExtensions :: FilePath -> String #

Get all extensions.

takeExtensions "/directory/path.ext" == ".ext"
takeExtensions "file.tar.gz" == ".tar.gz"

replaceExtensions :: FilePath -> String -> FilePath #

Replace all extensions of a file with a new extension. Note that replaceExtension and addExtension both work for adding multiple extensions, so only required when you need to drop all extensions first.

replaceExtensions "file.fred.bob" "txt" == "file.txt"
replaceExtensions "file.fred.bob" "tar.gz" == "file.tar.gz"

stripExtension :: String -> FilePath -> Maybe FilePath #

Drop the given extension from a FilePath, and the "." preceding it. Returns Nothing if the FilePath does not have the given extension, or Just and the part before the extension if it does.

This function can be more predictable than dropExtensions, especially if the filename might itself contain . characters.

stripExtension "hs.o" "foo.x.hs.o" == Just "foo.x"
stripExtension "hi.o" "foo.x.hs.o" == Nothing
dropExtension x == fromJust (stripExtension (takeExtension x) x)
dropExtensions x == fromJust (stripExtension (takeExtensions x) x)
stripExtension ".c.d" "a.b.c.d"  == Just "a.b"
stripExtension ".c.d" "a.b..c.d" == Just "a.b."
stripExtension "baz"  "foo.bar"  == Nothing
stripExtension "bar"  "foobar"   == Nothing
stripExtension ""     x          == Just x

Filename/directory functions

splitFileName :: FilePath -> (String, String) #

Split a filename into directory and file. </> is the inverse. The first component will often end with a trailing slash.

splitFileName "/directory/file.ext" == ("/directory/","file.ext")
Valid x => uncurry (</>) (splitFileName x) == x || fst (splitFileName x) == "./"
Valid x => isValid (fst (splitFileName x))
splitFileName "file/bob.txt" == ("file/", "bob.txt")
splitFileName "file/" == ("file/", "")
splitFileName "bob" == ("./", "bob")
Posix:   splitFileName "/" == ("/","")
Windows: splitFileName "c:" == ("c:","")

takeFileName :: FilePath -> FilePath #

Get the file name.

takeFileName "/directory/file.ext" == "file.ext"
takeFileName "test/" == ""
takeFileName x `isSuffixOf` x
takeFileName x == snd (splitFileName x)
Valid x => takeFileName (replaceFileName x "fred") == "fred"
Valid x => takeFileName (x </> "fred") == "fred"
Valid x => isRelative (takeFileName x)

replaceFileName :: FilePath -> String -> FilePath #

Set the filename.

replaceFileName "/directory/other.txt" "file.ext" == "/directory/file.ext"
Valid x => replaceFileName x (takeFileName x) == x

dropFileName :: FilePath -> FilePath #

Drop the filename. Unlike takeDirectory, this function will leave a trailing path separator on the directory.

dropFileName "/directory/file.ext" == "/directory/"
dropFileName x == fst (splitFileName x)

takeBaseName :: FilePath -> String #

Get the base name, without an extension or path.

takeBaseName "/directory/file.ext" == "file"
takeBaseName "file/test.txt" == "test"
takeBaseName "dave.ext" == "dave"
takeBaseName "" == ""
takeBaseName "test" == "test"
takeBaseName (addTrailingPathSeparator x) == ""
takeBaseName "file/file.tar.gz" == "file.tar"

replaceBaseName :: FilePath -> String -> FilePath #

Set the base name.

replaceBaseName "/directory/other.ext" "file" == "/directory/file.ext"
replaceBaseName "file/test.txt" "bob" == "file/bob.txt"
replaceBaseName "fred" "bill" == "bill"
replaceBaseName "/dave/fred/bob.gz.tar" "new" == "/dave/fred/new.tar"
Valid x => replaceBaseName x (takeBaseName x) == x

takeDirectory :: FilePath -> FilePath #

Get the directory name, move up one level.

          takeDirectory "/directory/other.ext" == "/directory"
          takeDirectory x `isPrefixOf` x || takeDirectory x == "."
          takeDirectory "foo" == "."
          takeDirectory "/" == "/"
          takeDirectory "/foo" == "/"
          takeDirectory "/foo/bar/baz" == "/foo/bar"
          takeDirectory "/foo/bar/baz/" == "/foo/bar/baz"
          takeDirectory "foo/bar/baz" == "foo/bar"
Windows:  takeDirectory "foo\\bar" == "foo"
Windows:  takeDirectory "foo\\bar\\\\" == "foo\\bar"
Windows:  takeDirectory "C:\\" == "C:\\"

replaceDirectory :: FilePath -> String -> FilePath #

Set the directory, keeping the filename the same.

replaceDirectory "root/file.ext" "/directory/" == "/directory/file.ext"
Valid x => replaceDirectory x (takeDirectory x) `equalFilePath` x

combine :: FilePath -> FilePath -> FilePath #

An alias for </>.

(</>) :: FilePath -> FilePath -> FilePath infixr 5 #

Combine two paths with a path separator. If the second path starts with a path separator or a drive letter, then it returns the second. The intention is that readFile (dir </> file) will access the same file as setCurrentDirectory dir; readFile file.

Posix:   "/directory" </> "file.ext" == "/directory/file.ext"
Windows: "/directory" </> "file.ext" == "/directory\\file.ext"
         "directory" </> "/file.ext" == "/file.ext"
Valid x => (takeDirectory x </> takeFileName x) `equalFilePath` x

Combined:

Posix:   "/" </> "test" == "/test"
Posix:   "home" </> "bob" == "home/bob"
Posix:   "x:" </> "foo" == "x:/foo"
Windows: "C:\\foo" </> "bar" == "C:\\foo\\bar"
Windows: "home" </> "bob" == "home\\bob"

Not combined:

Posix:   "home" </> "/bob" == "/bob"
Windows: "home" </> "C:\\bob" == "C:\\bob"

Not combined (tricky):

On Windows, if a filepath starts with a single slash, it is relative to the root of the current drive. In [1], this is (confusingly) referred to as an absolute path. The current behavior of </> is to never combine these forms.

Windows: "home" </> "/bob" == "/bob"
Windows: "home" </> "\\bob" == "\\bob"
Windows: "C:\\home" </> "\\bob" == "\\bob"

On Windows, from [1]: "If a file name begins with only a disk designator but not the backslash after the colon, it is interpreted as a relative path to the current directory on the drive with the specified letter." The current behavior of </> is to never combine these forms.

Windows: "D:\\foo" </> "C:bar" == "C:bar"
Windows: "C:\\foo" </> "C:bar" == "C:bar"

splitPath :: FilePath -> [FilePath] #

Split a path by the directory separator.

splitPath "/directory/file.ext" == ["/","directory/","file.ext"]
concat (splitPath x) == x
splitPath "test//item/" == ["test//","item/"]
splitPath "test/item/file" == ["test/","item/","file"]
splitPath "" == []
Windows: splitPath "c:\\test\\path" == ["c:\\","test\\","path"]
Posix:   splitPath "/file/test" == ["/","file/","test"]

joinPath :: [FilePath] -> FilePath #

Join path elements back together.

joinPath ["/","directory/","file.ext"] == "/directory/file.ext"
Valid x => joinPath (splitPath x) == x
joinPath [] == ""
Posix: joinPath ["test","file","path"] == "test/file/path"

splitDirectories :: FilePath -> [FilePath] #

Just as splitPath, but don't add the trailing slashes to each element.

         splitDirectories "/directory/file.ext" == ["/","directory","file.ext"]
         splitDirectories "test/file" == ["test","file"]
         splitDirectories "/test/file" == ["/","test","file"]
Windows: splitDirectories "C:\\test\\file" == ["C:\\", "test", "file"]
         Valid x => joinPath (splitDirectories x) `equalFilePath` x
         splitDirectories "" == []
Windows: splitDirectories "C:\\test\\\\\\file" == ["C:\\", "test", "file"]
         splitDirectories "/test///file" == ["/","test","file"]

Drive functions

splitDrive :: FilePath -> (FilePath, FilePath) #

Split a path into a drive and a path. On Posix, / is a Drive.

uncurry (++) (splitDrive x) == x
Windows: splitDrive "file" == ("","file")
Windows: splitDrive "c:/file" == ("c:/","file")
Windows: splitDrive "c:\\file" == ("c:\\","file")
Windows: splitDrive "\\\\shared\\test" == ("\\\\shared\\","test")
Windows: splitDrive "\\\\shared" == ("\\\\shared","")
Windows: splitDrive "\\\\?\\UNC\\shared\\file" == ("\\\\?\\UNC\\shared\\","file")
Windows: splitDrive "\\\\?\\UNCshared\\file" == ("\\\\?\\","UNCshared\\file")
Windows: splitDrive "\\\\?\\d:\\file" == ("\\\\?\\d:\\","file")
Windows: splitDrive "/d" == ("","/d")
Posix:   splitDrive "/test" == ("/","test")
Posix:   splitDrive "//test" == ("//","test")
Posix:   splitDrive "test/file" == ("","test/file")
Posix:   splitDrive "file" == ("","file")

joinDrive :: FilePath -> FilePath -> FilePath #

Join a drive and the rest of the path.

Valid x => uncurry joinDrive (splitDrive x) == x
Windows: joinDrive "C:" "foo" == "C:foo"
Windows: joinDrive "C:\\" "bar" == "C:\\bar"
Windows: joinDrive "\\\\share" "foo" == "\\\\share\\foo"
Windows: joinDrive "/:" "foo" == "/:\\foo"

takeDrive :: FilePath -> FilePath #

Get the drive from a filepath.

takeDrive x == fst (splitDrive x)

hasDrive :: FilePath -> Bool #

Does a path have a drive.

not (hasDrive x) == null (takeDrive x)
Posix:   hasDrive "/foo" == True
Windows: hasDrive "C:\\foo" == True
Windows: hasDrive "C:foo" == True
         hasDrive "foo" == False
         hasDrive "" == False

dropDrive :: FilePath -> FilePath #

Delete the drive, if it exists.

dropDrive x == snd (splitDrive x)

isDrive :: FilePath -> Bool #

Is an element a drive

Posix:   isDrive "/" == True
Posix:   isDrive "/foo" == False
Windows: isDrive "C:\\" == True
Windows: isDrive "C:\\foo" == False
         isDrive "" == False

Trailing slash functions

hasTrailingPathSeparator :: FilePath -> Bool #

Is an item either a directory or the last character a path separator?

hasTrailingPathSeparator "test" == False
hasTrailingPathSeparator "test/" == True

addTrailingPathSeparator :: FilePath -> FilePath #

Add a trailing file path separator if one is not already present.

hasTrailingPathSeparator (addTrailingPathSeparator x)
hasTrailingPathSeparator x ==> addTrailingPathSeparator x == x
Posix:    addTrailingPathSeparator "test/rest" == "test/rest/"

dropTrailingPathSeparator :: FilePath -> FilePath #

Remove any trailing path separators

dropTrailingPathSeparator "file/test/" == "file/test"
          dropTrailingPathSeparator "/" == "/"
Windows:  dropTrailingPathSeparator "\\" == "\\"
Posix:    not (hasTrailingPathSeparator (dropTrailingPathSeparator x)) || isDrive x

File name manipulations

normalise :: FilePath -> FilePath #

Normalise a file

  • // outside of the drive can be made blank
  • / -> pathSeparator
  • ./ -> ""
Posix:   normalise "/file/\\test////" == "/file/\\test/"
Posix:   normalise "/file/./test" == "/file/test"
Posix:   normalise "/test/file/../bob/fred/" == "/test/file/../bob/fred/"
Posix:   normalise "../bob/fred/" == "../bob/fred/"
Posix:   normalise "./bob/fred/" == "bob/fred/"
Windows: normalise "c:\\file/bob\\" == "C:\\file\\bob\\"
Windows: normalise "c:\\" == "C:\\"
Windows: normalise "C:.\\" == "C:"
Windows: normalise "\\\\server\\test" == "\\\\server\\test"
Windows: normalise "//server/test" == "\\\\server\\test"
Windows: normalise "c:/file" == "C:\\file"
Windows: normalise "/file" == "\\file"
Windows: normalise "\\" == "\\"
Windows: normalise "/./" == "\\"
         normalise "." == "."
Posix:   normalise "./" == "./"
Posix:   normalise "./." == "./"
Posix:   normalise "/./" == "/"
Posix:   normalise "/" == "/"
Posix:   normalise "bob/fred/." == "bob/fred/"
Posix:   normalise "//home" == "/home"

equalFilePath :: FilePath -> FilePath -> Bool #

Equality of two FilePaths. If you call System.Directory.canonicalizePath first this has a much better chance of working. Note that this doesn't follow symlinks or DOSNAM~1s.

         x == y ==> equalFilePath x y
         normalise x == normalise y ==> equalFilePath x y
         equalFilePath "foo" "foo/"
         not (equalFilePath "foo" "/foo")
Posix:   not (equalFilePath "foo" "FOO")
Windows: equalFilePath "foo" "FOO"
Windows: not (equalFilePath "C:" "C:/")

makeRelative :: FilePath -> FilePath -> FilePath #

Contract a filename, based on a relative path. Note that the resulting path will never introduce .. paths, as the presence of symlinks means ../b may not reach a/b if it starts from a/c. For a worked example see this blog post.

The corresponding makeAbsolute function can be found in System.Directory.

         makeRelative "/directory" "/directory/file.ext" == "file.ext"
         Valid x => makeRelative (takeDirectory x) x `equalFilePath` takeFileName x
         makeRelative x x == "."
         Valid x y => equalFilePath x y || (isRelative x && makeRelative y x == x) || equalFilePath (y </> makeRelative y x) x
Windows: makeRelative "C:\\Home" "c:\\home\\bob" == "bob"
Windows: makeRelative "C:\\Home" "c:/home/bob" == "bob"
Windows: makeRelative "C:\\Home" "D:\\Home\\Bob" == "D:\\Home\\Bob"
Windows: makeRelative "C:\\Home" "C:Home\\Bob" == "C:Home\\Bob"
Windows: makeRelative "/Home" "/home/bob" == "bob"
Windows: makeRelative "/" "//" == "//"
Posix:   makeRelative "/Home" "/home/bob" == "/home/bob"
Posix:   makeRelative "/home/" "/home/bob/foo/bar" == "bob/foo/bar"
Posix:   makeRelative "/fred" "bob" == "bob"
Posix:   makeRelative "/file/test" "/file/test/fred" == "fred"
Posix:   makeRelative "/file/test" "/file/test/fred/" == "fred/"
Posix:   makeRelative "some/path" "some/path/a/b/c" == "a/b/c"

isRelative :: FilePath -> Bool #

Is a path relative, or is it fixed to the root?

Windows: isRelative "path\\test" == True
Windows: isRelative "c:\\test" == False
Windows: isRelative "c:test" == True
Windows: isRelative "c:\\" == False
Windows: isRelative "c:/" == False
Windows: isRelative "c:" == True
Windows: isRelative "\\\\foo" == False
Windows: isRelative "\\\\?\\foo" == False
Windows: isRelative "\\\\?\\UNC\\foo" == False
Windows: isRelative "/foo" == True
Windows: isRelative "\\foo" == True
Posix:   isRelative "test/path" == True
Posix:   isRelative "/test" == False
Posix:   isRelative "/" == False

According to [1]:

  • "A UNC name of any format [is never relative]."
  • "You cannot use the "\?" prefix with a relative path."

isAbsolute :: FilePath -> Bool #

not . isRelative
isAbsolute x == not (isRelative x)

isValid :: FilePath -> Bool #

Is a FilePath valid, i.e. could you create a file like it? This function checks for invalid names, and invalid characters, but does not check if length limits are exceeded, as these are typically filesystem dependent.

         isValid "" == False
         isValid "\0" == False
Posix:   isValid "/random_ path:*" == True
Posix:   isValid x == not (null x)
Windows: isValid "c:\\test" == True
Windows: isValid "c:\\test:of_test" == False
Windows: isValid "test*" == False
Windows: isValid "c:\\test\\nul" == False
Windows: isValid "c:\\test\\prn.txt" == False
Windows: isValid "c:\\nul\\file" == False
Windows: isValid "\\\\" == False
Windows: isValid "\\\\\\foo" == False
Windows: isValid "\\\\?\\D:file" == False
Windows: isValid "foo\tbar" == False
Windows: isValid "nul .txt" == False
Windows: isValid " nul.txt" == True

makeValid :: FilePath -> FilePath #

Take a FilePath and make it valid; does not change already valid FilePaths.

isValid (makeValid x)
isValid x ==> makeValid x == x
makeValid "" == "_"
makeValid "file\0name" == "file_name"
Windows: makeValid "c:\\already\\/valid" == "c:\\already\\/valid"
Windows: makeValid "c:\\test:of_test" == "c:\\test_of_test"
Windows: makeValid "test*" == "test_"
Windows: makeValid "c:\\test\\nul" == "c:\\test\\nul_"
Windows: makeValid "c:\\test\\prn.txt" == "c:\\test\\prn_.txt"
Windows: makeValid "c:\\test/prn.txt" == "c:\\test/prn_.txt"
Windows: makeValid "c:\\nul\\file" == "c:\\nul_\\file"
Windows: makeValid "\\\\\\foo" == "\\\\drive"
Windows: makeValid "\\\\?\\D:file" == "\\\\?\\D:\\file"
Windows: makeValid "nul .txt" == "nul _.txt"