Fix documentation markup in </>
[packages/filepath.git] / System / FilePath / Internal.hs
1 #if __GLASGOW_HASKELL__ >= 704
2 {-# LANGUAGE Safe #-}
3 #endif
4 {-# LANGUAGE PatternGuards #-}
5
6 -- This template expects CPP definitions for:
7 -- MODULE_NAME = Posix | Windows
8 -- IS_WINDOWS = False | True
9
10 -- |
11 -- Module : System.FilePath.MODULE_NAME
12 -- Copyright : (c) Neil Mitchell 2005-2014
13 -- License : BSD3
14 --
15 -- Maintainer : ndmitchell@gmail.com
16 -- Stability : stable
17 -- Portability : portable
18 --
19 -- A library for 'FilePath' manipulations, using MODULE_NAME style paths on
20 -- all platforms. Importing "System.FilePath" is usually better.
21 --
22 -- Given the example 'FilePath': @\/directory\/file.ext@
23 --
24 -- We can use the following functions to extract pieces.
25 --
26 -- * 'takeFileName' gives @\"file.ext\"@
27 --
28 -- * 'takeDirectory' gives @\"\/directory\"@
29 --
30 -- * 'takeExtension' gives @\".ext\"@
31 --
32 -- * 'dropExtension' gives @\"\/directory\/file\"@
33 --
34 -- * 'takeBaseName' gives @\"file\"@
35 --
36 -- And we could have built an equivalent path with the following expressions:
37 --
38 -- * @\"\/directory\" '</>' \"file.ext\"@.
39 --
40 -- * @\"\/directory\/file" '<.>' \"ext\"@.
41 --
42 -- * @\"\/directory\/file.txt" '-<.>' \"ext\"@.
43 --
44 -- Each function in this module is documented with several examples,
45 -- which are also used as tests.
46 --
47 -- Here are a few examples of using the @filepath@ functions together:
48 --
49 -- /Example 1:/ Find the possible locations of a Haskell module @Test@ imported from module @Main@:
50 --
51 -- @['replaceFileName' path_to_main \"Test\" '<.>' ext | ext <- [\"hs\",\"lhs\"] ]@
52 --
53 -- /Example 2:/ Download a file from @url@ and save it to disk:
54 --
55 -- @do let file = 'makeValid' url
56 -- System.IO.createDirectoryIfMissing True ('takeDirectory' file)@
57 --
58 -- /Example 3:/ Compile a Haskell file, putting the @.hi@ file under @interface@:
59 --
60 -- @'takeDirectory' file '</>' \"interface\" '</>' ('takeFileName' file '-<.>' \"hi\")@
61 --
62 -- References:
63 -- [1] <http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx Naming Files, Paths and Namespaces> (Microsoft MSDN)
64 module System.FilePath.MODULE_NAME
65 (
66 -- * Separator predicates
67 FilePath,
68 pathSeparator, pathSeparators, isPathSeparator,
69 searchPathSeparator, isSearchPathSeparator,
70 extSeparator, isExtSeparator,
71
72 -- * @$PATH@ methods
73 splitSearchPath, getSearchPath,
74
75 -- * Extension functions
76 splitExtension,
77 takeExtension, replaceExtension, (-<.>), dropExtension, addExtension, hasExtension, (<.>),
78 splitExtensions, dropExtensions, takeExtensions, replaceExtensions,
79 stripExtension,
80
81 -- * Filename\/directory functions
82 splitFileName,
83 takeFileName, replaceFileName, dropFileName,
84 takeBaseName, replaceBaseName,
85 takeDirectory, replaceDirectory,
86 combine, (</>),
87 splitPath, joinPath, splitDirectories,
88
89 -- * Drive functions
90 splitDrive, joinDrive,
91 takeDrive, hasDrive, dropDrive, isDrive,
92
93 -- * Trailing slash functions
94 hasTrailingPathSeparator,
95 addTrailingPathSeparator,
96 dropTrailingPathSeparator,
97
98 -- * File name manipulations
99 normalise, equalFilePath,
100 makeRelative,
101 isRelative, isAbsolute,
102 isValid, makeValid
103 )
104 where
105
106 import Data.Char(toLower, toUpper, isAsciiLower, isAsciiUpper)
107 import Data.Maybe(isJust)
108 import Data.List(stripPrefix)
109
110 import System.Environment(getEnv)
111
112
113 infixr 7 <.>, -<.>
114 infixr 5 </>
115
116
117
118
119
120 ---------------------------------------------------------------------
121 -- Platform Abstraction Methods (private)
122
123 -- | Is the operating system Unix or Linux like
124 isPosix :: Bool
125 isPosix = not isWindows
126
127 -- | Is the operating system Windows like
128 isWindows :: Bool
129 isWindows = IS_WINDOWS
130
131
132 ---------------------------------------------------------------------
133 -- The basic functions
134
135 -- | The character that separates directories. In the case where more than
136 -- one character is possible, 'pathSeparator' is the \'ideal\' one.
137 --
138 -- > Windows: pathSeparator == '\\'
139 -- > Posix: pathSeparator == '/'
140 -- > isPathSeparator pathSeparator
141 pathSeparator :: Char
142 pathSeparator = if isWindows then '\\' else '/'
143
144 -- | The list of all possible separators.
145 --
146 -- > Windows: pathSeparators == ['\\', '/']
147 -- > Posix: pathSeparators == ['/']
148 -- > pathSeparator `elem` pathSeparators
149 pathSeparators :: [Char]
150 pathSeparators = if isWindows then "\\/" else "/"
151
152 -- | Rather than using @(== 'pathSeparator')@, use this. Test if something
153 -- is a path separator.
154 --
155 -- > isPathSeparator a == (a `elem` pathSeparators)
156 isPathSeparator :: Char -> Bool
157 isPathSeparator '/' = True
158 isPathSeparator '\\' = isWindows
159 isPathSeparator _ = False
160
161
162 -- | The character that is used to separate the entries in the $PATH environment variable.
163 --
164 -- > Windows: searchPathSeparator == ';'
165 -- > Posix: searchPathSeparator == ':'
166 searchPathSeparator :: Char
167 searchPathSeparator = if isWindows then ';' else ':'
168
169 -- | Is the character a file separator?
170 --
171 -- > isSearchPathSeparator a == (a == searchPathSeparator)
172 isSearchPathSeparator :: Char -> Bool
173 isSearchPathSeparator = (== searchPathSeparator)
174
175
176 -- | File extension character
177 --
178 -- > extSeparator == '.'
179 extSeparator :: Char
180 extSeparator = '.'
181
182 -- | Is the character an extension character?
183 --
184 -- > isExtSeparator a == (a == extSeparator)
185 isExtSeparator :: Char -> Bool
186 isExtSeparator = (== extSeparator)
187
188
189 ---------------------------------------------------------------------
190 -- Path methods (environment $PATH)
191
192 -- | Take a string, split it on the 'searchPathSeparator' character.
193 -- Blank items are ignored on Windows, and converted to @.@ on Posix.
194 -- On Windows path elements are stripped of quotes.
195 --
196 -- Follows the recommendations in
197 -- <http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html>
198 --
199 -- > Posix: splitSearchPath "File1:File2:File3" == ["File1","File2","File3"]
200 -- > Posix: splitSearchPath "File1::File2:File3" == ["File1",".","File2","File3"]
201 -- > Windows: splitSearchPath "File1;File2;File3" == ["File1","File2","File3"]
202 -- > Windows: splitSearchPath "File1;;File2;File3" == ["File1","File2","File3"]
203 -- > Windows: splitSearchPath "File1;\"File2\";File3" == ["File1","File2","File3"]
204 splitSearchPath :: String -> [FilePath]
205 splitSearchPath = f
206 where
207 f xs = case break isSearchPathSeparator xs of
208 (pre, [] ) -> g pre
209 (pre, _:post) -> g pre ++ f post
210
211 g "" = ["." | isPosix]
212 g ('\"':x@(_:_)) | isWindows && last x == '\"' = [init x]
213 g x = [x]
214
215
216 -- | Get a list of 'FilePath's in the $PATH variable.
217 getSearchPath :: IO [FilePath]
218 getSearchPath = fmap splitSearchPath (getEnv "PATH")
219
220
221 ---------------------------------------------------------------------
222 -- Extension methods
223
224 -- | Split on the extension. 'addExtension' is the inverse.
225 --
226 -- > splitExtension "/directory/path.ext" == ("/directory/path",".ext")
227 -- > uncurry (++) (splitExtension x) == x
228 -- > Valid x => uncurry addExtension (splitExtension x) == x
229 -- > splitExtension "file.txt" == ("file",".txt")
230 -- > splitExtension "file" == ("file","")
231 -- > splitExtension "file/file.txt" == ("file/file",".txt")
232 -- > splitExtension "file.txt/boris" == ("file.txt/boris","")
233 -- > splitExtension "file.txt/boris.ext" == ("file.txt/boris",".ext")
234 -- > splitExtension "file/path.txt.bob.fred" == ("file/path.txt.bob",".fred")
235 -- > splitExtension "file/path.txt/" == ("file/path.txt/","")
236 splitExtension :: FilePath -> (String, String)
237 splitExtension x = case nameDot of
238 "" -> (x,"")
239 _ -> (dir ++ init nameDot, extSeparator : ext)
240 where
241 (dir,file) = splitFileName_ x
242 (nameDot,ext) = breakEnd isExtSeparator file
243
244 -- | Get the extension of a file, returns @\"\"@ for no extension, @.ext@ otherwise.
245 --
246 -- > takeExtension "/directory/path.ext" == ".ext"
247 -- > takeExtension x == snd (splitExtension x)
248 -- > Valid x => takeExtension (addExtension x "ext") == ".ext"
249 -- > Valid x => takeExtension (replaceExtension x "ext") == ".ext"
250 takeExtension :: FilePath -> String
251 takeExtension = snd . splitExtension
252
253 -- | Remove the current extension and add another, equivalent to 'replaceExtension'.
254 --
255 -- > "/directory/path.txt" -<.> "ext" == "/directory/path.ext"
256 -- > "/directory/path.txt" -<.> ".ext" == "/directory/path.ext"
257 -- > "foo.o" -<.> "c" == "foo.c"
258 (-<.>) :: FilePath -> String -> FilePath
259 (-<.>) = replaceExtension
260
261 -- | Set the extension of a file, overwriting one if already present, equivalent to '-<.>'.
262 --
263 -- > replaceExtension "/directory/path.txt" "ext" == "/directory/path.ext"
264 -- > replaceExtension "/directory/path.txt" ".ext" == "/directory/path.ext"
265 -- > replaceExtension "file.txt" ".bob" == "file.bob"
266 -- > replaceExtension "file.txt" "bob" == "file.bob"
267 -- > replaceExtension "file" ".bob" == "file.bob"
268 -- > replaceExtension "file.txt" "" == "file"
269 -- > replaceExtension "file.fred.bob" "txt" == "file.fred.txt"
270 -- > replaceExtension x y == addExtension (dropExtension x) y
271 replaceExtension :: FilePath -> String -> FilePath
272 replaceExtension x y = dropExtension x <.> y
273
274 -- | Add an extension, even if there is already one there, equivalent to 'addExtension'.
275 --
276 -- > "/directory/path" <.> "ext" == "/directory/path.ext"
277 -- > "/directory/path" <.> ".ext" == "/directory/path.ext"
278 (<.>) :: FilePath -> String -> FilePath
279 (<.>) = addExtension
280
281 -- | Remove last extension, and the \".\" preceding it.
282 --
283 -- > dropExtension "/directory/path.ext" == "/directory/path"
284 -- > dropExtension x == fst (splitExtension x)
285 dropExtension :: FilePath -> FilePath
286 dropExtension = fst . splitExtension
287
288 -- | Add an extension, even if there is already one there, equivalent to '<.>'.
289 --
290 -- > addExtension "/directory/path" "ext" == "/directory/path.ext"
291 -- > addExtension "file.txt" "bib" == "file.txt.bib"
292 -- > addExtension "file." ".bib" == "file..bib"
293 -- > addExtension "file" ".bib" == "file.bib"
294 -- > addExtension "/" "x" == "/.x"
295 -- > Valid x => takeFileName (addExtension (addTrailingPathSeparator x) "ext") == ".ext"
296 -- > Windows: addExtension "\\\\share" ".txt" == "\\\\share\\.txt"
297 addExtension :: FilePath -> String -> FilePath
298 addExtension file "" = file
299 addExtension file xs@(x:_) = joinDrive a res
300 where
301 res = if isExtSeparator x then b ++ xs
302 else b ++ [extSeparator] ++ xs
303
304 (a,b) = splitDrive file
305
306 -- | Does the given filename have an extension?
307 --
308 -- > hasExtension "/directory/path.ext" == True
309 -- > hasExtension "/directory/path" == False
310 -- > null (takeExtension x) == not (hasExtension x)
311 hasExtension :: FilePath -> Bool
312 hasExtension = any isExtSeparator . takeFileName
313
314
315 -- | Drop the given extension from a FilePath, and the \".\" preceding it.
316 --
317 -- It returns Nothing if the FilePath does not have the extension given, or
318 -- Just the part before the extension, if it does.
319 --
320 -- It is safer to use this function than System.FilePath.dropExtensions,
321 -- because FilePath might be something like 'file.name.ext1.ext2', where we
322 -- want to only drop the 'ext1.ext2' part, but keep the full 'file.name' part.
323 --
324 -- > stripExtension "hs.o" "foo.x.hs.o" == Just "foo.x"
325 -- > stripExtension "hi.o" "foo.x.hs.o" == Nothing
326 -- > dropExtension x == fromJust (stripExtension (takeExtension x) x)
327 -- > dropExtensions x == fromJust (stripExtension (takeExtensions x) x)
328 -- > stripExtension ".c.d" "a.b.c.d" == Just "a.b"
329 -- > stripExtension ".c.d" "a.b..c.d" == Just "a.b."
330 -- > stripExtension "baz" "foo.bar" == Nothing
331 -- > stripExtension "bar" "foobar" == Nothing
332 -- > stripExtension "" x == Just x
333 stripExtension :: String -> FilePath -> Maybe FilePath
334 stripExtension [] path = Just path
335 stripExtension ext@(x:_) path = stripSuffix dotExt path
336 where dotExt = if isExtSeparator x then ext else '.':ext
337
338
339 -- | Split on all extensions.
340 --
341 -- > splitExtensions "/directory/path.ext" == ("/directory/path",".ext")
342 -- > splitExtensions "file.tar.gz" == ("file",".tar.gz")
343 -- > uncurry (++) (splitExtensions x) == x
344 -- > Valid x => uncurry addExtension (splitExtensions x) == x
345 -- > splitExtensions "file.tar.gz" == ("file",".tar.gz")
346 splitExtensions :: FilePath -> (FilePath, String)
347 splitExtensions x = (a ++ c, d)
348 where
349 (a,b) = splitFileName_ x
350 (c,d) = break isExtSeparator b
351
352 -- | Drop all extensions.
353 --
354 -- > dropExtensions "/directory/path.ext" == "/directory/path"
355 -- > dropExtensions "file.tar.gz" == "file"
356 -- > not $ hasExtension $ dropExtensions x
357 -- > not $ any isExtSeparator $ takeFileName $ dropExtensions x
358 dropExtensions :: FilePath -> FilePath
359 dropExtensions = fst . splitExtensions
360
361 -- | Get all extensions.
362 --
363 -- > takeExtensions "/directory/path.ext" == ".ext"
364 -- > takeExtensions "file.tar.gz" == ".tar.gz"
365 takeExtensions :: FilePath -> String
366 takeExtensions = snd . splitExtensions
367
368
369 -- | Replace all extensions of a file with a new extension. Note
370 -- that 'replaceExtension' and 'addExtension' both work for adding
371 -- multiple extensions, so only required when you need to drop
372 -- all extensions first.
373 --
374 -- > replaceExtensions "file.fred.bob" "txt" == "file.txt"
375 -- > replaceExtensions "file.fred.bob" "tar.gz" == "file.tar.gz"
376 replaceExtensions :: FilePath -> String -> FilePath
377 replaceExtensions x y = dropExtensions x <.> y
378
379
380
381 ---------------------------------------------------------------------
382 -- Drive methods
383
384 -- | Is the given character a valid drive letter?
385 -- only a-z and A-Z are letters, not isAlpha which is more unicodey
386 isLetter :: Char -> Bool
387 isLetter x = isAsciiLower x || isAsciiUpper x
388
389
390 -- | Split a path into a drive and a path.
391 -- On Posix, \/ is a Drive.
392 --
393 -- > uncurry (++) (splitDrive x) == x
394 -- > Windows: splitDrive "file" == ("","file")
395 -- > Windows: splitDrive "c:/file" == ("c:/","file")
396 -- > Windows: splitDrive "c:\\file" == ("c:\\","file")
397 -- > Windows: splitDrive "\\\\shared\\test" == ("\\\\shared\\","test")
398 -- > Windows: splitDrive "\\\\shared" == ("\\\\shared","")
399 -- > Windows: splitDrive "\\\\?\\UNC\\shared\\file" == ("\\\\?\\UNC\\shared\\","file")
400 -- > Windows: splitDrive "\\\\?\\UNCshared\\file" == ("\\\\?\\","UNCshared\\file")
401 -- > Windows: splitDrive "\\\\?\\d:\\file" == ("\\\\?\\d:\\","file")
402 -- > Windows: splitDrive "/d" == ("","/d")
403 -- > Posix: splitDrive "/test" == ("/","test")
404 -- > Posix: splitDrive "//test" == ("//","test")
405 -- > Posix: splitDrive "test/file" == ("","test/file")
406 -- > Posix: splitDrive "file" == ("","file")
407 splitDrive :: FilePath -> (FilePath, FilePath)
408 splitDrive x | isPosix = span (== '/') x
409 splitDrive x | Just y <- readDriveLetter x = y
410 splitDrive x | Just y <- readDriveUNC x = y
411 splitDrive x | Just y <- readDriveShare x = y
412 splitDrive x = ("",x)
413
414 addSlash :: FilePath -> FilePath -> (FilePath, FilePath)
415 addSlash a xs = (a++c,d)
416 where (c,d) = span isPathSeparator xs
417
418 -- See [1].
419 -- "\\?\D:\<path>" or "\\?\UNC\<server>\<share>"
420 readDriveUNC :: FilePath -> Maybe (FilePath, FilePath)
421 readDriveUNC (s1:s2:'?':s3:xs) | all isPathSeparator [s1,s2,s3] =
422 case map toUpper xs of
423 ('U':'N':'C':s4:_) | isPathSeparator s4 ->
424 let (a,b) = readDriveShareName (drop 4 xs)
425 in Just (s1:s2:'?':s3:take 4 xs ++ a, b)
426 _ -> case readDriveLetter xs of
427 -- Extended-length path.
428 Just (a,b) -> Just (s1:s2:'?':s3:a,b)
429 Nothing -> Nothing
430 readDriveUNC _ = Nothing
431
432 {- c:\ -}
433 readDriveLetter :: String -> Maybe (FilePath, FilePath)
434 readDriveLetter (x:':':y:xs) | isLetter x && isPathSeparator y = Just $ addSlash [x,':'] (y:xs)
435 readDriveLetter (x:':':xs) | isLetter x = Just ([x,':'], xs)
436 readDriveLetter _ = Nothing
437
438 {- \\sharename\ -}
439 readDriveShare :: String -> Maybe (FilePath, FilePath)
440 readDriveShare (s1:s2:xs) | isPathSeparator s1 && isPathSeparator s2 =
441 Just (s1:s2:a,b)
442 where (a,b) = readDriveShareName xs
443 readDriveShare _ = Nothing
444
445 {- assume you have already seen \\ -}
446 {- share\bob -> "share\", "bob" -}
447 readDriveShareName :: String -> (FilePath, FilePath)
448 readDriveShareName name = addSlash a b
449 where (a,b) = break isPathSeparator name
450
451
452
453 -- | Join a drive and the rest of the path.
454 --
455 -- > Valid x => uncurry joinDrive (splitDrive x) == x
456 -- > Windows: joinDrive "C:" "foo" == "C:foo"
457 -- > Windows: joinDrive "C:\\" "bar" == "C:\\bar"
458 -- > Windows: joinDrive "\\\\share" "foo" == "\\\\share\\foo"
459 -- > Windows: joinDrive "/:" "foo" == "/:\\foo"
460 joinDrive :: FilePath -> FilePath -> FilePath
461 joinDrive = combineAlways
462
463 -- | Get the drive from a filepath.
464 --
465 -- > takeDrive x == fst (splitDrive x)
466 takeDrive :: FilePath -> FilePath
467 takeDrive = fst . splitDrive
468
469 -- | Delete the drive, if it exists.
470 --
471 -- > dropDrive x == snd (splitDrive x)
472 dropDrive :: FilePath -> FilePath
473 dropDrive = snd . splitDrive
474
475 -- | Does a path have a drive.
476 --
477 -- > not (hasDrive x) == null (takeDrive x)
478 -- > Posix: hasDrive "/foo" == True
479 -- > Windows: hasDrive "C:\\foo" == True
480 -- > Windows: hasDrive "C:foo" == True
481 -- > hasDrive "foo" == False
482 -- > hasDrive "" == False
483 hasDrive :: FilePath -> Bool
484 hasDrive = not . null . takeDrive
485
486
487 -- | Is an element a drive
488 --
489 -- > Posix: isDrive "/" == True
490 -- > Posix: isDrive "/foo" == False
491 -- > Windows: isDrive "C:\\" == True
492 -- > Windows: isDrive "C:\\foo" == False
493 -- > isDrive "" == False
494 isDrive :: FilePath -> Bool
495 isDrive x = not (null x) && null (dropDrive x)
496
497
498 ---------------------------------------------------------------------
499 -- Operations on a filepath, as a list of directories
500
501 -- | Split a filename into directory and file. '</>' is the inverse.
502 -- The first component will often end with a trailing slash.
503 --
504 -- > splitFileName "/directory/file.ext" == ("/directory/","file.ext")
505 -- > Valid x => uncurry (</>) (splitFileName x) == x || fst (splitFileName x) == "./"
506 -- > Valid x => isValid (fst (splitFileName x))
507 -- > splitFileName "file/bob.txt" == ("file/", "bob.txt")
508 -- > splitFileName "file/" == ("file/", "")
509 -- > splitFileName "bob" == ("./", "bob")
510 -- > Posix: splitFileName "/" == ("/","")
511 -- > Windows: splitFileName "c:" == ("c:","")
512 splitFileName :: FilePath -> (String, String)
513 splitFileName x = (if null dir then "./" else dir, name)
514 where
515 (dir, name) = splitFileName_ x
516
517 -- version of splitFileName where, if the FilePath has no directory
518 -- component, the returned directory is "" rather than "./". This
519 -- is used in cases where we are going to combine the returned
520 -- directory to make a valid FilePath, and having a "./" appear would
521 -- look strange and upset simple equality properties. See
522 -- e.g. replaceFileName.
523 splitFileName_ :: FilePath -> (String, String)
524 splitFileName_ x = (drv ++ dir, file)
525 where
526 (drv,pth) = splitDrive x
527 (dir,file) = breakEnd isPathSeparator pth
528
529 -- | Set the filename.
530 --
531 -- > replaceFileName "/directory/other.txt" "file.ext" == "/directory/file.ext"
532 -- > Valid x => replaceFileName x (takeFileName x) == x
533 replaceFileName :: FilePath -> String -> FilePath
534 replaceFileName x y = a </> y where (a,_) = splitFileName_ x
535
536 -- | Drop the filename. Unlike 'takeDirectory', this function will leave
537 -- a trailing path separator on the directory.
538 --
539 -- > dropFileName "/directory/file.ext" == "/directory/"
540 -- > dropFileName x == fst (splitFileName x)
541 dropFileName :: FilePath -> FilePath
542 dropFileName = fst . splitFileName
543
544
545 -- | Get the file name.
546 --
547 -- > takeFileName "/directory/file.ext" == "file.ext"
548 -- > takeFileName "test/" == ""
549 -- > takeFileName x `isSuffixOf` x
550 -- > takeFileName x == snd (splitFileName x)
551 -- > Valid x => takeFileName (replaceFileName x "fred") == "fred"
552 -- > Valid x => takeFileName (x </> "fred") == "fred"
553 -- > Valid x => isRelative (takeFileName x)
554 takeFileName :: FilePath -> FilePath
555 takeFileName = snd . splitFileName
556
557 -- | Get the base name, without an extension or path.
558 --
559 -- > takeBaseName "/directory/file.ext" == "file"
560 -- > takeBaseName "file/test.txt" == "test"
561 -- > takeBaseName "dave.ext" == "dave"
562 -- > takeBaseName "" == ""
563 -- > takeBaseName "test" == "test"
564 -- > takeBaseName (addTrailingPathSeparator x) == ""
565 -- > takeBaseName "file/file.tar.gz" == "file.tar"
566 takeBaseName :: FilePath -> String
567 takeBaseName = dropExtension . takeFileName
568
569 -- | Set the base name.
570 --
571 -- > replaceBaseName "/directory/other.ext" "file" == "/directory/file.ext"
572 -- > replaceBaseName "file/test.txt" "bob" == "file/bob.txt"
573 -- > replaceBaseName "fred" "bill" == "bill"
574 -- > replaceBaseName "/dave/fred/bob.gz.tar" "new" == "/dave/fred/new.tar"
575 -- > Valid x => replaceBaseName x (takeBaseName x) == x
576 replaceBaseName :: FilePath -> String -> FilePath
577 replaceBaseName pth nam = combineAlways a (nam <.> ext)
578 where
579 (a,b) = splitFileName_ pth
580 ext = takeExtension b
581
582 -- | Is an item either a directory or the last character a path separator?
583 --
584 -- > hasTrailingPathSeparator "test" == False
585 -- > hasTrailingPathSeparator "test/" == True
586 hasTrailingPathSeparator :: FilePath -> Bool
587 hasTrailingPathSeparator "" = False
588 hasTrailingPathSeparator x = isPathSeparator (last x)
589
590
591 hasLeadingPathSeparator :: FilePath -> Bool
592 hasLeadingPathSeparator "" = False
593 hasLeadingPathSeparator x = isPathSeparator (head x)
594
595
596 -- | Add a trailing file path separator if one is not already present.
597 --
598 -- > hasTrailingPathSeparator (addTrailingPathSeparator x)
599 -- > hasTrailingPathSeparator x ==> addTrailingPathSeparator x == x
600 -- > Posix: addTrailingPathSeparator "test/rest" == "test/rest/"
601 addTrailingPathSeparator :: FilePath -> FilePath
602 addTrailingPathSeparator x = if hasTrailingPathSeparator x then x else x ++ [pathSeparator]
603
604
605 -- | Remove any trailing path separators
606 --
607 -- > dropTrailingPathSeparator "file/test/" == "file/test"
608 -- > dropTrailingPathSeparator "/" == "/"
609 -- > Windows: dropTrailingPathSeparator "\\" == "\\"
610 -- > Posix: not (hasTrailingPathSeparator (dropTrailingPathSeparator x)) || isDrive x
611 dropTrailingPathSeparator :: FilePath -> FilePath
612 dropTrailingPathSeparator x =
613 if hasTrailingPathSeparator x && not (isDrive x)
614 then let x' = dropWhileEnd isPathSeparator x
615 in if null x' then [last x] else x'
616 else x
617
618
619 -- | Get the directory name, move up one level.
620 --
621 -- > takeDirectory "/directory/other.ext" == "/directory"
622 -- > takeDirectory x `isPrefixOf` x || takeDirectory x == "."
623 -- > takeDirectory "foo" == "."
624 -- > takeDirectory "/" == "/"
625 -- > takeDirectory "/foo" == "/"
626 -- > takeDirectory "/foo/bar/baz" == "/foo/bar"
627 -- > takeDirectory "/foo/bar/baz/" == "/foo/bar/baz"
628 -- > takeDirectory "foo/bar/baz" == "foo/bar"
629 -- > Windows: takeDirectory "foo\\bar" == "foo"
630 -- > Windows: takeDirectory "foo\\bar\\\\" == "foo\\bar"
631 -- > Windows: takeDirectory "C:\\" == "C:\\"
632 takeDirectory :: FilePath -> FilePath
633 takeDirectory = dropTrailingPathSeparator . dropFileName
634
635 -- | Set the directory, keeping the filename the same.
636 --
637 -- > replaceDirectory "root/file.ext" "/directory/" == "/directory/file.ext"
638 -- > Valid x => replaceDirectory x (takeDirectory x) `equalFilePath` x
639 replaceDirectory :: FilePath -> String -> FilePath
640 replaceDirectory x dir = combineAlways dir (takeFileName x)
641
642
643 -- | An alias for '</>'.
644 combine :: FilePath -> FilePath -> FilePath
645 combine a b | hasLeadingPathSeparator b || hasDrive b = b
646 | otherwise = combineAlways a b
647
648 -- | Combine two paths, assuming rhs is NOT absolute.
649 combineAlways :: FilePath -> FilePath -> FilePath
650 combineAlways a b | null a = b
651 | null b = a
652 | hasTrailingPathSeparator a = a ++ b
653 | otherwise = case a of
654 [a1,':'] | isWindows && isLetter a1 -> a ++ b
655 _ -> a ++ [pathSeparator] ++ b
656
657
658 -- | Combine two paths with a path separator.
659 -- If the second path starts with a path separator or a drive letter, then it returns the second.
660 -- The intention is that @readFile (dir '</>' file)@ will access the same file as
661 -- @setCurrentDirectory dir; readFile file@.
662 --
663 -- > Posix: "/directory" </> "file.ext" == "/directory/file.ext"
664 -- > Windows: "/directory" </> "file.ext" == "/directory\\file.ext"
665 -- > "directory" </> "/file.ext" == "/file.ext"
666 -- > Valid x => (takeDirectory x </> takeFileName x) `equalFilePath` x
667 --
668 -- Combined:
669 --
670 -- > Posix: "/" </> "test" == "/test"
671 -- > Posix: "home" </> "bob" == "home/bob"
672 -- > Posix: "x:" </> "foo" == "x:/foo"
673 -- > Windows: "C:\\foo" </> "bar" == "C:\\foo\\bar"
674 -- > Windows: "home" </> "bob" == "home\\bob"
675 --
676 -- Not combined:
677 --
678 -- > Posix: "home" </> "/bob" == "/bob"
679 -- > Windows: "home" </> "C:\\bob" == "C:\\bob"
680 --
681 -- Not combined (tricky):
682 --
683 -- On Windows, if a filepath starts with a single slash, it is relative to the
684 -- root of the current drive. In [1], this is (confusingly) referred to as an
685 -- absolute path.
686 -- The current behavior of '</>' is to never combine these forms.
687 --
688 -- > Windows: "home" </> "/bob" == "/bob"
689 -- > Windows: "home" </> "\\bob" == "\\bob"
690 -- > Windows: "C:\\home" </> "\\bob" == "\\bob"
691 --
692 -- On Windows, from [1]: "If a file name begins with only a disk designator
693 -- but not the backslash after the colon, it is interpreted as a relative path
694 -- to the current directory on the drive with the specified letter."
695 -- The current behavior of '</>' is to never combine these forms.
696 --
697 -- > Windows: "D:\\foo" </> "C:bar" == "C:bar"
698 -- > Windows: "C:\\foo" </> "C:bar" == "C:bar"
699 (</>) :: FilePath -> FilePath -> FilePath
700 (</>) = combine
701
702
703 -- | Split a path by the directory separator.
704 --
705 -- > splitPath "/directory/file.ext" == ["/","directory/","file.ext"]
706 -- > concat (splitPath x) == x
707 -- > splitPath "test//item/" == ["test//","item/"]
708 -- > splitPath "test/item/file" == ["test/","item/","file"]
709 -- > splitPath "" == []
710 -- > Windows: splitPath "c:\\test\\path" == ["c:\\","test\\","path"]
711 -- > Posix: splitPath "/file/test" == ["/","file/","test"]
712 splitPath :: FilePath -> [FilePath]
713 splitPath x = [drive | drive /= ""] ++ f path
714 where
715 (drive,path) = splitDrive x
716
717 f "" = []
718 f y = (a++c) : f d
719 where
720 (a,b) = break isPathSeparator y
721 (c,d) = span isPathSeparator b
722
723 -- | Just as 'splitPath', but don't add the trailing slashes to each element.
724 --
725 -- > splitDirectories "/directory/file.ext" == ["/","directory","file.ext"]
726 -- > splitDirectories "test/file" == ["test","file"]
727 -- > splitDirectories "/test/file" == ["/","test","file"]
728 -- > Windows: splitDirectories "C:\\test\\file" == ["C:\\", "test", "file"]
729 -- > Valid x => joinPath (splitDirectories x) `equalFilePath` x
730 -- > splitDirectories "" == []
731 -- > Windows: splitDirectories "C:\\test\\\\\\file" == ["C:\\", "test", "file"]
732 -- > splitDirectories "/test///file" == ["/","test","file"]
733 splitDirectories :: FilePath -> [FilePath]
734 splitDirectories = map dropTrailingPathSeparator . splitPath
735
736
737 -- | Join path elements back together.
738 --
739 -- > joinPath ["/","directory/","file.ext"] == "/directory/file.ext"
740 -- > Valid x => joinPath (splitPath x) == x
741 -- > joinPath [] == ""
742 -- > Posix: joinPath ["test","file","path"] == "test/file/path"
743 joinPath :: [FilePath] -> FilePath
744 -- Note that this definition on c:\\c:\\, join then split will give c:\\.
745 joinPath = foldr combine ""
746
747
748
749
750
751
752 ---------------------------------------------------------------------
753 -- File name manipulators
754
755 -- | Equality of two 'FilePath's.
756 -- If you call @System.Directory.canonicalizePath@
757 -- first this has a much better chance of working.
758 -- Note that this doesn't follow symlinks or DOSNAM~1s.
759 --
760 -- > x == y ==> equalFilePath x y
761 -- > normalise x == normalise y ==> equalFilePath x y
762 -- > equalFilePath "foo" "foo/"
763 -- > not (equalFilePath "foo" "/foo")
764 -- > Posix: not (equalFilePath "foo" "FOO")
765 -- > Windows: equalFilePath "foo" "FOO"
766 -- > Windows: not (equalFilePath "C:" "C:/")
767 equalFilePath :: FilePath -> FilePath -> Bool
768 equalFilePath a b = f a == f b
769 where
770 f x | isWindows = dropTrailingPathSeparator $ map toLower $ normalise x
771 | otherwise = dropTrailingPathSeparator $ normalise x
772
773
774 -- | Contract a filename, based on a relative path. Note that the resulting path
775 -- will never introduce @..@ paths, as the presence of symlinks means @..\/b@
776 -- may not reach @a\/b@ if it starts from @a\/c@. For a worked example see
777 -- <http://neilmitchell.blogspot.co.uk/2015/10/filepaths-are-subtle-symlinks-are-hard.html this blog post>.
778 --
779 -- The corresponding @makeAbsolute@ function can be found in
780 -- @System.Directory@.
781 --
782 -- > makeRelative "/directory" "/directory/file.ext" == "file.ext"
783 -- > Valid x => makeRelative (takeDirectory x) x `equalFilePath` takeFileName x
784 -- > makeRelative x x == "."
785 -- > Valid x y => equalFilePath x y || (isRelative x && makeRelative y x == x) || equalFilePath (y </> makeRelative y x) x
786 -- > Windows: makeRelative "C:\\Home" "c:\\home\\bob" == "bob"
787 -- > Windows: makeRelative "C:\\Home" "c:/home/bob" == "bob"
788 -- > Windows: makeRelative "C:\\Home" "D:\\Home\\Bob" == "D:\\Home\\Bob"
789 -- > Windows: makeRelative "C:\\Home" "C:Home\\Bob" == "C:Home\\Bob"
790 -- > Windows: makeRelative "/Home" "/home/bob" == "bob"
791 -- > Windows: makeRelative "/" "//" == "//"
792 -- > Posix: makeRelative "/Home" "/home/bob" == "/home/bob"
793 -- > Posix: makeRelative "/home/" "/home/bob/foo/bar" == "bob/foo/bar"
794 -- > Posix: makeRelative "/fred" "bob" == "bob"
795 -- > Posix: makeRelative "/file/test" "/file/test/fred" == "fred"
796 -- > Posix: makeRelative "/file/test" "/file/test/fred/" == "fred/"
797 -- > Posix: makeRelative "some/path" "some/path/a/b/c" == "a/b/c"
798 makeRelative :: FilePath -> FilePath -> FilePath
799 makeRelative root path
800 | equalFilePath root path = "."
801 | takeAbs root /= takeAbs path = path
802 | otherwise = f (dropAbs root) (dropAbs path)
803 where
804 f "" y = dropWhile isPathSeparator y
805 f x y = let (x1,x2) = g x
806 (y1,y2) = g y
807 in if equalFilePath x1 y1 then f x2 y2 else path
808
809 g x = (dropWhile isPathSeparator a, dropWhile isPathSeparator b)
810 where (a,b) = break isPathSeparator $ dropWhile isPathSeparator x
811
812 -- on windows, need to drop '/' which is kind of absolute, but not a drive
813 dropAbs x | hasLeadingPathSeparator x && not (hasDrive x) = tail x
814 dropAbs x = dropDrive x
815
816 takeAbs x | hasLeadingPathSeparator x && not (hasDrive x) = [pathSeparator]
817 takeAbs x = map (\y -> if isPathSeparator y then pathSeparator else toLower y) $ takeDrive x
818
819 -- | Normalise a file
820 --
821 -- * \/\/ outside of the drive can be made blank
822 --
823 -- * \/ -> 'pathSeparator'
824 --
825 -- * .\/ -> \"\"
826 --
827 -- > Posix: normalise "/file/\\test////" == "/file/\\test/"
828 -- > Posix: normalise "/file/./test" == "/file/test"
829 -- > Posix: normalise "/test/file/../bob/fred/" == "/test/file/../bob/fred/"
830 -- > Posix: normalise "../bob/fred/" == "../bob/fred/"
831 -- > Posix: normalise "./bob/fred/" == "bob/fred/"
832 -- > Windows: normalise "c:\\file/bob\\" == "C:\\file\\bob\\"
833 -- > Windows: normalise "c:\\" == "C:\\"
834 -- > Windows: normalise "C:.\\" == "C:"
835 -- > Windows: normalise "\\\\server\\test" == "\\\\server\\test"
836 -- > Windows: normalise "//server/test" == "\\\\server\\test"
837 -- > Windows: normalise "c:/file" == "C:\\file"
838 -- > Windows: normalise "/file" == "\\file"
839 -- > Windows: normalise "\\" == "\\"
840 -- > Windows: normalise "/./" == "\\"
841 -- > normalise "." == "."
842 -- > Posix: normalise "./" == "./"
843 -- > Posix: normalise "./." == "./"
844 -- > Posix: normalise "/./" == "/"
845 -- > Posix: normalise "/" == "/"
846 -- > Posix: normalise "bob/fred/." == "bob/fred/"
847 -- > Posix: normalise "//home" == "/home"
848 normalise :: FilePath -> FilePath
849 normalise path = result ++ [pathSeparator | addPathSeparator]
850 where
851 (drv,pth) = splitDrive path
852 result = joinDrive' (normaliseDrive drv) (f pth)
853
854 joinDrive' "" "" = "."
855 joinDrive' d p = joinDrive d p
856
857 addPathSeparator = isDirPath pth
858 && not (hasTrailingPathSeparator result)
859 && not (isRelativeDrive drv)
860
861 isDirPath xs = hasTrailingPathSeparator xs
862 || not (null xs) && last xs == '.' && hasTrailingPathSeparator (init xs)
863
864 f = joinPath . dropDots . propSep . splitDirectories
865
866 propSep (x:xs) | all isPathSeparator x = [pathSeparator] : xs
867 | otherwise = x : xs
868 propSep [] = []
869
870 dropDots = filter ("." /=)
871
872 normaliseDrive :: FilePath -> FilePath
873 normaliseDrive "" = ""
874 normaliseDrive _ | isPosix = [pathSeparator]
875 normaliseDrive drive = if isJust $ readDriveLetter x2
876 then map toUpper x2
877 else x2
878 where
879 x2 = map repSlash drive
880
881 repSlash x = if isPathSeparator x then pathSeparator else x
882
883 -- Information for validity functions on Windows. See [1].
884 isBadCharacter :: Char -> Bool
885 isBadCharacter x = x >= '\0' && x <= '\31' || x `elem` ":*?><|\""
886
887 badElements :: [FilePath]
888 badElements =
889 ["CON","PRN","AUX","NUL","CLOCK$"
890 ,"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9"
891 ,"LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"]
892
893
894 -- | Is a FilePath valid, i.e. could you create a file like it? This function checks for invalid names,
895 -- and invalid characters, but does not check if length limits are exceeded, as these are typically
896 -- filesystem dependent.
897 --
898 -- > isValid "" == False
899 -- > isValid "\0" == False
900 -- > Posix: isValid "/random_ path:*" == True
901 -- > Posix: isValid x == not (null x)
902 -- > Windows: isValid "c:\\test" == True
903 -- > Windows: isValid "c:\\test:of_test" == False
904 -- > Windows: isValid "test*" == False
905 -- > Windows: isValid "c:\\test\\nul" == False
906 -- > Windows: isValid "c:\\test\\prn.txt" == False
907 -- > Windows: isValid "c:\\nul\\file" == False
908 -- > Windows: isValid "\\\\" == False
909 -- > Windows: isValid "\\\\\\foo" == False
910 -- > Windows: isValid "\\\\?\\D:file" == False
911 -- > Windows: isValid "foo\tbar" == False
912 -- > Windows: isValid "nul .txt" == False
913 -- > Windows: isValid " nul.txt" == True
914 isValid :: FilePath -> Bool
915 isValid "" = False
916 isValid x | '\0' `elem` x = False
917 isValid _ | isPosix = True
918 isValid path =
919 not (any isBadCharacter x2) &&
920 not (any f $ splitDirectories x2) &&
921 not (isJust (readDriveShare x1) && all isPathSeparator x1) &&
922 not (isJust (readDriveUNC x1) && not (hasTrailingPathSeparator x1))
923 where
924 (x1,x2) = splitDrive path
925 f x = map toUpper (dropWhileEnd (== ' ') $ dropExtensions x) `elem` badElements
926
927
928 -- | Take a FilePath and make it valid; does not change already valid FilePaths.
929 --
930 -- > isValid (makeValid x)
931 -- > isValid x ==> makeValid x == x
932 -- > makeValid "" == "_"
933 -- > makeValid "file\0name" == "file_name"
934 -- > Windows: makeValid "c:\\already\\/valid" == "c:\\already\\/valid"
935 -- > Windows: makeValid "c:\\test:of_test" == "c:\\test_of_test"
936 -- > Windows: makeValid "test*" == "test_"
937 -- > Windows: makeValid "c:\\test\\nul" == "c:\\test\\nul_"
938 -- > Windows: makeValid "c:\\test\\prn.txt" == "c:\\test\\prn_.txt"
939 -- > Windows: makeValid "c:\\test/prn.txt" == "c:\\test/prn_.txt"
940 -- > Windows: makeValid "c:\\nul\\file" == "c:\\nul_\\file"
941 -- > Windows: makeValid "\\\\\\foo" == "\\\\drive"
942 -- > Windows: makeValid "\\\\?\\D:file" == "\\\\?\\D:\\file"
943 -- > Windows: makeValid "nul .txt" == "nul _.txt"
944 makeValid :: FilePath -> FilePath
945 makeValid "" = "_"
946 makeValid path
947 | isPosix = map (\x -> if x == '\0' then '_' else x) path
948 | isJust (readDriveShare drv) && all isPathSeparator drv = take 2 drv ++ "drive"
949 | isJust (readDriveUNC drv) && not (hasTrailingPathSeparator drv) =
950 makeValid (drv ++ [pathSeparator] ++ pth)
951 | otherwise = joinDrive drv $ validElements $ validChars pth
952 where
953 (drv,pth) = splitDrive path
954
955 validChars = map f
956 f x = if isBadCharacter x then '_' else x
957
958 validElements x = joinPath $ map g $ splitPath x
959 g x = h a ++ b
960 where (a,b) = break isPathSeparator x
961 h x = if map toUpper (dropWhileEnd (== ' ') a) `elem` badElements then a ++ "_" <.> b else x
962 where (a,b) = splitExtensions x
963
964
965 -- | Is a path relative, or is it fixed to the root?
966 --
967 -- > Windows: isRelative "path\\test" == True
968 -- > Windows: isRelative "c:\\test" == False
969 -- > Windows: isRelative "c:test" == True
970 -- > Windows: isRelative "c:\\" == False
971 -- > Windows: isRelative "c:/" == False
972 -- > Windows: isRelative "c:" == True
973 -- > Windows: isRelative "\\\\foo" == False
974 -- > Windows: isRelative "\\\\?\\foo" == False
975 -- > Windows: isRelative "\\\\?\\UNC\\foo" == False
976 -- > Windows: isRelative "/foo" == True
977 -- > Windows: isRelative "\\foo" == True
978 -- > Posix: isRelative "test/path" == True
979 -- > Posix: isRelative "/test" == False
980 -- > Posix: isRelative "/" == False
981 --
982 -- According to [1]:
983 --
984 -- * "A UNC name of any format [is never relative]."
985 --
986 -- * "You cannot use the "\\?\" prefix with a relative path."
987 isRelative :: FilePath -> Bool
988 isRelative x = null drive || isRelativeDrive drive
989 where drive = takeDrive x
990
991
992 {- c:foo -}
993 -- From [1]: "If a file name begins with only a disk designator but not the
994 -- backslash after the colon, it is interpreted as a relative path to the
995 -- current directory on the drive with the specified letter."
996 isRelativeDrive :: String -> Bool
997 isRelativeDrive x =
998 maybe False (not . hasTrailingPathSeparator . fst) (readDriveLetter x)
999
1000
1001 -- | @not . 'isRelative'@
1002 --
1003 -- > isAbsolute x == not (isRelative x)
1004 isAbsolute :: FilePath -> Bool
1005 isAbsolute = not . isRelative
1006
1007
1008 -----------------------------------------------------------------------------
1009 -- dropWhileEnd (>2) [1,2,3,4,1,2,3,4] == [1,2,3,4,1,2])
1010 -- Note that Data.List.dropWhileEnd is only available in base >= 4.5.
1011 dropWhileEnd :: (a -> Bool) -> [a] -> [a]
1012 dropWhileEnd p = reverse . dropWhile p . reverse
1013
1014 -- takeWhileEnd (>2) [1,2,3,4,1,2,3,4] == [3,4])
1015 takeWhileEnd :: (a -> Bool) -> [a] -> [a]
1016 takeWhileEnd p = reverse . takeWhile p . reverse
1017
1018 -- spanEnd (>2) [1,2,3,4,1,2,3,4] = ([1,2,3,4,1,2], [3,4])
1019 spanEnd :: (a -> Bool) -> [a] -> ([a], [a])
1020 spanEnd p xs = (dropWhileEnd p xs, takeWhileEnd p xs)
1021
1022 -- breakEnd (< 2) [1,2,3,4,1,2,3,4] == ([1,2,3,4,1],[2,3,4])
1023 breakEnd :: (a -> Bool) -> [a] -> ([a], [a])
1024 breakEnd p = spanEnd (not . p)
1025
1026 -- | The stripSuffix function drops the given suffix from a list. It returns
1027 -- Nothing if the list did not end with the suffix given, or Just the list
1028 -- before the suffix, if it does.
1029 stripSuffix :: Eq a => [a] -> [a] -> Maybe [a]
1030 stripSuffix xs ys = fmap reverse $ stripPrefix (reverse xs) (reverse ys)