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