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.
 
 
 
 

193 lines
5.2 KiB

  1. {-# LANGUAGE TemplateHaskell #-}
  2. {-# LANGUAGE DeriveGeneric #-}
  3. {-# LANGUAGE DeriveDataTypeable #-}
  4. {-# LANGUAGE EmptyDataDecls #-}
  5. -- | A normalizing well-typed path type.
  6. module Path
  7. (-- * Types
  8. Path
  9. ,Abs
  10. ,Rel
  11. ,File
  12. ,Dir
  13. -- * Parsing
  14. ,parseAbsDir
  15. ,parseRelDir
  16. ,parseAbsFile
  17. ,parseRelFile
  18. ,PathParseException
  19. -- * Constructors
  20. ,mkAbsDir
  21. ,mkRelDir
  22. ,mkAbsFile
  23. ,mkRelFile
  24. -- * Conversion
  25. ,toFilePath
  26. )
  27. where
  28. import Control.Exception (Exception)
  29. import Control.Monad.Catch (MonadThrow(..))
  30. import Data.Data
  31. import Data.List
  32. import Language.Haskell.TH
  33. import Path.Internal
  34. import qualified System.FilePath as FilePath
  35. --------------------------------------------------------------------------------
  36. -- Types
  37. -- | An absolute path.
  38. data Abs
  39. -- | A relative path; one without a root.
  40. data Rel
  41. -- | A file path.
  42. data File
  43. -- | A directory path.
  44. data Dir
  45. -- | Exception when parsing a location.
  46. data PathParseException
  47. = InvalidAbsDir FilePath
  48. | InvalidRelDir FilePath
  49. | InvalidAbsFile FilePath
  50. | InvalidRelFile FilePath
  51. deriving (Show,Typeable)
  52. instance Exception PathParseException
  53. --------------------------------------------------------------------------------
  54. -- Parsers
  55. -- | Get a location for an absolute directory. Produces a normalized
  56. -- path which always ends in a path separator.
  57. --
  58. -- Throws: 'PathParseException'
  59. --
  60. parseAbsDir :: MonadThrow m
  61. => FilePath -> m (Path Abs Dir)
  62. parseAbsDir filepath =
  63. if FilePath.isAbsolute filepath &&
  64. not (null (normalizeDir filepath)) &&
  65. not (isPrefixOf "~/" filepath)
  66. then return (Path (normalizeDir filepath))
  67. else throwM (InvalidAbsDir filepath)
  68. -- | Get a location for a relative directory. Produces a normalized
  69. -- path which always ends in a path separator.
  70. --
  71. -- Throws: 'PathParseException'
  72. --
  73. parseRelDir :: MonadThrow m
  74. => FilePath -> m (Path Rel Dir)
  75. parseRelDir filepath =
  76. if not (FilePath.isAbsolute filepath) &&
  77. not (null filepath) &&
  78. not (isPrefixOf "~/" filepath) &&
  79. not (null (normalizeDir filepath))
  80. then return (Path (normalizeDir filepath))
  81. else throwM (InvalidRelDir filepath)
  82. -- | Get a location for an absolute file. Produces a normalized
  83. -- path which always ends in a path separator.
  84. --
  85. -- Throws: 'PathParseException'
  86. --
  87. parseAbsFile :: MonadThrow m
  88. => FilePath -> m (Path Abs File)
  89. parseAbsFile filepath =
  90. if FilePath.isAbsolute filepath &&
  91. not (FilePath.hasTrailingPathSeparator filepath) &&
  92. not (isPrefixOf "~/" filepath) &&
  93. not (null (normalizeFile filepath))
  94. then return (Path (normalizeFile filepath))
  95. else throwM (InvalidAbsFile filepath)
  96. -- | Get a location for a relative file. Produces a normalized
  97. -- path which always ends in a path separator.
  98. --
  99. -- Throws: 'PathParseException'
  100. --
  101. parseRelFile :: MonadThrow m
  102. => FilePath -> m (Path Rel File)
  103. parseRelFile filepath =
  104. if not (FilePath.isAbsolute filepath || FilePath.hasTrailingPathSeparator filepath) &&
  105. not (null filepath) &&
  106. not (isPrefixOf "~/" filepath) &&
  107. not (null (normalizeFile filepath))
  108. then return (Path (normalizeFile filepath))
  109. else throwM (InvalidRelFile filepath)
  110. --------------------------------------------------------------------------------
  111. -- Constructors
  112. -- | Make a 'Path Abs Dir'.
  113. --
  114. -- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
  115. -- may compile on your platform, but it may not compile on another
  116. -- platform (Windows).
  117. mkAbsDir :: FilePath -> Q Exp
  118. mkAbsDir s =
  119. case parseAbsDir s of
  120. Left err -> error (show err)
  121. Right (Path str) ->
  122. [|Path $(return (LitE (StringL str))) :: Path Abs Dir|]
  123. -- | Make a 'Path Rel Dir'.
  124. mkRelDir :: FilePath -> Q Exp
  125. mkRelDir s =
  126. case parseRelDir s of
  127. Left err -> error (show err)
  128. Right (Path str) ->
  129. [|Path $(return (LitE (StringL str))) :: Path Rel Dir|]
  130. -- | Make a 'Path Abs File'.
  131. --
  132. -- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)
  133. -- may compile on your platform, but it may not compile on another
  134. -- platform (Windows).
  135. mkAbsFile :: FilePath -> Q Exp
  136. mkAbsFile s =
  137. case parseAbsFile s of
  138. Left err -> error (show err)
  139. Right (Path str) ->
  140. [|Path $(return (LitE (StringL str))) :: Path Abs File|]
  141. -- | Make a 'Path Rel File'.
  142. mkRelFile :: FilePath -> Q Exp
  143. mkRelFile s =
  144. case parseRelFile s of
  145. Left err -> error (show err)
  146. Right (Path str) ->
  147. [|Path $(return (LitE (StringL str))) :: Path Rel File|]
  148. --------------------------------------------------------------------------------
  149. -- Conversion
  150. -- | Convert to a 'FilePath' type.
  151. toFilePath :: Path b t -> FilePath
  152. toFilePath (Path l) = l
  153. --------------------------------------------------------------------------------
  154. -- Internal functions
  155. -- | Internal use for normalizing a directory.
  156. normalizeDir :: FilePath -> FilePath
  157. normalizeDir =
  158. clean . FilePath.addTrailingPathSeparator . FilePath.normalise
  159. where clean "./" = ""
  160. clean ('/':'/':xs) = clean ('/':xs)
  161. clean x = x
  162. -- | Internal use for normalizing a fileectory.
  163. normalizeFile :: FilePath -> FilePath
  164. normalizeFile =
  165. clean . FilePath.normalise
  166. where clean "./" = ""
  167. clean ('/':'/':xs) = clean ('/':xs)
  168. clean x = x