You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

536 lines
14 KiB

  1. {-# LANGUAGE TupleSections #-}
  2. {-# OPTIONS_GHC -Wall #-}
  3. -- | The equivalent of "System.FilePath" on raw (byte string) file paths.
  4. --
  5. -- Not all functions of "System.FilePath" are implemented yet. Feel free to contribute!
  6. module System.Posix.FilePath (
  7. pathSeparator
  8. , isPathSeparator
  9. , searchPathSeparator
  10. , isSearchPathSeparator
  11. , extSeparator
  12. , isExtSeparator
  13. , splitExtension
  14. , takeExtension
  15. , replaceExtension
  16. , dropExtension
  17. , addExtension
  18. , hasExtension
  19. , (<.>)
  20. , splitExtensions
  21. , dropExtensions
  22. , takeExtensions
  23. , splitFileName
  24. , takeFileName
  25. , replaceFileName
  26. , dropFileName
  27. , takeBaseName
  28. , replaceBaseName
  29. , takeDirectory
  30. , replaceDirectory
  31. , combine
  32. , (</>)
  33. , splitPath
  34. , joinPath
  35. , normalise
  36. , splitDirectories
  37. , hasTrailingPathSeparator
  38. , addTrailingPathSeparator
  39. , dropTrailingPathSeparator
  40. , isRelative
  41. , isAbsolute
  42. , isValid
  43. , equalFilePath
  44. , module System.Posix.ByteString.FilePath
  45. ) where
  46. import Data.ByteString (ByteString)
  47. import qualified Data.ByteString as BS
  48. import System.Posix.ByteString.FilePath
  49. import Data.Maybe (isJust)
  50. import Data.Word8
  51. import Control.Arrow (second)
  52. -- $setup
  53. -- >>> import Data.Char
  54. -- >>> import Test.QuickCheck
  55. -- >>> import Control.Applicative
  56. -- >>> import qualified Data.ByteString as BS
  57. -- >>> import Data.ByteString (ByteString)
  58. -- >>> instance Arbitrary ByteString where arbitrary = BS.pack <$> arbitrary
  59. -- >>> instance CoArbitrary ByteString where coarbitrary = coarbitrary . BS.unpack
  60. --
  61. -- >>> let _chr :: Word8 -> Char; _chr = chr . fromIntegral
  62. -- | Path separator character
  63. pathSeparator :: Word8
  64. pathSeparator = _slash
  65. -- | Check if a character is the path separator
  66. --
  67. -- prop> \n -> (_chr n == '/') == isPathSeparator n
  68. isPathSeparator :: Word8 -> Bool
  69. isPathSeparator = (== pathSeparator)
  70. -- | Search path separator
  71. searchPathSeparator :: Word8
  72. searchPathSeparator = _colon
  73. -- | Check if a character is the search path separator
  74. --
  75. -- prop> \n -> (_chr n == ':') == isSearchPathSeparator n
  76. isSearchPathSeparator :: Word8 -> Bool
  77. isSearchPathSeparator = (== searchPathSeparator)
  78. -- | File extension separator
  79. extSeparator :: Word8
  80. extSeparator = _period
  81. -- | Check if a character is the file extension separator
  82. --
  83. -- prop> \n -> (_chr n == '.') == isExtSeparator n
  84. isExtSeparator :: Word8 -> Bool
  85. isExtSeparator = (== extSeparator)
  86. ------------------------
  87. -- extension stuff
  88. -- | Split a 'RawFilePath' into a path+filename and extension
  89. --
  90. -- >>> splitExtension "file.exe"
  91. -- ("file",".exe")
  92. -- >>> splitExtension "file"
  93. -- ("file","")
  94. -- >>> splitExtension "/path/file.tar.gz"
  95. -- ("/path/file.tar",".gz")
  96. --
  97. -- prop> \path -> uncurry (BS.append) (splitExtension path) == path
  98. splitExtension :: RawFilePath -> (RawFilePath, ByteString)
  99. splitExtension x = if BS.null basename
  100. then (x,BS.empty)
  101. else (BS.append path (BS.init basename),BS.cons extSeparator fileExt)
  102. where
  103. (path,file) = splitFileNameRaw x
  104. (basename,fileExt) = BS.breakEnd isExtSeparator file
  105. -- | Get the final extension from a 'RawFilePath'
  106. --
  107. -- >>> takeExtension "file.exe"
  108. -- ".exe"
  109. -- >>> takeExtension "file"
  110. -- ""
  111. -- >>> takeExtension "/path/file.tar.gz"
  112. -- ".gz"
  113. takeExtension :: RawFilePath -> ByteString
  114. takeExtension = snd . splitExtension
  115. -- | Change a file's extension
  116. --
  117. -- prop> \path -> let ext = takeExtension path in replaceExtension path ext == path
  118. replaceExtension :: RawFilePath -> ByteString -> RawFilePath
  119. replaceExtension path ext = dropExtension path <.> ext
  120. -- | Drop the final extension from a 'RawFilePath'
  121. --
  122. -- >>> dropExtension "file.exe"
  123. -- "file"
  124. -- >>> dropExtension "file"
  125. -- "file"
  126. -- >>> dropExtension "/path/file.tar.gz"
  127. -- "/path/file.tar"
  128. dropExtension :: RawFilePath -> RawFilePath
  129. dropExtension = fst . splitExtension
  130. -- | Add an extension to a 'RawFilePath'
  131. --
  132. -- >>> addExtension "file" ".exe"
  133. -- "file.exe"
  134. -- >>> addExtension "file.tar" ".gz"
  135. -- "file.tar.gz"
  136. -- >>> addExtension "/path/" ".ext"
  137. -- "/path/.ext"
  138. addExtension :: RawFilePath -> ByteString -> RawFilePath
  139. addExtension file ext
  140. | BS.null ext = file
  141. | isExtSeparator (BS.head ext) = BS.append file ext
  142. | otherwise = BS.intercalate (BS.singleton extSeparator) [file, ext]
  143. -- | Operator version of 'addExtension'
  144. (<.>) :: RawFilePath -> ByteString -> RawFilePath
  145. (<.>) = addExtension
  146. -- | Check if a 'RawFilePath' has an extension
  147. --
  148. -- >>> hasExtension "file"
  149. -- False
  150. -- >>> hasExtension "file.tar"
  151. -- True
  152. -- >>> hasExtension "/path.part1/"
  153. -- False
  154. hasExtension :: RawFilePath -> Bool
  155. hasExtension = isJust . BS.elemIndex extSeparator . takeFileName
  156. -- | Split a 'RawFilePath' on the first extension
  157. --
  158. -- >>> splitExtensions "/path/file.tar.gz"
  159. -- ("/path/file",".tar.gz")
  160. --
  161. -- prop> \path -> uncurry addExtension (splitExtensions path) == path
  162. splitExtensions :: RawFilePath -> (RawFilePath, ByteString)
  163. splitExtensions x = if BS.null basename
  164. then (path,fileExt)
  165. else (BS.append path basename,fileExt)
  166. where
  167. (path,file) = splitFileNameRaw x
  168. (basename,fileExt) = BS.break isExtSeparator file
  169. -- | Remove all extensions from a 'RawFilePath'
  170. --
  171. -- >>> dropExtensions "/path/file.tar.gz"
  172. -- "/path/file"
  173. dropExtensions :: RawFilePath -> RawFilePath
  174. dropExtensions = fst . splitExtensions
  175. -- | Take all extensions from a 'RawFilePath'
  176. --
  177. -- >>> takeExtensions "/path/file.tar.gz"
  178. -- ".tar.gz"
  179. takeExtensions :: RawFilePath -> ByteString
  180. takeExtensions = snd . splitExtensions
  181. ------------------------
  182. -- more stuff
  183. -- | Split a 'RawFilePath' into (path,file). 'combine' is the inverse
  184. --
  185. -- >>> splitFileName "path/file.txt"
  186. -- ("path/","file.txt")
  187. -- >>> splitFileName "path/"
  188. -- ("path/","")
  189. -- >>> splitFileName "file.txt"
  190. -- ("./","file.txt")
  191. --
  192. -- prop> \path -> uncurry combine (splitFileName path) == path || fst (splitFileName path) == "./"
  193. splitFileName :: RawFilePath -> (RawFilePath, RawFilePath)
  194. splitFileName x = if BS.null path
  195. then (dotSlash, file)
  196. else (path,file)
  197. where
  198. (path,file) = splitFileNameRaw x
  199. dotSlash = _period `BS.cons` (BS.singleton pathSeparator)
  200. -- | Get the file name
  201. --
  202. -- >>> takeFileName "path/file.txt"
  203. -- "file.txt"
  204. -- >>> takeFileName "path/"
  205. -- ""
  206. takeFileName :: RawFilePath -> RawFilePath
  207. takeFileName = snd . splitFileName
  208. -- | Change the file name
  209. --
  210. -- prop> \path -> replaceFileName path (takeFileName path) == path
  211. replaceFileName :: RawFilePath -> ByteString -> RawFilePath
  212. replaceFileName x y = fst (splitFileNameRaw x) </> y
  213. -- | Drop the file name
  214. --
  215. -- >>> dropFileName "path/file.txt"
  216. -- "path/"
  217. -- >>> dropFileName "file.txt"
  218. -- "./"
  219. dropFileName :: RawFilePath -> RawFilePath
  220. dropFileName = fst . splitFileName
  221. -- | Get the file name, without a trailing extension
  222. --
  223. -- >>> takeBaseName "path/file.tar.gz"
  224. -- "file.tar"
  225. -- >>> takeBaseName ""
  226. -- ""
  227. takeBaseName :: RawFilePath -> ByteString
  228. takeBaseName = dropExtension . takeFileName
  229. -- | Change the base name
  230. --
  231. -- >>> replaceBaseName "path/file.tar.gz" "bob"
  232. -- "path/bob.gz"
  233. --
  234. -- prop> \path -> replaceBaseName path (takeBaseName path) == path
  235. replaceBaseName :: RawFilePath -> ByteString -> RawFilePath
  236. replaceBaseName path name = combineRaw dir (name <.> ext)
  237. where
  238. (dir,file) = splitFileNameRaw path
  239. ext = takeExtension file
  240. -- | Get the directory, moving up one level if it's already a directory
  241. --
  242. -- >>> takeDirectory "path/file.txt"
  243. -- "path"
  244. -- >>> takeDirectory "file"
  245. -- "."
  246. -- >>> takeDirectory "/path/to/"
  247. -- "/path/to"
  248. -- >>> takeDirectory "/path/to"
  249. -- "/path"
  250. takeDirectory :: RawFilePath -> RawFilePath
  251. takeDirectory x = case () of
  252. () | x == BS.singleton pathSeparator -> x
  253. | BS.null res && not (BS.null file) -> file
  254. | otherwise -> res
  255. where
  256. res = fst $ BS.spanEnd isPathSeparator file
  257. file = dropFileName x
  258. -- | Change the directory component of a 'RawFilePath'
  259. --
  260. -- prop> \path -> replaceDirectory path (takeDirectory path) `equalFilePath` path || takeDirectory path == "."
  261. replaceDirectory :: RawFilePath -> ByteString -> RawFilePath
  262. replaceDirectory file dir = combineRaw dir (takeFileName file)
  263. -- | Join two paths together
  264. --
  265. -- >>> combine "/" "file"
  266. -- "/file"
  267. -- >>> combine "/path/to" "file"
  268. -- "/path/to/file"
  269. -- >>> combine "file" "/absolute/path"
  270. -- "/absolute/path"
  271. combine :: RawFilePath -> RawFilePath -> RawFilePath
  272. combine a b | not (BS.null b) && isPathSeparator (BS.head b) = b
  273. | otherwise = combineRaw a b
  274. -- | Operator version of combine
  275. (</>) :: RawFilePath -> RawFilePath -> RawFilePath
  276. (</>) = combine
  277. -- | Split a path into a list of components:
  278. --
  279. -- >>> splitPath "/path/to/file.txt"
  280. -- ["/","path/","to/","file.txt"]
  281. --
  282. -- prop> \path -> BS.concat (splitPath path) == path
  283. splitPath :: RawFilePath -> [RawFilePath]
  284. splitPath = splitter
  285. where
  286. splitter x
  287. | BS.null x = []
  288. | otherwise = case BS.elemIndex pathSeparator x of
  289. Nothing -> [x]
  290. Just ix -> case BS.findIndex (not . isPathSeparator) $ BS.drop (ix+1) x of
  291. Nothing -> [x]
  292. Just runlen -> uncurry (:) . second splitter $ BS.splitAt (ix+1+runlen) x
  293. -- | Like 'splitPath', but without trailing slashes
  294. --
  295. -- >>> splitDirectories "/path/to/file.txt"
  296. -- ["/","path","to","file.txt"]
  297. -- >>> splitDirectories ""
  298. -- []
  299. splitDirectories :: RawFilePath -> [RawFilePath]
  300. splitDirectories x
  301. | BS.null x = []
  302. | isPathSeparator (BS.head x) = let (root,rest) = BS.splitAt 1 x
  303. in root : splitter rest
  304. | otherwise = splitter x
  305. where
  306. splitter = filter (not . BS.null) . BS.split pathSeparator
  307. -- | Join a split path back together
  308. --
  309. -- prop> \path -> joinPath (splitPath path) == path
  310. --
  311. -- >>> joinPath ["path","to","file.txt"]
  312. -- "path/to/file.txt"
  313. joinPath :: [RawFilePath] -> RawFilePath
  314. joinPath = foldr (</>) BS.empty
  315. -- |Normalise a file.
  316. --
  317. -- >>> normalise "/file/\\test////"
  318. -- "/file/\\test/"
  319. -- >>> normalise "/file/./test"
  320. -- "/file/test"
  321. -- >>> normalise "/test/file/../bob/fred/"
  322. -- "/test/file/../bob/fred/"
  323. -- >>> normalise "../bob/fred/"
  324. -- "../bob/fred/"
  325. -- >>> normalise "./bob/fred/"
  326. -- "bob/fred/"
  327. -- >>> normalise "./bob////.fred/./...///./..///#."
  328. -- "bob/.fred/.../../#."
  329. -- >>> normalise "."
  330. -- "."
  331. -- >>> normalise "./"
  332. -- "./"
  333. -- >>> normalise "./."
  334. -- "./"
  335. -- >>> normalise "/./"
  336. -- "/"
  337. -- >>> normalise "/"
  338. -- "/"
  339. -- >>> normalise "bob/fred/."
  340. -- "bob/fred/"
  341. -- >>> normalise "//home"
  342. -- "/home"
  343. normalise :: RawFilePath -> RawFilePath
  344. normalise filepath =
  345. result `BS.append`
  346. (if addPathSeparator
  347. then BS.singleton pathSeparator
  348. else BS.empty)
  349. where
  350. result = let n = f filepath
  351. in if BS.null n
  352. then BS.singleton _period
  353. else n
  354. addPathSeparator = isDirPath filepath &&
  355. not (hasTrailingPathSeparator result)
  356. isDirPath xs = hasTrailingPathSeparator xs
  357. || not (BS.null xs) && BS.last xs == _period
  358. && hasTrailingPathSeparator (BS.init xs)
  359. f = joinPath . dropDots . propSep . splitDirectories
  360. propSep :: [ByteString] -> [ByteString]
  361. propSep (x:xs)
  362. | BS.all (== pathSeparator) x = BS.singleton pathSeparator : xs
  363. | otherwise = x : xs
  364. propSep [] = []
  365. dropDots :: [ByteString] -> [ByteString]
  366. dropDots = filter (BS.singleton _period /=)
  367. ------------------------
  368. -- trailing path separators
  369. -- | Check if the last character of a 'RawFilePath' is '/'.
  370. --
  371. -- >>> hasTrailingPathSeparator "/path/"
  372. -- True
  373. -- >>> hasTrailingPathSeparator "/"
  374. -- True
  375. -- >>> hasTrailingPathSeparator "/path"
  376. -- False
  377. hasTrailingPathSeparator :: RawFilePath -> Bool
  378. hasTrailingPathSeparator x
  379. | BS.null x = False
  380. | otherwise = isPathSeparator $ BS.last x
  381. -- | Add a trailing path separator.
  382. --
  383. -- >>> addTrailingPathSeparator "/path"
  384. -- "/path/"
  385. -- >>> addTrailingPathSeparator "/path/"
  386. -- "/path/"
  387. -- >>> addTrailingPathSeparator "/"
  388. -- "/"
  389. addTrailingPathSeparator :: RawFilePath -> RawFilePath
  390. addTrailingPathSeparator x = if hasTrailingPathSeparator x
  391. then x
  392. else x `BS.snoc` pathSeparator
  393. -- | Remove a trailing path separator
  394. --
  395. -- >>> dropTrailingPathSeparator "/path/"
  396. -- "/path"
  397. -- >>> dropTrailingPathSeparator "/path////"
  398. -- "/path"
  399. -- >>> dropTrailingPathSeparator "/"
  400. -- "/"
  401. -- >>> dropTrailingPathSeparator "//"
  402. -- "/"
  403. dropTrailingPathSeparator :: RawFilePath -> RawFilePath
  404. dropTrailingPathSeparator x
  405. | x == BS.singleton pathSeparator = x
  406. | otherwise = if hasTrailingPathSeparator x
  407. then dropTrailingPathSeparator $ BS.init x
  408. else x
  409. ------------------------
  410. -- Filename/system stuff
  411. -- | Check if a path is absolute
  412. --
  413. -- >>> isAbsolute "/path"
  414. -- True
  415. -- >>> isAbsolute "path"
  416. -- False
  417. -- >>> isAbsolute ""
  418. -- False
  419. isAbsolute :: RawFilePath -> Bool
  420. isAbsolute x
  421. | BS.length x > 0 = isPathSeparator (BS.head x)
  422. | otherwise = False
  423. -- | Check if a path is relative
  424. --
  425. -- prop> \path -> isRelative path /= isAbsolute path
  426. isRelative :: RawFilePath -> Bool
  427. isRelative = not . isAbsolute
  428. -- | Is a FilePath valid, i.e. could you create a file like it?
  429. --
  430. -- >>> isValid ""
  431. -- False
  432. -- >>> isValid "\0"
  433. -- False
  434. -- >>> isValid "/random_ path:*"
  435. -- True
  436. isValid :: RawFilePath -> Bool
  437. isValid filepath
  438. | BS.null filepath = False
  439. | _nul `BS.elem` filepath = False
  440. | otherwise = True
  441. -- |Equality of two filepaths. The filepaths are normalised
  442. -- and trailing path separators are dropped.
  443. --
  444. -- >>> equalFilePath "foo" "foo"
  445. -- True
  446. -- >>> equalFilePath "foo" "foo/"
  447. -- True
  448. -- >>> equalFilePath "foo" "./foo"
  449. -- True
  450. -- >>> equalFilePath "foo" "/foo"
  451. -- False
  452. -- >>> equalFilePath "foo" "FOO"
  453. -- False
  454. -- >>> equalFilePath "foo" "../foo"
  455. -- False
  456. --
  457. -- prop> \p -> equalFilePath p p
  458. equalFilePath :: RawFilePath -> RawFilePath -> Bool
  459. equalFilePath p1 p2 = f p1 == f p2
  460. where
  461. f x = dropTrailingPathSeparator $ normalise x
  462. ------------------------
  463. -- internal stuff
  464. -- Just split the input FileName without adding/normalizing or changing
  465. -- anything.
  466. splitFileNameRaw :: RawFilePath -> (RawFilePath, RawFilePath)
  467. splitFileNameRaw x = BS.breakEnd isPathSeparator x
  468. -- | Combine two paths, assuming rhs is NOT absolute.
  469. combineRaw :: RawFilePath -> RawFilePath -> RawFilePath
  470. combineRaw a b | BS.null a = b
  471. | BS.null b = a
  472. | isPathSeparator (BS.last a) = BS.append a b
  473. | otherwise = BS.intercalate (BS.singleton pathSeparator) [a, b]