#42, add replaceExtensions
[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
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 -- | Replace all extensions of a file with a new extension. Note
344 -- that 'replaceExtension' and 'addExtension' both work for adding
345 -- multiple extensions, so only required when you need to drop
346 -- all extensions first.
347 --
348 -- > replaceExtensions "file.fred.bob" "txt" == "file.txt"
349 -- > replaceExtensions "file.fred.bob" "tar.gz" == "file.tar.gz"
350 replaceExtensions :: FilePath -> String -> FilePath
351 replaceExtensions x y = dropExtensions x <.> y
352
353
354
355 ---------------------------------------------------------------------
356 -- Drive methods
357
358 -- | Is the given character a valid drive letter?
359 -- only a-z and A-Z are letters, not isAlpha which is more unicodey
360 isLetter :: Char -> Bool
361 isLetter x = isAsciiLower x || isAsciiUpper x
362
363
364 -- | Split a path into a drive and a path.
365 -- On Posix, \/ is a Drive.
366 --
367 -- > uncurry (++) (splitDrive x) == x
368 -- > Windows: splitDrive "file" == ("","file")
369 -- > Windows: splitDrive "c:/file" == ("c:/","file")
370 -- > Windows: splitDrive "c:\\file" == ("c:\\","file")
371 -- > Windows: splitDrive "\\\\shared\\test" == ("\\\\shared\\","test")
372 -- > Windows: splitDrive "\\\\shared" == ("\\\\shared","")
373 -- > Windows: splitDrive "\\\\?\\UNC\\shared\\file" == ("\\\\?\\UNC\\shared\\","file")
374 -- > Windows: splitDrive "\\\\?\\UNCshared\\file" == ("\\\\?\\","UNCshared\\file")
375 -- > Windows: splitDrive "\\\\?\\d:\\file" == ("\\\\?\\d:\\","file")
376 -- > Windows: splitDrive "/d" == ("","/d")
377 -- > Posix: splitDrive "/test" == ("/","test")
378 -- > Posix: splitDrive "//test" == ("//","test")
379 -- > Posix: splitDrive "test/file" == ("","test/file")
380 -- > Posix: splitDrive "file" == ("","file")
381 splitDrive :: FilePath -> (FilePath, FilePath)
382 splitDrive x | isPosix = span (== '/') x
383 splitDrive x | Just y <- readDriveLetter x = y
384 splitDrive x | Just y <- readDriveUNC x = y
385 splitDrive x | Just y <- readDriveShare x = y
386 splitDrive x = ("",x)
387
388 addSlash :: FilePath -> FilePath -> (FilePath, FilePath)
389 addSlash a xs = (a++c,d)
390 where (c,d) = span isPathSeparator xs
391
392 -- See [1].
393 -- "\\?\D:\<path>" or "\\?\UNC\<server>\<share>"
394 readDriveUNC :: FilePath -> Maybe (FilePath, FilePath)
395 readDriveUNC (s1:s2:'?':s3:xs) | all isPathSeparator [s1,s2,s3] =
396 case map toUpper xs of
397 ('U':'N':'C':s4:_) | isPathSeparator s4 ->
398 let (a,b) = readDriveShareName (drop 4 xs)
399 in Just (s1:s2:'?':s3:take 4 xs ++ a, b)
400 _ -> case readDriveLetter xs of
401 -- Extended-length path.
402 Just (a,b) -> Just (s1:s2:'?':s3:a,b)
403 Nothing -> Nothing
404 readDriveUNC _ = Nothing
405
406 {- c:\ -}
407 readDriveLetter :: String -> Maybe (FilePath, FilePath)
408 readDriveLetter (x:':':y:xs) | isLetter x && isPathSeparator y = Just $ addSlash [x,':'] (y:xs)
409 readDriveLetter (x:':':xs) | isLetter x = Just ([x,':'], xs)
410 readDriveLetter _ = Nothing
411
412 {- \\sharename\ -}
413 readDriveShare :: String -> Maybe (FilePath, FilePath)
414 readDriveShare (s1:s2:xs) | isPathSeparator s1 && isPathSeparator s2 =
415 Just (s1:s2:a,b)
416 where (a,b) = readDriveShareName xs
417 readDriveShare _ = Nothing
418
419 {- assume you have already seen \\ -}
420 {- share\bob -> "share\", "bob" -}
421 readDriveShareName :: String -> (FilePath, FilePath)
422 readDriveShareName name = addSlash a b
423 where (a,b) = break isPathSeparator name
424
425
426
427 -- | Join a drive and the rest of the path.
428 --
429 -- > Valid x => uncurry joinDrive (splitDrive x) == x
430 -- > Windows: joinDrive "C:" "foo" == "C:foo"
431 -- > Windows: joinDrive "C:\\" "bar" == "C:\\bar"
432 -- > Windows: joinDrive "\\\\share" "foo" == "\\\\share\\foo"
433 -- > Windows: joinDrive "/:" "foo" == "/:\\foo"
434 joinDrive :: FilePath -> FilePath -> FilePath
435 joinDrive = combineAlways
436
437 -- | Get the drive from a filepath.
438 --
439 -- > takeDrive x == fst (splitDrive x)
440 takeDrive :: FilePath -> FilePath
441 takeDrive = fst . splitDrive
442
443 -- | Delete the drive, if it exists.
444 --
445 -- > dropDrive x == snd (splitDrive x)
446 dropDrive :: FilePath -> FilePath
447 dropDrive = snd . splitDrive
448
449 -- | Does a path have a drive.
450 --
451 -- > not (hasDrive x) == null (takeDrive x)
452 -- > Posix: hasDrive "/foo" == True
453 -- > Windows: hasDrive "C:\\foo" == True
454 -- > Windows: hasDrive "C:foo" == True
455 -- > hasDrive "foo" == False
456 -- > hasDrive "" == False
457 hasDrive :: FilePath -> Bool
458 hasDrive = not . null . takeDrive
459
460
461 -- | Is an element a drive
462 --
463 -- > Posix: isDrive "/" == True
464 -- > Posix: isDrive "/foo" == False
465 -- > Windows: isDrive "C:\\" == True
466 -- > Windows: isDrive "C:\\foo" == False
467 -- > isDrive "" == False
468 isDrive :: FilePath -> Bool
469 isDrive x = not (null x) && null (dropDrive x)
470
471
472 ---------------------------------------------------------------------
473 -- Operations on a filepath, as a list of directories
474
475 -- | Split a filename into directory and file. '</>' is the inverse.
476 -- The first component will often end with a trailing slash.
477 --
478 -- > splitFileName "/directory/file.ext" == ("/directory/","file.ext")
479 -- > Valid x => uncurry (</>) (splitFileName x) == x || fst (splitFileName x) == "./"
480 -- > Valid x => isValid (fst (splitFileName x))
481 -- > splitFileName "file/bob.txt" == ("file/", "bob.txt")
482 -- > splitFileName "file/" == ("file/", "")
483 -- > splitFileName "bob" == ("./", "bob")
484 -- > Posix: splitFileName "/" == ("/","")
485 -- > Windows: splitFileName "c:" == ("c:","")
486 splitFileName :: FilePath -> (String, String)
487 splitFileName x = (if null dir then "./" else dir, name)
488 where
489 (dir, name) = splitFileName_ x
490
491 -- version of splitFileName where, if the FilePath has no directory
492 -- component, the returned directory is "" rather than "./". This
493 -- is used in cases where we are going to combine the returned
494 -- directory to make a valid FilePath, and having a "./" appear would
495 -- look strange and upset simple equality properties. See
496 -- e.g. replaceFileName.
497 splitFileName_ :: FilePath -> (String, String)
498 splitFileName_ x = (drv ++ dir, file)
499 where
500 (drv,pth) = splitDrive x
501 (dir,file) = breakEnd isPathSeparator pth
502
503 -- | Set the filename.
504 --
505 -- > replaceFileName "/directory/other.txt" "file.ext" == "/directory/file.ext"
506 -- > Valid x => replaceFileName x (takeFileName x) == x
507 replaceFileName :: FilePath -> String -> FilePath
508 replaceFileName x y = a </> y where (a,_) = splitFileName_ x
509
510 -- | Drop the filename. Unlike 'takeDirectory', this function will leave
511 -- a trailing path separator on the directory.
512 --
513 -- > dropFileName "/directory/file.ext" == "/directory/"
514 -- > dropFileName x == fst (splitFileName x)
515 dropFileName :: FilePath -> FilePath
516 dropFileName = fst . splitFileName
517
518
519 -- | Get the file name.
520 --
521 -- > takeFileName "/directory/file.ext" == "file.ext"
522 -- > takeFileName "test/" == ""
523 -- > takeFileName x `isSuffixOf` x
524 -- > takeFileName x == snd (splitFileName x)
525 -- > Valid x => takeFileName (replaceFileName x "fred") == "fred"
526 -- > Valid x => takeFileName (x </> "fred") == "fred"
527 -- > Valid x => isRelative (takeFileName x)
528 takeFileName :: FilePath -> FilePath
529 takeFileName = snd . splitFileName
530
531 -- | Get the base name, without an extension or path.
532 --
533 -- > takeBaseName "/directory/file.ext" == "file"
534 -- > takeBaseName "file/test.txt" == "test"
535 -- > takeBaseName "dave.ext" == "dave"
536 -- > takeBaseName "" == ""
537 -- > takeBaseName "test" == "test"
538 -- > takeBaseName (addTrailingPathSeparator x) == ""
539 -- > takeBaseName "file/file.tar.gz" == "file.tar"
540 takeBaseName :: FilePath -> String
541 takeBaseName = dropExtension . takeFileName
542
543 -- | Set the base name.
544 --
545 -- > replaceBaseName "/directory/other.ext" "file" == "/directory/file.ext"
546 -- > replaceBaseName "file/test.txt" "bob" == "file/bob.txt"
547 -- > replaceBaseName "fred" "bill" == "bill"
548 -- > replaceBaseName "/dave/fred/bob.gz.tar" "new" == "/dave/fred/new.tar"
549 -- > Valid x => replaceBaseName x (takeBaseName x) == x
550 replaceBaseName :: FilePath -> String -> FilePath
551 replaceBaseName pth nam = combineAlways a (nam <.> ext)
552 where
553 (a,b) = splitFileName_ pth
554 ext = takeExtension b
555
556 -- | Is an item either a directory or the last character a path separator?
557 --
558 -- > hasTrailingPathSeparator "test" == False
559 -- > hasTrailingPathSeparator "test/" == True
560 hasTrailingPathSeparator :: FilePath -> Bool
561 hasTrailingPathSeparator "" = False
562 hasTrailingPathSeparator x = isPathSeparator (last x)
563
564
565 hasLeadingPathSeparator :: FilePath -> Bool
566 hasLeadingPathSeparator "" = False
567 hasLeadingPathSeparator x = isPathSeparator (head x)
568
569
570 -- | Add a trailing file path separator if one is not already present.
571 --
572 -- > hasTrailingPathSeparator (addTrailingPathSeparator x)
573 -- > hasTrailingPathSeparator x ==> addTrailingPathSeparator x == x
574 -- > Posix: addTrailingPathSeparator "test/rest" == "test/rest/"
575 addTrailingPathSeparator :: FilePath -> FilePath
576 addTrailingPathSeparator x = if hasTrailingPathSeparator x then x else x ++ [pathSeparator]
577
578
579 -- | Remove any trailing path separators
580 --
581 -- > dropTrailingPathSeparator "file/test/" == "file/test"
582 -- > dropTrailingPathSeparator "/" == "/"
583 -- > Windows: dropTrailingPathSeparator "\\" == "\\"
584 -- > Posix: not (hasTrailingPathSeparator (dropTrailingPathSeparator x)) || isDrive x
585 dropTrailingPathSeparator :: FilePath -> FilePath
586 dropTrailingPathSeparator x =
587 if hasTrailingPathSeparator x && not (isDrive x)
588 then let x' = dropWhileEnd isPathSeparator x
589 in if null x' then [last x] else x'
590 else x
591
592
593 -- | Get the directory name, move up one level.
594 --
595 -- > takeDirectory "/directory/other.ext" == "/directory"
596 -- > takeDirectory x `isPrefixOf` x || takeDirectory x == "."
597 -- > takeDirectory "foo" == "."
598 -- > takeDirectory "/" == "/"
599 -- > takeDirectory "/foo" == "/"
600 -- > takeDirectory "/foo/bar/baz" == "/foo/bar"
601 -- > takeDirectory "/foo/bar/baz/" == "/foo/bar/baz"
602 -- > takeDirectory "foo/bar/baz" == "foo/bar"
603 -- > Windows: takeDirectory "foo\\bar" == "foo"
604 -- > Windows: takeDirectory "foo\\bar\\\\" == "foo\\bar"
605 -- > Windows: takeDirectory "C:\\" == "C:\\"
606 takeDirectory :: FilePath -> FilePath
607 takeDirectory = dropTrailingPathSeparator . dropFileName
608
609 -- | Set the directory, keeping the filename the same.
610 --
611 -- > replaceDirectory "root/file.ext" "/directory/" == "/directory/file.ext"
612 -- > Valid x => replaceDirectory x (takeDirectory x) `equalFilePath` x
613 replaceDirectory :: FilePath -> String -> FilePath
614 replaceDirectory x dir = combineAlways dir (takeFileName x)
615
616
617 -- | An alias for '</>'.
618 combine :: FilePath -> FilePath -> FilePath
619 combine a b | hasLeadingPathSeparator b || hasDrive b = b
620 | otherwise = combineAlways a b
621
622 -- | Combine two paths, assuming rhs is NOT absolute.
623 combineAlways :: FilePath -> FilePath -> FilePath
624 combineAlways a b | null a = b
625 | null b = a
626 | hasTrailingPathSeparator a = a ++ b
627 | otherwise = case a of
628 [a1,':'] | isWindows && isLetter a1 -> a ++ b
629 _ -> a ++ [pathSeparator] ++ b
630
631
632 -- | Combine two paths with a path separator.
633 -- If the second path starts with a path separator or a drive letter, then it returns the second.
634 -- The intention is that @readFile (dir '</>' file)@ will access the same file as
635 -- @setCurrentDirectory dir; readFile file@.
636 --
637 -- > Posix: "/directory" </> "file.ext" == "/directory/file.ext"
638 -- > Windows: "/directory" </> "file.ext" == "/directory\\file.ext"
639 -- > "directory" </> "/file.ext" == "/file.ext"
640 -- > Valid x => (takeDirectory x </> takeFileName x) `equalFilePath` x
641 --
642 -- Combined:
643 --
644 -- > Posix: "/" </> "test" == "/test"
645 -- > Posix: "home" </> "bob" == "home/bob"
646 -- > Posix: "x:" </> "foo" == "x:/foo"
647 -- > Windows: "C:\\foo" </> "bar" == "C:\\foo\\bar"
648 -- > Windows: "home" </> "bob" == "home\\bob"
649 --
650 -- Not combined:
651 --
652 -- > Posix: "home" </> "/bob" == "/bob"
653 -- > Windows: "home" </> "C:\\bob" == "C:\\bob"
654 --
655 -- Not combined (tricky):
656 --
657 -- On Windows, if a filepath starts with a single slash, it is relative to the
658 -- root of the current drive. In [1], this is (confusingly) referred to as an
659 -- absolute path.
660 -- The current behavior of @</>@ is to never combine these forms.
661 --
662 -- > Windows: "home" </> "/bob" == "/bob"
663 -- > Windows: "home" </> "\\bob" == "\\bob"
664 -- > Windows: "C:\\home" </> "\\bob" == "\\bob"
665 --
666 -- On Windows, from [1]: "If a file name begins with only a disk designator
667 -- but not the backslash after the colon, it is interpreted as a relative path
668 -- to the current directory on the drive with the specified letter."
669 -- The current behavior of @</>@ is to never combine these forms.
670 --
671 -- > Windows: "D:\\foo" </> "C:bar" == "C:bar"
672 -- > Windows: "C:\\foo" </> "C:bar" == "C:bar"
673 (</>) :: FilePath -> FilePath -> FilePath
674 (</>) = combine
675
676
677 -- | Split a path by the directory separator.
678 --
679 -- > splitPath "/directory/file.ext" == ["/","directory/","file.ext"]
680 -- > concat (splitPath x) == x
681 -- > splitPath "test//item/" == ["test//","item/"]
682 -- > splitPath "test/item/file" == ["test/","item/","file"]
683 -- > splitPath "" == []
684 -- > Windows: splitPath "c:\\test\\path" == ["c:\\","test\\","path"]
685 -- > Posix: splitPath "/file/test" == ["/","file/","test"]
686 splitPath :: FilePath -> [FilePath]
687 splitPath x = [drive | drive /= ""] ++ f path
688 where
689 (drive,path) = splitDrive x
690
691 f "" = []
692 f y = (a++c) : f d
693 where
694 (a,b) = break isPathSeparator y
695 (c,d) = span isPathSeparator b
696
697 -- | Just as 'splitPath', but don't add the trailing slashes to each element.
698 --
699 -- > splitDirectories "/directory/file.ext" == ["/","directory","file.ext"]
700 -- > splitDirectories "test/file" == ["test","file"]
701 -- > splitDirectories "/test/file" == ["/","test","file"]
702 -- > Windows: splitDirectories "C:\\test\\file" == ["C:\\", "test", "file"]
703 -- > Valid x => joinPath (splitDirectories x) `equalFilePath` x
704 -- > splitDirectories "" == []
705 -- > Windows: splitDirectories "C:\\test\\\\\\file" == ["C:\\", "test", "file"]
706 -- > splitDirectories "/test///file" == ["/","test","file"]
707 splitDirectories :: FilePath -> [FilePath]
708 splitDirectories = map dropTrailingPathSeparator . splitPath
709
710
711 -- | Join path elements back together.
712 --
713 -- > joinPath ["/","directory/","file.ext"] == "/directory/file.ext"
714 -- > Valid x => joinPath (splitPath x) == x
715 -- > joinPath [] == ""
716 -- > Posix: joinPath ["test","file","path"] == "test/file/path"
717 joinPath :: [FilePath] -> FilePath
718 -- Note that this definition on c:\\c:\\, join then split will give c:\\.
719 joinPath = foldr combine ""
720
721
722
723
724
725
726 ---------------------------------------------------------------------
727 -- File name manipulators
728
729 -- | Equality of two 'FilePath's.
730 -- If you call @System.Directory.canonicalizePath@
731 -- first this has a much better chance of working.
732 -- Note that this doesn't follow symlinks or DOSNAM~1s.
733 --
734 -- > x == y ==> equalFilePath x y
735 -- > normalise x == normalise y ==> equalFilePath x y
736 -- > equalFilePath "foo" "foo/"
737 -- > not (equalFilePath "foo" "/foo")
738 -- > Posix: not (equalFilePath "foo" "FOO")
739 -- > Windows: equalFilePath "foo" "FOO"
740 -- > Windows: not (equalFilePath "C:" "C:/")
741 equalFilePath :: FilePath -> FilePath -> Bool
742 equalFilePath a b = f a == f b
743 where
744 f x | isWindows = dropTrailingPathSeparator $ map toLower $ normalise x
745 | otherwise = dropTrailingPathSeparator $ normalise x
746
747
748 -- | Contract a filename, based on a relative path. Note that the resulting path
749 -- will never introduce @..@ paths, as the presence of symlinks means @..\/b@
750 -- may not reach @a\/b@ if it starts from @a\/c@. For a worked example see
751 -- <http://neilmitchell.blogspot.co.uk/2015/10/filepaths-are-subtle-symlinks-are-hard.html this blog post>.
752 --
753 -- The corresponding @makeAbsolute@ function can be found in
754 -- @System.Directory@.
755 --
756 -- > makeRelative "/directory" "/directory/file.ext" == "file.ext"
757 -- > Valid x => makeRelative (takeDirectory x) x `equalFilePath` takeFileName x
758 -- > makeRelative x x == "."
759 -- > Valid x y => equalFilePath x y || (isRelative x && makeRelative y x == x) || equalFilePath (y </> makeRelative y x) x
760 -- > Windows: makeRelative "C:\\Home" "c:\\home\\bob" == "bob"
761 -- > Windows: makeRelative "C:\\Home" "c:/home/bob" == "bob"
762 -- > Windows: makeRelative "C:\\Home" "D:\\Home\\Bob" == "D:\\Home\\Bob"
763 -- > Windows: makeRelative "C:\\Home" "C:Home\\Bob" == "C:Home\\Bob"
764 -- > Windows: makeRelative "/Home" "/home/bob" == "bob"
765 -- > Windows: makeRelative "/" "//" == "//"
766 -- > Posix: makeRelative "/Home" "/home/bob" == "/home/bob"
767 -- > Posix: makeRelative "/home/" "/home/bob/foo/bar" == "bob/foo/bar"
768 -- > Posix: makeRelative "/fred" "bob" == "bob"
769 -- > Posix: makeRelative "/file/test" "/file/test/fred" == "fred"
770 -- > Posix: makeRelative "/file/test" "/file/test/fred/" == "fred/"
771 -- > Posix: makeRelative "some/path" "some/path/a/b/c" == "a/b/c"
772 makeRelative :: FilePath -> FilePath -> FilePath
773 makeRelative root path
774 | equalFilePath root path = "."
775 | takeAbs root /= takeAbs path = path
776 | otherwise = f (dropAbs root) (dropAbs path)
777 where
778 f "" y = dropWhile isPathSeparator y
779 f x y = let (x1,x2) = g x
780 (y1,y2) = g y
781 in if equalFilePath x1 y1 then f x2 y2 else path
782
783 g x = (dropWhile isPathSeparator a, dropWhile isPathSeparator b)
784 where (a,b) = break isPathSeparator $ dropWhile isPathSeparator x
785
786 -- on windows, need to drop '/' which is kind of absolute, but not a drive
787 dropAbs x | hasLeadingPathSeparator x && not (hasDrive x) = tail x
788 dropAbs x = dropDrive x
789
790 takeAbs x | hasLeadingPathSeparator x && not (hasDrive x) = [pathSeparator]
791 takeAbs x = map (\y -> if isPathSeparator y then pathSeparator else toLower y) $ takeDrive x
792
793 -- | Normalise a file
794 --
795 -- * \/\/ outside of the drive can be made blank
796 --
797 -- * \/ -> 'pathSeparator'
798 --
799 -- * .\/ -> \"\"
800 --
801 -- > Posix: normalise "/file/\\test////" == "/file/\\test/"
802 -- > Posix: normalise "/file/./test" == "/file/test"
803 -- > Posix: normalise "/test/file/../bob/fred/" == "/test/file/../bob/fred/"
804 -- > Posix: normalise "../bob/fred/" == "../bob/fred/"
805 -- > Posix: normalise "./bob/fred/" == "bob/fred/"
806 -- > Windows: normalise "c:\\file/bob\\" == "C:\\file\\bob\\"
807 -- > Windows: normalise "c:\\" == "C:\\"
808 -- > Windows: normalise "C:.\\" == "C:"
809 -- > Windows: normalise "\\\\server\\test" == "\\\\server\\test"
810 -- > Windows: normalise "//server/test" == "\\\\server\\test"
811 -- > Windows: normalise "c:/file" == "C:\\file"
812 -- > Windows: normalise "/file" == "\\file"
813 -- > Windows: normalise "\\" == "\\"
814 -- > Windows: normalise "/./" == "\\"
815 -- > normalise "." == "."
816 -- > Posix: normalise "./" == "./"
817 -- > Posix: normalise "./." == "./"
818 -- > Posix: normalise "/./" == "/"
819 -- > Posix: normalise "/" == "/"
820 -- > Posix: normalise "bob/fred/." == "bob/fred/"
821 -- > Posix: normalise "//home" == "/home"
822 normalise :: FilePath -> FilePath
823 normalise path = result ++ [pathSeparator | addPathSeparator]
824 where
825 (drv,pth) = splitDrive path
826 result = joinDrive' (normaliseDrive drv) (f pth)
827
828 joinDrive' "" "" = "."
829 joinDrive' d p = joinDrive d p
830
831 addPathSeparator = isDirPath pth
832 && not (hasTrailingPathSeparator result)
833 && not (isRelativeDrive drv)
834
835 isDirPath xs = hasTrailingPathSeparator xs
836 || not (null xs) && last xs == '.' && hasTrailingPathSeparator (init xs)
837
838 f = joinPath . dropDots . propSep . splitDirectories
839
840 propSep (x:xs) | all isPathSeparator x = [pathSeparator] : xs
841 | otherwise = x : xs
842 propSep [] = []
843
844 dropDots = filter ("." /=)
845
846 normaliseDrive :: FilePath -> FilePath
847 normaliseDrive "" = ""
848 normaliseDrive _ | isPosix = [pathSeparator]
849 normaliseDrive drive = if isJust $ readDriveLetter x2
850 then map toUpper x2
851 else x2
852 where
853 x2 = map repSlash drive
854
855 repSlash x = if isPathSeparator x then pathSeparator else x
856
857 -- Information for validity functions on Windows. See [1].
858 isBadCharacter :: Char -> Bool
859 isBadCharacter x = x >= '\0' && x <= '\31' || x `elem` ":*?><|\""
860
861 badElements :: [FilePath]
862 badElements =
863 ["CON","PRN","AUX","NUL","CLOCK$"
864 ,"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9"
865 ,"LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"]
866
867
868 -- | Is a FilePath valid, i.e. could you create a file like it? This function checks for invalid names,
869 -- and invalid characters, but does not check if length limits are exceeded, as these are typically
870 -- filesystem dependent.
871 --
872 -- > isValid "" == False
873 -- > isValid "\0" == False
874 -- > Posix: isValid "/random_ path:*" == True
875 -- > Posix: isValid x == not (null x)
876 -- > Windows: isValid "c:\\test" == True
877 -- > Windows: isValid "c:\\test:of_test" == False
878 -- > Windows: isValid "test*" == False
879 -- > Windows: isValid "c:\\test\\nul" == False
880 -- > Windows: isValid "c:\\test\\prn.txt" == False
881 -- > Windows: isValid "c:\\nul\\file" == False
882 -- > Windows: isValid "\\\\" == False
883 -- > Windows: isValid "\\\\\\foo" == False
884 -- > Windows: isValid "\\\\?\\D:file" == False
885 -- > Windows: isValid "foo\tbar" == False
886 -- > Windows: isValid "nul .txt" == False
887 -- > Windows: isValid " nul.txt" == True
888 isValid :: FilePath -> Bool
889 isValid "" = False
890 isValid x | '\0' `elem` x = False
891 isValid _ | isPosix = True
892 isValid path =
893 not (any isBadCharacter x2) &&
894 not (any f $ splitDirectories x2) &&
895 not (isJust (readDriveShare x1) && all isPathSeparator x1) &&
896 not (isJust (readDriveUNC x1) && not (hasTrailingPathSeparator x1))
897 where
898 (x1,x2) = splitDrive path
899 f x = map toUpper (dropWhileEnd (== ' ') $ dropExtensions x) `elem` badElements
900
901
902 -- | Take a FilePath and make it valid; does not change already valid FilePaths.
903 --
904 -- > isValid (makeValid x)
905 -- > isValid x ==> makeValid x == x
906 -- > makeValid "" == "_"
907 -- > makeValid "file\0name" == "file_name"
908 -- > Windows: makeValid "c:\\already\\/valid" == "c:\\already\\/valid"
909 -- > Windows: makeValid "c:\\test:of_test" == "c:\\test_of_test"
910 -- > Windows: makeValid "test*" == "test_"
911 -- > Windows: makeValid "c:\\test\\nul" == "c:\\test\\nul_"
912 -- > Windows: makeValid "c:\\test\\prn.txt" == "c:\\test\\prn_.txt"
913 -- > Windows: makeValid "c:\\test/prn.txt" == "c:\\test/prn_.txt"
914 -- > Windows: makeValid "c:\\nul\\file" == "c:\\nul_\\file"
915 -- > Windows: makeValid "\\\\\\foo" == "\\\\drive"
916 -- > Windows: makeValid "\\\\?\\D:file" == "\\\\?\\D:\\file"
917 -- > Windows: makeValid "nul .txt" == "nul _.txt"
918 makeValid :: FilePath -> FilePath
919 makeValid "" = "_"
920 makeValid path
921 | isPosix = map (\x -> if x == '\0' then '_' else x) path
922 | isJust (readDriveShare drv) && all isPathSeparator drv = take 2 drv ++ "drive"
923 | isJust (readDriveUNC drv) && not (hasTrailingPathSeparator drv) =
924 makeValid (drv ++ [pathSeparator] ++ pth)
925 | otherwise = joinDrive drv $ validElements $ validChars pth
926 where
927 (drv,pth) = splitDrive path
928
929 validChars = map f
930 f x = if isBadCharacter x then '_' else x
931
932 validElements x = joinPath $ map g $ splitPath x
933 g x = h a ++ b
934 where (a,b) = break isPathSeparator x
935 h x = if map toUpper (dropWhileEnd (== ' ') a) `elem` badElements then a ++ "_" <.> b else x
936 where (a,b) = splitExtensions x
937
938
939 -- | Is a path relative, or is it fixed to the root?
940 --
941 -- > Windows: isRelative "path\\test" == True
942 -- > Windows: isRelative "c:\\test" == False
943 -- > Windows: isRelative "c:test" == True
944 -- > Windows: isRelative "c:\\" == False
945 -- > Windows: isRelative "c:/" == False
946 -- > Windows: isRelative "c:" == True
947 -- > Windows: isRelative "\\\\foo" == False
948 -- > Windows: isRelative "\\\\?\\foo" == False
949 -- > Windows: isRelative "\\\\?\\UNC\\foo" == False
950 -- > Windows: isRelative "/foo" == True
951 -- > Windows: isRelative "\\foo" == True
952 -- > Posix: isRelative "test/path" == True
953 -- > Posix: isRelative "/test" == False
954 -- > Posix: isRelative "/" == False
955 --
956 -- According to [1]:
957 --
958 -- * "A UNC name of any format [is never relative]."
959 --
960 -- * "You cannot use the "\\?\" prefix with a relative path."
961 isRelative :: FilePath -> Bool
962 isRelative x = null drive || isRelativeDrive drive
963 where drive = takeDrive x
964
965
966 {- c:foo -}
967 -- From [1]: "If a file name begins with only a disk designator but not the
968 -- backslash after the colon, it is interpreted as a relative path to the
969 -- current directory on the drive with the specified letter."
970 isRelativeDrive :: String -> Bool
971 isRelativeDrive x =
972 maybe False (not . hasTrailingPathSeparator . fst) (readDriveLetter x)
973
974
975 -- | @not . 'isRelative'@
976 --
977 -- > isAbsolute x == not (isRelative x)
978 isAbsolute :: FilePath -> Bool
979 isAbsolute = not . isRelative
980
981
982 -----------------------------------------------------------------------------
983 -- dropWhileEnd (>2) [1,2,3,4,1,2,3,4] == [1,2,3,4,1,2])
984 -- Note that Data.List.dropWhileEnd is only available in base >= 4.5.
985 dropWhileEnd :: (a -> Bool) -> [a] -> [a]
986 dropWhileEnd p = reverse . dropWhile p . reverse
987
988 -- takeWhileEnd (>2) [1,2,3,4,1,2,3,4] == [3,4])
989 takeWhileEnd :: (a -> Bool) -> [a] -> [a]
990 takeWhileEnd p = reverse . takeWhile p . reverse
991
992 -- spanEnd (>2) [1,2,3,4,1,2,3,4] = ([1,2,3,4,1,2], [3,4])
993 spanEnd :: (a -> Bool) -> [a] -> ([a], [a])
994 spanEnd p xs = (dropWhileEnd p xs, takeWhileEnd p xs)
995
996 -- breakEnd (< 2) [1,2,3,4,1,2,3,4] == ([1,2,3,4,1],[2,3,4])
997 breakEnd :: (a -> Bool) -> [a] -> ([a], [a])
998 breakEnd p = spanEnd (not . p)