packages feed

path 0.6.1 → 0.7.0

raw patch · 14 files changed

+1462/−938 lines, 14 filesdep +genvalidity-hspecdep +textdep ~basedep ~genvaliditydep ~genvalidity-property

Dependencies added: genvalidity-hspec, text

Dependency ranges changed: base, genvalidity, genvalidity-property, validity

Files

CHANGELOG view
@@ -1,3 +1,42 @@+0.7.0:+	* BREAKING CHANGE: "fileExtension" now throws an exception if the file has no+		extension. You can use the result as a "Maybe" in pure+		code or handle the exception appropriately in any other monad.+	* Old extension operations "addFileExtension" and "setFileExtension" have+		been deprecated and replaced by "addExtension" and "replaceExtension"+		respectively with new behavior.+		ADAPTING YOUR CODE TO THIS CHANGE:+		* Code that sets an extension not starting with a "." e.g.  "foo", must+		  be changed such that it starts with a "." i.e. ".foo".+		* Code that sets multiple extensions in one go e.g. ".tar.gz" must be+		  changed to set them one at a time instead i.e. add ".tar" first and+		  then add ".gz".+		* Code that sets an extension starting with multiple dots e.g.+		  "..foo" must be changed such as to make the extra dots part of the+		  file name instead.+		Details:+		The new operations "addExtension" and "replaceExtension" accept only "valid"+		extension forms which is exactly the same as what "fileExtension" returns.+		A valid extension starts with a @.@ followed by one or more characters+		not including @.@ followed by zero or more @.@s in trailing position.+		This change allows extension operations to be principled following+		these laws:+		* flip addExtension file >=> fileExtension == return+		* (fileExtension >=> flip replaceExtension file) file == return file+	* Add splitExtension operation such that:+		* uncurry addExtension . swap >=> splitExtension == return+		* splitExtension >=> uncurry addExtension . swap == return+		* fileExtension == (fmap snd) . splitExtension@+	* Add 'Path.Posix' and 'Path.Windows' modules for manipulating+		Windows or Posix style paths independently of the current platform.+	* Add 'Lift' instance for 'Path'.+	* `Path.Windows` normalizes path separators throughout path,+		including immediately following drive letter.+	* `Path.Windows` handles UNC (`\\host\share\`) and Unicode (`\\?\C:\`) path+		without breaking the double-separator prefix.+	* Remove support for old GHC version. The oldest supported version+	is 8.2.+ 0.6.1: 	* Add 'addFileExtension' function and its operator form: (<.>). 	* Derive 'Eq' instance for 'PathException'.
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2015–2016, FP Complete+Copyright (c) 2015–2018, FP Complete All rights reserved.  Redistribution and use in source and binary forms, with or without
README.md view
@@ -92,7 +92,7 @@  ```haskell newtype Path b t = Path FilePath-  deriving (Typeable)+  deriving (Data, Typeable, Generic) ```  The type variables are:@@ -269,7 +269,7 @@ Stripping the parent directory from a path:  ```haskell-stripDir :: MonadThrow m => Path b Dir -> Path b t -> m (Path Rel t)+stripProperPrefix :: MonadThrow m => Path b Dir -> Path b t -> m (Path Rel t) ```  ## Review@@ -401,7 +401,7 @@ ## Accepting user input  Sometimes you have user input that contains `../`. The solution we went with-is to have a function like `resolveDir`:+is to have a function like `resolveDir` (found in [`path-io`](http://hackage.haskell.org/package/path-io) package):  ```haskell resolveDir :: (MonadIO m, MonadThrow m)
path.cabal view
@@ -1,29 +1,49 @@ name:                path-version:             0.6.1+version:             0.7.0 synopsis:            Support for well-typed paths description:         Support for well-typed paths. license:             BSD3 license-file:        LICENSE author:              Chris Done <chrisdone@fpcomplete.com> maintainer:          Chris Done <chrisdone@fpcomplete.com>-copyright:           2015–2017 FP Complete+copyright:           2015–2018 FP Complete category:            System, Filesystem build-type:          Simple-cabal-version:       >=1.10-tested-with:         GHC==7.8.4, GHC==7.10.3, GHC==8.0.2, GHC==8.2.1-extra-source-files:  README.md, CHANGELOG+cabal-version:       1.18+tested-with:         GHC==8.2.2, GHC==8.4.4, GHC==8.6.5+extra-source-files:  README.md+                   , CHANGELOG+                   , src/Path/Include.hs +flag dev+  description:        Turn on development settings.+  manual:             True+  default:            False+ library   hs-source-dirs:    src-  ghc-options:       -Wall -O2-  exposed-modules:   Path, Path.Internal+  exposed-modules:   Path+                   , Path.Internal+                   , Path.Posix+                   , Path.Windows   build-depends:     aeson-                   , base       >= 4.7     && < 5+                   , base       >= 4.10     && < 5                    , deepseq-                   , exceptions >= 0.4     && < 0.9+                   , exceptions >= 0.4     && < 0.11                    , filepath   < 1.2.0.1  || >= 1.3                    , hashable   >= 1.2     && < 1.3+                   , text                    , template-haskell+  if flag(dev)+    ghc-options:      -Wall -Werror+  else+    ghc-options:      -O2 -Wall+  if flag(dev)+    ghc-options:      -Wcompat+                      -Wincomplete-record-updates+                      -Wincomplete-uni-patterns+                      -Wnoncanonical-monad-instances+                      -Wnoncanonical-monadfail-instances   default-language:  Haskell2010  test-suite test@@ -31,14 +51,19 @@   main-is:           Main.hs   other-modules:     Posix                    , Windows+                   , Common   hs-source-dirs:    test   build-depends:     aeson-                   , base       >= 4.7     && < 5+                   , base       >= 4.10     && < 5                    , bytestring                    , filepath   < 1.2.0.1  || >= 1.3                    , hspec      >= 2.0     && < 3                    , mtl        >= 2.0     && < 3                    , path+  if flag(dev)+    ghc-options:      -Wall -Werror+  else+    ghc-options:      -O2 -Wall   default-language:  Haskell2010  test-suite validity-test@@ -48,15 +73,16 @@   hs-source-dirs:    test   build-depends:     QuickCheck                    , aeson-                   , base       >= 4.7 && < 5+                   , base       >= 4.10 && < 5                    , bytestring                    , filepath   < 1.2.0.1  || >= 1.3-                   , genvalidity >= 0.3 && < 0.4-                   , genvalidity-property >= 0.0 && < 0.1+                   , genvalidity >= 0.8+                   , genvalidity-property >= 0.4+                   , genvalidity-hspec >= 0.7                    , hspec      >= 2.0     && < 3                    , mtl        >= 2.0     && < 3                    , path-                   , validity   >= 0.3.1.1 && < 0.4+                   , validity   >= 0.8.0.0   default-language:  Haskell2010   ghc-options: -threaded -rtsopts -with-rtsopts=-N 
src/Path.hs view
@@ -1,611 +1,15 @@ -- | This library provides a well-typed representation of paths in a filesystem--- directory tree.  A path is represented by a number of path components--- separated by a path separator which is a @/@ on POSIX systems and can be a--- @/@ or @\\@ on Windows.+-- directory tree. ----- The root of the tree is represented by a @/@ on POSIX and a drive letter--- followed by a @/@ or @\\@ on Windows (e.g. @C:\\@).  Paths can be absolute--- or relative. An absolute path always starts from the root of the tree (e.g.--- @\/x/y@) whereas a relative path never starts with the root (e.g. @x/y@).--- Just like we represent the notion of an absolute root by "@/@", the same way--- we represent the notion of a relative root by "@.@". The relative root denotes--- the directory which contains the first component of a relative path.+-- Both "Path.Posix" and "Path.Windows" provide the same interface. This+-- module will reexport the appropriate module for your platform.  {-# LANGUAGE CPP #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE PatternGuards #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE EmptyDataDecls #-}-{-# LANGUAGE FlexibleInstances #-} -module Path-  (-- * Types-   Path-  ,Abs-  ,Rel-  ,File-  ,Dir-   -- * Exceptions-  ,PathException(..)-  -- * QuasiQuoters-  -- | Using the following requires the QuasiQuotes language extension.-  ---  -- __For Windows users__, the QuasiQuoters are especially beneficial because they-  -- prevent Haskell from treating @\\@ as an escape character.-  -- This makes Windows paths easier to write.-  ---  -- @-  -- [absfile|C:\\chris\\foo.txt|]-  -- @-  ,absdir-  ,reldir-  ,absfile-  ,relfile-  -- * Operations-  ,(</>)-  ,stripProperPrefix-  ,isProperPrefixOf-  ,parent-  ,filename-  ,dirname-  ,fileExtension-  ,addFileExtension-  ,(<.>)-  ,setFileExtension-  ,(-<.>)-   -- * Parsing-  ,parseAbsDir-  ,parseRelDir-  ,parseAbsFile-  ,parseRelFile-  -- * Conversion-  ,toFilePath-  ,fromAbsDir-  ,fromRelDir-  ,fromAbsFile-  ,fromRelFile-  -- * TemplateHaskell constructors-  -- | These require the TemplateHaskell language extension.-  ,mkAbsDir-  ,mkRelDir-  ,mkAbsFile-  ,mkRelFile-  -- * Deprecated-  ,PathParseException-  ,stripDir-  ,isParentOf-  )-  where--import           Control.Exception (Exception)-import           Control.Monad-import           Control.Monad.Catch (MonadThrow(..))-import           Data.Aeson (FromJSON (..))-import qualified Data.Aeson.Types as Aeson-import           Data.Data-import           Data.List-import           Data.Maybe-import           Language.Haskell.TH-import           Language.Haskell.TH.Quote (QuasiQuoter(..))-import           Path.Internal-import qualified System.FilePath as FilePath------------------------------------------------------------------------------------- Types---- | An absolute path.-data Abs deriving (Typeable)---- | A relative path; one without a root. Note that a @..@ path component to--- represent the parent directory is not allowed by this library.-data Rel deriving (Typeable)---- | A file path.-data File deriving (Typeable)---- | A directory path.-data Dir deriving (Typeable)--instance FromJSON (Path Abs File) where-  parseJSON = parseJSONWith parseAbsFile-  {-# INLINE parseJSON #-}--instance FromJSON (Path Rel File) where-  parseJSON = parseJSONWith parseRelFile-  {-# INLINE parseJSON #-}--instance FromJSON (Path Abs Dir) where-  parseJSON = parseJSONWith parseAbsDir-  {-# INLINE parseJSON #-}--instance FromJSON (Path Rel Dir) where-  parseJSON = parseJSONWith parseRelDir-  {-# INLINE parseJSON #-}--parseJSONWith :: (Show e, FromJSON a)-              => (a -> Either e b) -> Aeson.Value -> Aeson.Parser b-parseJSONWith f x =-  do fp <- parseJSON x-     case f fp of-       Right p -> return p-       Left e -> fail (show e)-{-# INLINE parseJSONWith #-}---- | Exceptions that can occur during path operations.------ @since 0.6.0-data PathException-  = InvalidAbsDir FilePath-  | InvalidRelDir FilePath-  | InvalidAbsFile FilePath-  | InvalidRelFile FilePath-  | NotAProperPrefix FilePath FilePath-  deriving (Show,Eq,Typeable)-instance Exception PathException------------------------------------------------------------------------------------- QuasiQuoters--qq :: (String -> Q Exp) -> QuasiQuoter-qq quoteExp' =-  QuasiQuoter-  { quoteExp  = quoteExp'-  , quotePat  = \_ ->-      fail "illegal QuasiQuote (allowed as expression only, used as a pattern)"-  , quoteType = \_ ->-      fail "illegal QuasiQuote (allowed as expression only, used as a type)"-  , quoteDec  = \_ ->-      fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"-  }---- | Construct a 'Path' 'Abs' 'Dir' using QuasiQuotes.------ @--- [absdir|/|]------ [absdir|\/home\/chris|]--- @------ Remember: due to the nature of absolute paths a path like @[absdir|\/home\/chris|]@--- may compile on your platform, but it may not compile on another--- platform (Windows).------ @since 0.5.13-absdir :: QuasiQuoter-absdir = qq mkAbsDir---- | Construct a 'Path' 'Rel' 'Dir' using QuasiQuotes.------ @--- [absdir|\/home|]\<\/>[reldir|chris|]--- @------ @since 0.5.13-reldir :: QuasiQuoter-reldir = qq mkRelDir---- | Construct a 'Path' 'Abs' 'File' using QuasiQuotes.------ @--- [absfile|\/home\/chris\/foo.txt|]--- @------ Remember: due to the nature of absolute paths a path like @[absdir|\/home\/chris\/foo.txt|]@--- may compile on your platform, but it may not compile on another--- platform (Windows).------ @since 0.5.13-absfile :: QuasiQuoter-absfile = qq mkAbsFile---- | Construct a 'Path' 'Rel' 'File' using QuasiQuotes.------ @--- [absdir|\/home\/chris|]\<\/>[relfile|foo.txt|]--- @------ @since 0.5.13-relfile :: QuasiQuoter-relfile = qq mkRelFile------------------------------------------------------------------------------------- Operations---- | Append two paths.------ The following cases are valid and the equalities hold:------ @$(mkAbsDir x) \<\/> $(mkRelDir y) = $(mkAbsDir (x ++ \"/\" ++ y))@------ @$(mkAbsDir x) \<\/> $(mkRelFile y) = $(mkAbsFile (x ++ \"/\" ++ y))@------ @$(mkRelDir x) \<\/> $(mkRelDir y) = $(mkRelDir (x ++ \"/\" ++ y))@------ @$(mkRelDir x) \<\/> $(mkRelFile y) = $(mkRelFile (x ++ \"/\" ++ y))@------ The following are proven not possible to express:------ @$(mkAbsFile …) \<\/> x@------ @$(mkRelFile …) \<\/> x@------ @x \<\/> $(mkAbsFile …)@------ @x \<\/> $(mkAbsDir …)@----infixr 5 </>-(</>) :: Path b Dir -> Path Rel t -> Path b t-(</>) (Path a) (Path b) = Path (a ++ b)---- | If the directory in the first argument is a proper prefix of the path in--- the second argument strip it from the second argument, generating a path--- relative to the directory.--- Throws 'NotAProperPrefix' if the directory is not a proper prefix of the--- path.------ The following properties hold:------ @stripProperPrefix x (x \<\/> y) = y@------ Cases which are proven not possible:------ @stripProperPrefix (a :: Path Abs …) (b :: Path Rel …)@------ @stripProperPrefix (a :: Path Rel …) (b :: Path Abs …)@------ In other words the bases must match.------ @since 0.6.0-stripProperPrefix :: MonadThrow m-         => Path b Dir -> Path b t -> m (Path Rel t)-stripProperPrefix (Path p) (Path l) =-  case stripPrefix p l of-    Nothing -> throwM (NotAProperPrefix p l)-    Just "" -> throwM (NotAProperPrefix p l)-    Just ok -> return (Path ok)---- | Determines if the path in the first parameter is a proper prefix of the--- path in the second parameter.------ The following properties hold:------ @not (x \`isProperPrefixOf\` x)@------ @x \`isProperPrefixOf\` (x \<\/\> y)@------ @since 0.6.0-isProperPrefixOf :: Path b Dir -> Path b t -> Bool-isProperPrefixOf p l = isJust (stripProperPrefix p l)---- | Take the parent path component from a path.------ The following properties hold:------ @--- parent (x \<\/> y) == x--- parent \"\/x\" == \"\/\"--- parent \"x\" == \".\"--- @------ On the root (absolute or relative), getting the parent is idempotent:------ @--- parent \"\/\" = \"\/\"--- parent \"\.\" = \"\.\"--- @----parent :: Path b t -> Path b Dir-parent (Path "") = Path ""-parent (Path fp) | FilePath.isDrive fp = Path fp-parent (Path fp) =-    Path-    $ normalizeDir-    $ FilePath.takeDirectory-    $ FilePath.dropTrailingPathSeparator fp---- | Extract the file part of a path.------ The following properties hold:------ @filename (p \<\/> a) == filename a@----filename :: Path b File -> Path Rel File-filename (Path l) =-  Path (FilePath.takeFileName l)---- | Extract the last directory name of a path.------ The following properties hold:------ @dirname $(mkRelDir ".") == $(mkRelDir ".")@------ @dirname (p \<\/> a) == dirname a@----dirname :: Path b Dir -> Path Rel Dir-dirname (Path "") = Path ""-dirname (Path l) | FilePath.isDrive l = Path ""-dirname (Path l) = Path (last (FilePath.splitPath l))---- | Get extension from given file path.------ @since 0.5.11-fileExtension :: Path b File -> String-fileExtension = FilePath.takeExtension . toFilePath---- | Add extension to given file path. Throws if the--- resulting filename does not parse.------ >>> addFileExtension "txt $(mkRelFile "foo")--- "foo.txt"--- >>> addFileExtension "symbols" $(mkRelFile "Data.List")--- "Data.List.symbols"--- >>> addFileExtension ".symbols" $(mkRelFile "Data.List")--- "Data.List.symbols"--- >>> addFileExtension "symbols" $(mkRelFile "Data.List.")--- "Data.List..symbols"--- >>> addFileExtension ".symbols" $(mkRelFile "Data.List.")--- "Data.List..symbols"--- >>> addFileExtension "evil/" $(mkRelFile "Data.List")--- *** Exception: InvalidRelFile "Data.List.evil/"------ @since 0.6.1-addFileExtension :: MonadThrow m-  => String            -- ^ Extension to add-  -> Path b File       -- ^ Old file name-  -> m (Path b File)   -- ^ New file name with the desired extension added at the end-addFileExtension ext (Path path) =-  if FilePath.isAbsolute path-    then liftM coercePath (parseAbsFile (FilePath.addExtension path ext))-    else liftM coercePath (parseRelFile (FilePath.addExtension path ext))-  where coercePath :: Path a b -> Path a' b'-        coercePath (Path a) = Path a---- | A synonym for 'addFileExtension' in the form of an operator.--- See more examples there.------ >>> $(mkRelFile "Data.List") <.> "symbols"--- "Data.List.symbols"--- >>> $(mkRelFile "Data.List") <.> "evil/"--- *** Exception: InvalidRelFile "Data.List.evil/"------ @since 0.6.1-infixr 7 <.>-(<.>) :: MonadThrow m-  => Path b File       -- ^ Old file name-  -> String            -- ^ Extension to add-  -> m (Path b File)   -- ^ New file name with the desired extension added at the end-(<.>) = flip addFileExtension---- | Replace\/add extension to given file path. Throws if the--- resulting filename does not parse.------ @since 0.5.11-setFileExtension :: MonadThrow m-  => String            -- ^ Extension to set-  -> Path b File       -- ^ Old file name-  -> m (Path b File)   -- ^ New file name with the desired extension-setFileExtension ext (Path path) =-  if FilePath.isAbsolute path-    then liftM coercePath (parseAbsFile (FilePath.replaceExtension path ext))-    else liftM coercePath (parseRelFile (FilePath.replaceExtension path ext))-  where coercePath :: Path a b -> Path a' b'-        coercePath (Path a) = Path a---- | A synonym for 'setFileExtension' in the form of an operator.------ @since 0.6.0-infixr 7 -<.>-(-<.>) :: MonadThrow m-  => Path b File       -- ^ Old file name-  -> String            -- ^ Extension to set-  -> m (Path b File)   -- ^ New file name with the desired extension-(-<.>) = flip setFileExtension------------------------------------------------------------------------------------- Parsers---- | Convert an absolute 'FilePath' to a normalized absolute dir 'Path'.------ Throws: 'InvalidAbsDir' when the supplied path:------ * is not an absolute path--- * contains a @..@ path component representing the parent directory--- * is not a valid path (See 'System.FilePath.isValid')----parseAbsDir :: MonadThrow m-            => FilePath -> m (Path Abs Dir)-parseAbsDir filepath =-  if FilePath.isAbsolute filepath &&-     not (hasParentDir filepath) &&-     FilePath.isValid filepath-     then return (Path (normalizeDir filepath))-     else throwM (InvalidAbsDir filepath)---- | Convert a relative 'FilePath' to a normalized relative dir 'Path'.------ Throws: 'InvalidRelDir' when the supplied path:------ * is not a relative path--- * is @""@--- * contains a @..@ path component representing the parent directory--- * is not a valid path (See 'System.FilePath.isValid')----parseRelDir :: MonadThrow m-            => FilePath -> m (Path Rel Dir)-parseRelDir filepath =-  if not (FilePath.isAbsolute filepath) &&-     not (hasParentDir filepath) &&-     not (null filepath) &&-     FilePath.isValid filepath-     then return (Path (normalizeDir filepath))-     else throwM (InvalidRelDir filepath)---- | Convert an absolute 'FilePath' to a normalized absolute file 'Path'.------ Throws: 'InvalidAbsFile' when the supplied path:------ * is not an absolute path--- * is a directory path i.e.------     * has a trailing path separator---     * is @.@ or ends in @/.@------ * contains a @..@ path component representing the parent directory--- * is not a valid path (See 'System.FilePath.isValid')----parseAbsFile :: MonadThrow m-             => FilePath -> m (Path Abs File)-parseAbsFile filepath =-  case validAbsFile filepath of-    True-      | normalized <- normalizeFilePath filepath-      , validAbsFile normalized ->-        return (Path normalized)-    _ -> throwM (InvalidAbsFile filepath)---- | Is the string a valid absolute file?-validAbsFile :: FilePath -> Bool-validAbsFile filepath =-  FilePath.isAbsolute filepath &&-  not (FilePath.hasTrailingPathSeparator filepath) &&-  not (hasParentDir filepath) &&-  FilePath.isValid filepath---- | Convert a relative 'FilePath' to a normalized relative file 'Path'.------ Throws: 'InvalidRelFile' when the supplied path:------ * is not a relative path--- * is @""@--- * is a directory path i.e.------     * has a trailing path separator---     * is @.@ or ends in @/.@------ * contains a @..@ path component representing the parent directory--- * is not a valid path (See 'System.FilePath.isValid')----parseRelFile :: MonadThrow m-             => FilePath -> m (Path Rel File)-parseRelFile filepath =-  case validRelFile filepath of-    True-      | normalized <- normalizeFilePath filepath-      , validRelFile normalized -> return (Path normalized)-    _ -> throwM (InvalidRelFile filepath)---- | Is the string a valid relative file?-validRelFile :: FilePath -> Bool-validRelFile filepath =-  not-    (FilePath.isAbsolute filepath || FilePath.hasTrailingPathSeparator filepath) &&-  not (null filepath) &&-  not (hasParentDir filepath) &&-  filepath /= "." && FilePath.isValid filepath------------------------------------------------------------------------------------- Conversion---- | Convert absolute path to directory to 'FilePath' type.-fromAbsDir :: Path Abs Dir -> FilePath-fromAbsDir = toFilePath---- | Convert relative path to directory to 'FilePath' type.-fromRelDir :: Path Rel Dir -> FilePath-fromRelDir = toFilePath---- | Convert absolute path to file to 'FilePath' type.-fromAbsFile :: Path Abs File -> FilePath-fromAbsFile = toFilePath---- | Convert relative path to file to 'FilePath' type.-fromRelFile :: Path Rel File -> FilePath-fromRelFile = toFilePath------------------------------------------------------------------------------------- Constructors---- | Make a 'Path' 'Abs' Dir'.------ Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)--- may compile on your platform, but it may not compile on another--- platform (Windows).-mkAbsDir :: FilePath -> Q Exp-mkAbsDir s =-  case parseAbsDir s of-    Left err -> error (show err)-    Right (Path str) ->-      [|Path $(return (LitE (StringL str))) :: Path Abs Dir|]---- | Make a 'Path' 'Rel' 'Dir'.-mkRelDir :: FilePath -> Q Exp-mkRelDir s =-  case parseRelDir s of-    Left err -> error (show err)-    Right (Path str) ->-      [|Path $(return (LitE (StringL str))) :: Path Rel Dir|]---- | Make a 'Path' 'Abs' 'File'.------ Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)--- may compile on your platform, but it may not compile on another--- platform (Windows).-mkAbsFile :: FilePath -> Q Exp-mkAbsFile s =-  case parseAbsFile s of-    Left err -> error (show err)-    Right (Path str) ->-      [|Path $(return (LitE (StringL str))) :: Path Abs File|]---- | Make a 'Path' 'Rel' 'File'.-mkRelFile :: FilePath -> Q Exp-mkRelFile s =-  case parseRelFile s of-    Left err -> error (show err)-    Right (Path str) ->-      [|Path $(return (LitE (StringL str))) :: Path Rel File|]------------------------------------------------------------------------------------- Internal functions---- | Internal use for normalizing a directory.-normalizeDir :: FilePath -> FilePath-normalizeDir =-      normalizeRelDir-    . FilePath.addTrailingPathSeparator-    . normalizeFilePath-  where-      -- Represent a "." in relative dir path as "" internally so that it-      -- composes without having to renormalize the path.-      normalizeRelDir p | p == relRootFP = ""-      normalizeRelDir p = p--normalizeFilePath :: FilePath -> FilePath-#if defined(mingw32_HOST_OS) || defined(__MINGW32__) || MIN_VERSION_filepath(1,4,0)-normalizeFilePath = FilePath.normalise+#if defined(mingw32_HOST_OS)+module Path(module Path.Windows) where+import Path.Windows #else-normalizeFilePath = normalizeLeadingSeparators . FilePath.normalise-    where-        sep = FilePath.pathSeparator-        normalizeLeadingSeparators (x1:x2:xs) | x1 == sep && x2 == sep-            = normalizeLeadingSeparators (sep:xs)-        normalizeLeadingSeparators x = x+module Path(module Path.Posix) where+import Path.Posix #endif------------------------------------------------------------------------------------- Deprecated--{-# DEPRECATED PathParseException "Please use PathException instead." #-}--- | Same as 'PathException'.-type PathParseException = PathException--{-# DEPRECATED stripDir "Please use stripProperPrefix instead." #-}--- | Same as 'stripProperPrefix'.-stripDir :: MonadThrow m-         => Path b Dir -> Path b t -> m (Path Rel t)-stripDir = stripProperPrefix--{-# DEPRECATED isParentOf "Please use isProperPrefixOf instead." #-}--- | Same as 'isProperPrefixOf'.-isParentOf :: Path b Dir -> Path b t -> Bool-isParentOf = isProperPrefixOf
+ src/Path/Include.hs view
@@ -0,0 +1,814 @@+-- This template expects CPP definitions for:+--     PLATFORM_NAME = Posix | Windows+--     IS_WINDOWS    = False | True++-- | This library provides a well-typed representation of paths in a filesystem+-- directory tree.+--+-- __Note__: This module is for working with PLATFORM_NAME style paths. Importing+-- "Path" is usually better.+--+-- A path is represented by a number of path components separated by a path+-- separator which is a @/@ on POSIX systems and can be a @/@ or @\\@ on Windows.+-- The root of the tree is represented by a @/@ on POSIX and a drive letter+-- followed by a @/@ or @\\@ on Windows (e.g. @C:\\@).  Paths can be absolute+-- or relative. An absolute path always starts from the root of the tree (e.g.+-- @\/x/y@) whereas a relative path never starts with the root (e.g. @x/y@).+-- Just like we represent the notion of an absolute root by "@/@", the same way+-- we represent the notion of a relative root by "@.@". The relative root denotes+-- the directory which contains the first component of a relative path.++{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE EmptyDataDecls #-}+{-# LANGUAGE FlexibleInstances #-}++module Path.PLATFORM_NAME+  (-- * Types+   Path+  ,Abs+  ,Rel+  ,File+  ,Dir+   -- * Exceptions+  ,PathException(..)+  -- * QuasiQuoters+  -- | Using the following requires the QuasiQuotes language extension.+  --+  -- __For Windows users__, the QuasiQuoters are especially beneficial because they+  -- prevent Haskell from treating @\\@ as an escape character.+  -- This makes Windows paths easier to write.+  --+  -- @+  -- [absfile|C:\\chris\\foo.txt|]+  -- @+  ,absdir+  ,reldir+  ,absfile+  ,relfile+  -- * Operations+  ,(</>)+  ,stripProperPrefix+  ,isProperPrefixOf+  ,parent+  ,filename+  ,dirname+  ,addExtension+  ,splitExtension+  ,fileExtension+  ,replaceExtension+   -- * Parsing+  ,parseAbsDir+  ,parseRelDir+  ,parseAbsFile+  ,parseRelFile+  -- * Conversion+  ,toFilePath+  ,fromAbsDir+  ,fromRelDir+  ,fromAbsFile+  ,fromRelFile+  -- * TemplateHaskell constructors+  -- | These require the TemplateHaskell language extension.+  ,mkAbsDir+  ,mkRelDir+  ,mkAbsFile+  ,mkRelFile+  -- * Deprecated+  ,PathParseException+  ,stripDir+  ,isParentOf+  ,addFileExtension+  ,(<.>)+  ,setFileExtension+  ,(-<.>)+  )+  where++import           Control.Exception (Exception(..))+import           Control.Monad (liftM, when)+import           Control.Monad.Catch (MonadThrow(..))+import           Data.Aeson (FromJSON (..), FromJSONKey(..))+import qualified Data.Aeson.Types as Aeson+import           Data.Data+import qualified Data.Text as T+import           Data.List+import           Data.Maybe+import           Language.Haskell.TH+import           Language.Haskell.TH.Syntax (lift)+import           Language.Haskell.TH.Quote (QuasiQuoter(..))+import           Path.Internal+import qualified System.FilePath.PLATFORM_NAME as FilePath++--------------------------------------------------------------------------------+-- Types++-- | An absolute path.+data Abs deriving (Typeable)++-- | A relative path; one without a root. Note that a @..@ path component to+-- represent the parent directory is not allowed by this library.+data Rel deriving (Typeable)++-- | A file path.+data File deriving (Typeable)++-- | A directory path.+data Dir deriving (Typeable)++instance FromJSON (Path Abs File) where+  parseJSON = parseJSONWith parseAbsFile+  {-# INLINE parseJSON #-}++instance FromJSON (Path Rel File) where+  parseJSON = parseJSONWith parseRelFile+  {-# INLINE parseJSON #-}++instance FromJSON (Path Abs Dir) where+  parseJSON = parseJSONWith parseAbsDir+  {-# INLINE parseJSON #-}++instance FromJSON (Path Rel Dir) where+  parseJSON = parseJSONWith parseRelDir+  {-# INLINE parseJSON #-}++parseJSONWith :: (Show e, FromJSON a)+              => (a -> Either e b) -> Aeson.Value -> Aeson.Parser b+parseJSONWith f x =+  do fp <- parseJSON x+     case f fp of+       Right p -> return p+       Left e -> fail (show e)+{-# INLINE parseJSONWith #-}++instance FromJSONKey (Path Abs File) where+  fromJSONKey = fromJSONKeyWith parseAbsFile+  {-# INLINE fromJSONKey #-}++instance FromJSONKey (Path Rel File) where+  fromJSONKey = fromJSONKeyWith parseRelFile+  {-# INLINE fromJSONKey #-}++instance FromJSONKey (Path Abs Dir) where+  fromJSONKey = fromJSONKeyWith parseAbsDir+  {-# INLINE fromJSONKey #-}++instance FromJSONKey (Path Rel Dir) where+  fromJSONKey = fromJSONKeyWith parseRelDir+  {-# INLINE fromJSONKey #-}++fromJSONKeyWith :: (Show e)+                => (String -> Either e b) -> Aeson.FromJSONKeyFunction b+fromJSONKeyWith f =+  Aeson.FromJSONKeyTextParser $ \t ->+    case f (T.unpack t) of+      Left e -> fail (show e)+      Right rf -> pure rf++{-# INLINE fromJSONKeyWith #-}++-- | Exceptions that can occur during path operations.+--+-- @since 0.6.0+data PathException+  = InvalidAbsDir FilePath+  | InvalidRelDir FilePath+  | InvalidAbsFile FilePath+  | InvalidRelFile FilePath+  | NotAProperPrefix FilePath FilePath+  | HasNoExtension FilePath+  | InvalidExtension String+  deriving (Show,Eq,Typeable)++instance Exception PathException where+  displayException (InvalidExtension ext) = concat+    [ "Invalid extension ["+    , ext+    , "]. A valid extension starts with a '.' followed by one or more "+    , "characters other than '.', and it must be a valid filename, "+    , "notably it cannot include a path separator."+    ]+  displayException x = show x++--------------------------------------------------------------------------------+-- QuasiQuoters++qq :: (String -> Q Exp) -> QuasiQuoter+qq quoteExp' =+  QuasiQuoter+  { quoteExp  = quoteExp'+  , quotePat  = \_ ->+      fail "illegal QuasiQuote (allowed as expression only, used as a pattern)"+  , quoteType = \_ ->+      fail "illegal QuasiQuote (allowed as expression only, used as a type)"+  , quoteDec  = \_ ->+      fail "illegal QuasiQuote (allowed as expression only, used as a declaration)"+  }++-- | Construct a 'Path' 'Abs' 'Dir' using QuasiQuotes.+--+-- @+-- [absdir|/|]+--+-- [absdir|\/home\/chris|]+-- @+--+-- Remember: due to the nature of absolute paths a path like @[absdir|\/home\/chris|]@+-- may compile on your platform, but it may not compile on another+-- platform (Windows).+--+-- @since 0.5.13+absdir :: QuasiQuoter+absdir = qq mkAbsDir++-- | Construct a 'Path' 'Rel' 'Dir' using QuasiQuotes.+--+-- @+-- [absdir|\/home|]\<\/>[reldir|chris|]+-- @+--+-- @since 0.5.13+reldir :: QuasiQuoter+reldir = qq mkRelDir++-- | Construct a 'Path' 'Abs' 'File' using QuasiQuotes.+--+-- @+-- [absfile|\/home\/chris\/foo.txt|]+-- @+--+-- Remember: due to the nature of absolute paths a path like @[absdir|\/home\/chris\/foo.txt|]@+-- may compile on your platform, but it may not compile on another+-- platform (Windows).+--+-- @since 0.5.13+absfile :: QuasiQuoter+absfile = qq mkAbsFile++-- | Construct a 'Path' 'Rel' 'File' using QuasiQuotes.+--+-- @+-- [absdir|\/home\/chris|]\<\/>[relfile|foo.txt|]+-- @+--+-- @since 0.5.13+relfile :: QuasiQuoter+relfile = qq mkRelFile++--------------------------------------------------------------------------------+-- Operations++-- | Append two paths.+--+-- The following cases are valid and the equalities hold:+--+-- @$(mkAbsDir x) \<\/> $(mkRelDir y) = $(mkAbsDir (x ++ \"/\" ++ y))@+--+-- @$(mkAbsDir x) \<\/> $(mkRelFile y) = $(mkAbsFile (x ++ \"/\" ++ y))@+--+-- @$(mkRelDir x) \<\/> $(mkRelDir y) = $(mkRelDir (x ++ \"/\" ++ y))@+--+-- @$(mkRelDir x) \<\/> $(mkRelFile y) = $(mkRelFile (x ++ \"/\" ++ y))@+--+-- The following are proven not possible to express:+--+-- @$(mkAbsFile …) \<\/> x@+--+-- @$(mkRelFile …) \<\/> x@+--+-- @x \<\/> $(mkAbsFile …)@+--+-- @x \<\/> $(mkAbsDir …)@+--+infixr 5 </>+(</>) :: Path b Dir -> Path Rel t -> Path b t+(</>) (Path a) (Path b) = Path (a ++ b)++-- | If the directory in the first argument is a proper prefix of the path in+-- the second argument strip it from the second argument, generating a path+-- relative to the directory.+-- Throws 'NotAProperPrefix' if the directory is not a proper prefix of the+-- path.+--+-- The following properties hold:+--+-- @stripProperPrefix x (x \<\/> y) = y@+--+-- Cases which are proven not possible:+--+-- @stripProperPrefix (a :: Path Abs …) (b :: Path Rel …)@+--+-- @stripProperPrefix (a :: Path Rel …) (b :: Path Abs …)@+--+-- In other words the bases must match.+--+-- @since 0.6.0+stripProperPrefix :: MonadThrow m+         => Path b Dir -> Path b t -> m (Path Rel t)+stripProperPrefix (Path p) (Path l) =+  case stripPrefix p l of+    Nothing -> throwM (NotAProperPrefix p l)+    Just "" -> throwM (NotAProperPrefix p l)+    Just ok -> return (Path ok)++-- | Determines if the path in the first parameter is a proper prefix of the+-- path in the second parameter.+--+-- The following properties hold:+--+-- @not (x \`isProperPrefixOf\` x)@+--+-- @x \`isProperPrefixOf\` (x \<\/\> y)@+--+-- @since 0.6.0+isProperPrefixOf :: Path b Dir -> Path b t -> Bool+isProperPrefixOf p l = isJust (stripProperPrefix p l)++-- | Take the parent path component from a path.+--+-- The following properties hold:+--+-- @+-- parent (x \<\/> y) == x+-- parent \"\/x\" == \"\/\"+-- parent \"x\" == \".\"+-- @+--+-- On the root (absolute or relative), getting the parent is idempotent:+--+-- @+-- parent \"\/\" = \"\/\"+-- parent \"\.\" = \"\.\"+-- @+--+parent :: Path b t -> Path b Dir+parent (Path "") = Path ""+parent (Path fp) | FilePath.isDrive fp = Path fp+parent (Path fp) =+    Path+    $ normalizeDir+    $ FilePath.takeDirectory+    $ FilePath.dropTrailingPathSeparator fp++-- | Extract the file part of a path.+--+-- The following properties hold:+--+-- @filename (p \<\/> a) == filename a@+--+filename :: Path b File -> Path Rel File+filename (Path l) =+  Path (FilePath.takeFileName l)++-- | Extract the last directory name of a path.+--+-- The following properties hold:+--+-- @dirname $(mkRelDir ".") == $(mkRelDir ".")@+--+-- @dirname (p \<\/> a) == dirname a@+--+dirname :: Path b Dir -> Path Rel Dir+dirname (Path "") = Path ""+dirname (Path l) | FilePath.isDrive l = Path ""+dirname (Path l) = Path (last (FilePath.splitPath l))++-- | 'splitExtension' is the inverse of 'addExtension'. It splits the given+-- file path into a valid filename and a valid extension.+--+-- >>> splitExtension $(mkRelFile "name.foo"     ) == Just ($(mkRelFile "name"    ), ".foo"  )+-- >>> splitExtension $(mkRelFile "name.foo."    ) == Just ($(mkRelFile "name"    ), ".foo." )+-- >>> splitExtension $(mkRelFile "name.foo.."   ) == Just ($(mkRelFile "name"    ), ".foo..")+-- >>> splitExtension $(mkRelFile "name.bar.foo" ) == Just ($(mkRelFile "name.bar"), ".foo"  )+-- >>> splitExtension $(mkRelFile ".name.foo"    ) == Just ($(mkRelFile ".name"   ), ".foo"  )+-- >>> splitExtension $(mkRelFile "name..foo"    ) == Just ($(mkRelFile "name."   ), ".foo"  )+-- >>> splitExtension $(mkRelFile "....foo"      ) == Just ($(mkRelFile "..."     ), ".foo"  )+--+-- Throws 'HasNoExtension' exception if the filename does not have an extension+-- or in other words it cannot be split into a valid filename and a valid+-- extension. The following cases throw an exception, please note that "." and+-- ".." are not valid filenames:+--+-- >>> splitExtension $(mkRelFile "name"   )+-- >>> splitExtension $(mkRelFile "name."  )+-- >>> splitExtension $(mkRelFile "name.." )+-- >>> splitExtension $(mkRelFile ".name"  )+-- >>> splitExtension $(mkRelFile "..name" )+-- >>> splitExtension $(mkRelFile "...name")+--+-- 'splitExtension' and 'addExtension' are inverses of each other, the+-- following laws hold:+--+-- @+-- uncurry addExtension . swap >=> splitExtension == return+-- splitExtension >=> uncurry addExtension . swap == return+-- @+--+-- @since 0.7.0+splitExtension :: MonadThrow m => Path b File -> m (Path b File, String)+splitExtension (Path fpath) =+    if nameDot == [] || ext == []+    then throwM $ HasNoExtension fpath+    else let fname = init nameDot+         in if fname == [] || fname == "." || fname == ".."+            then throwM $ HasNoExtension fpath+            else return ( Path (normalizeDrive drv ++ dir ++ fname)+                        , FilePath.extSeparator : ext+                        )+    where++    -- trailing separators are ignored for the split and considered part of the+    -- second component in the split.+    splitLast isSep str =+        let rstr = reverse str+            notSep = not . isSep+            name = (dropWhile notSep . dropWhile isSep) rstr+            trailingSeps = takeWhile isSep rstr+            xtn  = (takeWhile notSep . dropWhile isSep) rstr+        in (reverse name, reverse xtn ++ trailingSeps)+    normalizeDrive+        | IS_WINDOWS = normalizeTrailingSeps+        | otherwise  = id++    (drv, pth)     = FilePath.splitDrive fpath+    (dir, file)    = splitLast FilePath.isPathSeparator pth+    (nameDot, ext) = splitLast FilePath.isExtSeparator file++-- | Get extension from given file path. Throws 'HasNoExtension' exception if+-- the file does not have an extension.  The following laws hold:+--+-- @+-- flip addExtension file >=> fileExtension == return+-- fileExtension == (fmap snd) . splitExtension+-- @+--+-- @since 0.5.11+fileExtension :: MonadThrow m => Path b File -> m String+fileExtension = (liftM snd) . splitExtension++-- | Add extension to given file path.+--+-- >>> addExtension ".foo"   $(mkRelFile "name"     ) == Just $(mkRelFile "name.foo"    )+-- >>> addExtension ".foo."  $(mkRelFile "name"     ) == Just $(mkRelFile "name.foo."   )+-- >>> addExtension ".foo.." $(mkRelFile "name"     ) == Just $(mkRelFile "name.foo.."  )+-- >>> addExtension ".foo"   $(mkRelFile "name.bar" ) == Just $(mkRelFile "name.bar.foo")+-- >>> addExtension ".foo"   $(mkRelFile ".name"    ) == Just $(mkRelFile ".name.foo"   )+-- >>> addExtension ".foo"   $(mkRelFile "name."    ) == Just $(mkRelFile "name..foo"   )+-- >>> addExtension ".foo"   $(mkRelFile "..."      ) == Just $(mkRelFile "....foo"     )+--+-- Throws an 'InvalidExtension' exception if the extension is not valid. A+-- valid extension starts with a @.@ followed by one or more characters not+-- including @.@ followed by zero or more @.@ in trailing position. Moreover,+-- an extension must be a valid filename, notably it cannot include path+-- separators. Particularly, @.foo.bar@ is an invalid extension, instead you+-- have to first set @.foo@ and then @.bar@ individually. Some examples of+-- invalid extensions are:+--+-- >>> addExtension "foo"      $(mkRelFile "name")+-- >>> addExtension "..foo"    $(mkRelFile "name")+-- >>> addExtension ".foo.bar" $(mkRelFile "name")+-- >>> addExtension ".foo/bar" $(mkRelFile "name")+--+-- @since 0.7.0+addExtension :: MonadThrow m+  => String            -- ^ Extension to add+  -> Path b File       -- ^ Old file name+  -> m (Path b File)   -- ^ New file name with the desired extension added at the end+addExtension ext (Path path) = do+    validateExtension ext+    return $ Path (path ++ ext)++    where++    validateExtension ex@(sep:xs) = do+        -- has to start with a "."+        when (not $ FilePath.isExtSeparator sep) $+            throwM $ InvalidExtension ex++        -- just a "." is not a valid extension+        when (xs == []) $+            throwM $ InvalidExtension ex++        -- cannot have path separators+        when (any FilePath.isPathSeparator xs) $+            throwM $ InvalidExtension ex++        -- All "."s is not a valid extension+        let ys = dropWhile FilePath.isExtSeparator (reverse xs)+        when (ys == []) $+            throwM $ InvalidExtension ex++        -- Cannot have "."s except in trailing position+        when (any FilePath.isExtSeparator ys) $+            throwM $ InvalidExtension ex++        -- must be valid as a filename+        _ <- parseRelFile ex+        return ()+    validateExtension ex = throwM $ InvalidExtension ex++-- | Add extension to given file path. Throws if the+-- resulting filename does not parse.+--+-- >>> addFileExtension "txt $(mkRelFile "foo")+-- "foo.txt"+-- >>> addFileExtension "symbols" $(mkRelFile "Data.List")+-- "Data.List.symbols"+-- >>> addFileExtension ".symbols" $(mkRelFile "Data.List")+-- "Data.List.symbols"+-- >>> addFileExtension "symbols" $(mkRelFile "Data.List.")+-- "Data.List..symbols"+-- >>> addFileExtension ".symbols" $(mkRelFile "Data.List.")+-- "Data.List..symbols"+-- >>> addFileExtension "evil/" $(mkRelFile "Data.List")+-- *** Exception: InvalidRelFile "Data.List.evil/"+--+-- @since 0.6.1+{-# DEPRECATED addFileExtension "Please use addExtension instead." #-}+addFileExtension :: MonadThrow m+  => String            -- ^ Extension to add+  -> Path b File       -- ^ Old file name+  -> m (Path b File)   -- ^ New file name with the desired extension added at the end+addFileExtension ext (Path path) =+  if FilePath.isAbsolute path+    then liftM coercePath (parseAbsFile (FilePath.addExtension path ext))+    else liftM coercePath (parseRelFile (FilePath.addExtension path ext))+  where coercePath :: Path a b -> Path a' b'+        coercePath (Path a) = Path a++-- | A synonym for 'addFileExtension' in the form of an infix operator.+-- See more examples there.+--+-- >>> $(mkRelFile "Data.List") <.> "symbols"+-- "Data.List.symbols"+-- >>> $(mkRelFile "Data.List") <.> "evil/"+-- *** Exception: InvalidRelFile "Data.List.evil/"+--+-- @since 0.6.1+infixr 7 <.>+{-# DEPRECATED (<.>) "Please use addExtension instead." #-}+(<.>) :: MonadThrow m+  => Path b File       -- ^ Old file name+  -> String            -- ^ Extension to add+  -> m (Path b File)   -- ^ New file name with the desired extension added at the end+(<.>) = flip addFileExtension++-- | If the file has an extension replace it with the given extension otherwise+-- add the new extension to it. Throws an 'InvalidExtension' exception if the+-- new extension is not a valid extension (see 'fileExtension' for validity+-- rules).+--+-- The following law holds:+--+-- @(fileExtension >=> flip replaceExtension file) file == return file@+--+-- @since 0.7.0+replaceExtension :: MonadThrow m+  => String            -- ^ Extension to set+  -> Path b File       -- ^ Old file name+  -> m (Path b File)   -- ^ New file name with the desired extension+replaceExtension ext path =+    addExtension ext (maybe path fst $ splitExtension path)++-- | Replace\/add extension to given file path. Throws if the+-- resulting filename does not parse.+--+-- @since 0.5.11+{-# DEPRECATED setFileExtension "Please use replaceExtension instead." #-}+setFileExtension :: MonadThrow m+  => String            -- ^ Extension to set+  -> Path b File       -- ^ Old file name+  -> m (Path b File)   -- ^ New file name with the desired extension+setFileExtension ext (Path path) =+  if FilePath.isAbsolute path+    then liftM coercePath (parseAbsFile (FilePath.replaceExtension path ext))+    else liftM coercePath (parseRelFile (FilePath.replaceExtension path ext))+  where coercePath :: Path a b -> Path a' b'+        coercePath (Path a) = Path a++-- | A synonym for 'setFileExtension' in the form of an operator.+--+-- @since 0.6.0+infixr 7 -<.>+{-# DEPRECATED (-<.>) "Please use replaceExtension instead." #-}+(-<.>) :: MonadThrow m+  => Path b File       -- ^ Old file name+  -> String            -- ^ Extension to set+  -> m (Path b File)   -- ^ New file name with the desired extension+(-<.>) = flip setFileExtension++--------------------------------------------------------------------------------+-- Parsers++-- | Convert an absolute 'FilePath' to a normalized absolute dir 'Path'.+--+-- Throws: 'InvalidAbsDir' when the supplied path:+--+-- * is not an absolute path+-- * contains a @..@ path component representing the parent directory+-- * is not a valid path (See 'FilePath.isValid')+--+parseAbsDir :: MonadThrow m+            => FilePath -> m (Path Abs Dir)+parseAbsDir filepath =+  if FilePath.isAbsolute filepath &&+     not (hasParentDir filepath) &&+     FilePath.isValid filepath+     then return (Path (normalizeDir filepath))+     else throwM (InvalidAbsDir filepath)++-- | Convert a relative 'FilePath' to a normalized relative dir 'Path'.+--+-- Throws: 'InvalidRelDir' when the supplied path:+--+-- * is not a relative path+-- * is @""@+-- * contains a @..@ path component representing the parent directory+-- * is not a valid path (See 'FilePath.isValid')+-- * is all path separators+--+parseRelDir :: MonadThrow m+            => FilePath -> m (Path Rel Dir)+parseRelDir filepath =+  if not (FilePath.isAbsolute filepath) &&+     not (hasParentDir filepath) &&+     not (null filepath) &&+     not (all FilePath.isPathSeparator filepath) &&+     FilePath.isValid filepath+     then return (Path (normalizeDir filepath))+     else throwM (InvalidRelDir filepath)++-- | Convert an absolute 'FilePath' to a normalized absolute file 'Path'.+--+-- Throws: 'InvalidAbsFile' when the supplied path:+--+-- * is not an absolute path+-- * is a directory path i.e.+--+--     * has a trailing path separator+--     * is @.@ or ends in @/.@+--+-- * contains a @..@ path component representing the parent directory+-- * is not a valid path (See 'FilePath.isValid')+--+parseAbsFile :: MonadThrow m+             => FilePath -> m (Path Abs File)+parseAbsFile filepath =+  case validAbsFile filepath of+    True+      | normalized <- normalizeFilePath filepath+      , validAbsFile normalized ->+        return (Path normalized)+    _ -> throwM (InvalidAbsFile filepath)++-- | Is the string a valid absolute file?+validAbsFile :: FilePath -> Bool+validAbsFile filepath =+  FilePath.isAbsolute filepath &&+  not (FilePath.hasTrailingPathSeparator filepath) &&+  not (hasParentDir filepath) &&+  FilePath.isValid filepath++-- | Convert a relative 'FilePath' to a normalized relative file 'Path'.+--+-- Throws: 'InvalidRelFile' when the supplied path:+--+-- * is not a relative path+-- * is @""@+-- * is a directory path i.e.+--+--     * has a trailing path separator+--     * is @.@ or ends in @/.@+--+-- * contains a @..@ path component representing the parent directory+-- * is not a valid path (See 'FilePath.isValid')+--+parseRelFile :: MonadThrow m+             => FilePath -> m (Path Rel File)+parseRelFile filepath =+  case validRelFile filepath of+    True+      | normalized <- normalizeFilePath filepath+      , validRelFile normalized -> return (Path normalized)+    _ -> throwM (InvalidRelFile filepath)++-- | Is the string a valid relative file?+validRelFile :: FilePath -> Bool+validRelFile filepath =+  not+    (FilePath.isAbsolute filepath || FilePath.hasTrailingPathSeparator filepath) &&+  not (null filepath) &&+  not (hasParentDir filepath) &&+  filepath /= "." && FilePath.isValid filepath++--------------------------------------------------------------------------------+-- Conversion++-- | Convert absolute path to directory to 'FilePath' type.+fromAbsDir :: Path Abs Dir -> FilePath+fromAbsDir = toFilePath++-- | Convert relative path to directory to 'FilePath' type.+fromRelDir :: Path Rel Dir -> FilePath+fromRelDir = toFilePath++-- | Convert absolute path to file to 'FilePath' type.+fromAbsFile :: Path Abs File -> FilePath+fromAbsFile = toFilePath++-- | Convert relative path to file to 'FilePath' type.+fromRelFile :: Path Rel File -> FilePath+fromRelFile = toFilePath++--------------------------------------------------------------------------------+-- Constructors++-- | Make a 'Path' 'Abs' 'Dir'.+--+-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)+-- may compile on your platform, but it may not compile on another+-- platform (Windows).+mkAbsDir :: FilePath -> Q Exp+mkAbsDir = either (error . show) lift . parseAbsDir++-- | Make a 'Path' 'Rel' 'Dir'.+mkRelDir :: FilePath -> Q Exp+mkRelDir = either (error . show) lift . parseRelDir++-- | Make a 'Path' 'Abs' 'File'.+--+-- Remember: due to the nature of absolute paths this (e.g. @\/home\/foo@)+-- may compile on your platform, but it may not compile on another+-- platform (Windows).+mkAbsFile :: FilePath -> Q Exp+mkAbsFile = either (error . show) lift . parseAbsFile++-- | Make a 'Path' 'Rel' 'File'.+mkRelFile :: FilePath -> Q Exp+mkRelFile = either (error . show) lift . parseRelFile++--------------------------------------------------------------------------------+-- Internal functions++-- | Normalizes directory path with platform-specific rules.+normalizeDir :: FilePath -> FilePath+normalizeDir =+      normalizeRelDir+    . FilePath.addTrailingPathSeparator+    . normalizeFilePath+  where -- Represent a "." in relative dir path as "" internally so that it+        -- composes without having to renormalize the path.+        normalizeRelDir p+          | p == relRootFP = ""+          | otherwise = p++-- | Replaces consecutive path seps with single sep and replaces alt sep with standard sep.+normalizeAllSeps :: FilePath -> FilePath+normalizeAllSeps = foldr normSeps []+  where normSeps ch [] = [ch]+        normSeps ch path@(p0:_)+          | FilePath.isPathSeparator ch && FilePath.isPathSeparator p0 = path+          | FilePath.isPathSeparator ch = FilePath.pathSeparator:path+          | otherwise = ch:path++-- | Normalizes seps in whole path, but if there are 2+ seps at the beginning,+--   they are normalized to exactly 2 to preserve UNC and Unicode prefixed paths.+normalizeWindowsSeps :: FilePath -> FilePath+normalizeWindowsSeps path = normLeadingSeps ++ normalizeAllSeps rest+  where (leadingSeps, rest) = span FilePath.isPathSeparator path+        normLeadingSeps = replicate (min 2 (length leadingSeps)) FilePath.pathSeparator++-- | Normalizes seps only at the beginning of a path.+normalizeLeadingSeps :: FilePath -> FilePath+normalizeLeadingSeps path = normLeadingSep ++ rest+  where (leadingSeps, rest) = span FilePath.isPathSeparator path+        normLeadingSep = replicate (min 1 (length leadingSeps)) FilePath.pathSeparator++-- | Normalizes seps only at the end of a path.+normalizeTrailingSeps :: FilePath -> FilePath+normalizeTrailingSeps = reverse . normalizeLeadingSeps . reverse++-- | Applies platform-specific sep normalization following @FilePath.normalise@.+normalizeFilePath :: FilePath -> FilePath+normalizeFilePath+  | IS_WINDOWS = normalizeWindowsSeps . FilePath.normalise+  | otherwise  = normalizeLeadingSeps . FilePath.normalise++--------------------------------------------------------------------------------+-- Deprecated++{-# DEPRECATED PathParseException "Please use PathException instead." #-}+-- | Same as 'PathException'.+type PathParseException = PathException++{-# DEPRECATED stripDir "Please use stripProperPrefix instead." #-}+-- | Same as 'stripProperPrefix'.+stripDir :: MonadThrow m+         => Path b Dir -> Path b t -> m (Path Rel t)+stripDir = stripProperPrefix++{-# DEPRECATED isParentOf "Please use isProperPrefixOf instead." #-}+-- | Same as 'isProperPrefixOf'.+isParentOf :: Path b Dir -> Path b t -> Bool+isParentOf = isProperPrefixOf
src/Path/Internal.hs view
@@ -1,5 +1,7 @@ {-# LANGUAGE CPP                #-}+{-# LANGUAGE DeriveGeneric      #-} {-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE TemplateHaskell    #-}  -- | Internal types and functions. @@ -12,10 +14,14 @@   where  import Control.DeepSeq (NFData (..))-import Data.Aeson (ToJSON (..))+import Data.Aeson (ToJSON (..), ToJSONKey(..))+import Data.Aeson.Types (toJSONKeyText)+import qualified Data.Text as T (pack)+import GHC.Generics (Generic) import Data.Data import Data.Hashable import Data.List+import Language.Haskell.TH.Syntax (Exp(..), Lift(..), Lit(..)) import qualified System.FilePath as FilePath  -- | Path of some base and type.@@ -33,7 +39,7 @@ -- All directories end in a trailing separator. There are no duplicate -- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc. newtype Path b t = Path FilePath-  deriving (Typeable)+  deriving (Data, Typeable, Generic)  -- | String equality. --@@ -83,6 +89,9 @@   {-# INLINE toEncoding #-} #endif +instance ToJSONKey (Path b t) where+  toJSONKey = toJSONKeyText $ T.pack . toFilePath+ instance Hashable (Path b t) where   -- A "." is represented as an empty string ("") internally. Hashing ""   -- results in a hash that is the same as the salt. To produce a more@@ -103,3 +112,6 @@         case FilePath.pathSeparator of             '/' -> filepath'             x   -> map (\y -> if x == y then '/' else y) filepath'++instance Lift (Path a b) where+  lift (Path str) = [|Path $(return (LitE (StringL str)))|]
+ src/Path/Posix.hs view
@@ -0,0 +1,4 @@+{-# LANGUAGE CPP #-}+#define PLATFORM_NAME   Posix+#define IS_WINDOWS      False+#include "Include.hs"
+ src/Path/Windows.hs view
@@ -0,0 +1,4 @@+{-# LANGUAGE CPP #-}+#define PLATFORM_NAME   Windows+#define IS_WINDOWS      True+#include "Include.hs"
+ test/Common.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE TemplateHaskell #-}++-- | Test functions that are common to Posix and Windows++module Common (extensionOperations) where++import Control.Monad+import Path+import System.FilePath (pathSeparator)+import Test.Hspec++validExtensionsSpec :: String -> Path b File -> Path b File -> Spec+validExtensionsSpec ext file fext = do+    let f = show $ toFilePath file+    let fx = show $ toFilePath fext++    it ("addExtension " ++ show ext ++ " " ++ f ++ " == " ++ fx) $+        addExtension ext file `shouldReturn` fext++    it ("fileExtension " ++ fx ++ " == " ++ ext) $+        fileExtension fext `shouldReturn` ext++    it ("replaceExtension " ++ show ext ++ " " ++ fx ++ " == " ++ fx) $+        replaceExtension ext fext `shouldReturn` fext++extensionOperations :: String -> Spec+extensionOperations rootDrive = do+    let ext = ".foo"+    let extensions = ext : [".foo.", ".foo.."]++    -- Only filenames and extensions+    forM_ extensions (\x ->+        forM_ filenames $ \f -> do+            let Just file = parseRelFile f+            let Just fext = parseRelFile (f ++ x)+            (validExtensionsSpec x file fext))++    -- Relative dir paths+    forM_ dirnames (\d -> do+        forM_ filenames (\f -> do+            let f1 = d ++ [pathSeparator] ++ f+            let Just file = parseRelFile f1+            let Just fext = parseRelFile (f1 ++ ext)+            validExtensionsSpec ext file fext))++    -- Absolute dir paths+    forM_ dirnames (\d -> do+        forM_ filenames (\f -> do+            let f1 = rootDrive ++ d ++ [pathSeparator] ++ f+            let Just file = parseAbsFile f1+            let Just fext = parseAbsFile (f1 ++ ext)+            validExtensionsSpec ext file fext))++    -- Invalid extensions+    forM_ invalidExtensions $ \x -> do+        it ("throws InvalidExtension when extension is [" ++ x ++ "]")  $+            addExtension x $(mkRelFile "name")+            `shouldThrow` (== InvalidExtension x)++    where++    filenames =+        [ "name"+        , "name."+        , "name.."+        , ".name"+        , "..name"+        , "name.name"+        , "name..name"+        , "..."+        ]+    dirnames = filenames ++ ["."]+    invalidExtensions =+        [ ""+        , "."+        , "x"+        , ".."+        , "..."+        , "xy"+        , "foo"+        , "foo."+        , "foo.."+        , "..foo"+        , "...foo"+        , ".foo.bar"+        , ".foo" ++ [pathSeparator] ++ "bar"+        ]
test/Path/Gen.hs view
@@ -1,76 +1,152 @@ {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TemplateHaskell #-}+ module Path.Gen where -import           Data.Functor-import           Prelude+import Data.Functor+import Prelude -import           Path-import           Path.Internal+import Path+import Path.Internal  import qualified System.FilePath as FilePath -import           Data.Maybe (mapMaybe)-import           Data.List (isInfixOf)-import           Data.Validity-import           Data.GenValidity--import           Test.QuickCheck+import Data.GenValidity+import Data.List (isInfixOf, isSuffixOf)+import Data.Maybe (isJust, mapMaybe)+import Data.Validity +import Test.QuickCheck +-- | An absolute path to a file is valid if:+--+-- * Its path is an absolute path+-- * Its path has no trailing path separators+-- * Its path is valid according to 'System.FilePath's definition.+-- * Its path does not end in '/.'+-- * Its path is not '.'+-- * Its path does not contain '..'.+-- * Parsing the path and rendering it again results in the same path. instance Validity (Path Abs File) where-  isValid p@(Path fp)-    =  FilePath.isAbsolute fp-    && not (FilePath.hasTrailingPathSeparator fp)-    && FilePath.isValid fp-    && not (hasParentDir fp)-    && (parseAbsFile fp == Just p)+  validate p@(Path fp) =+    mconcat+      [ declare "The path is absolute." $ FilePath.isAbsolute fp+      , declare "The path has no trailing path separator." $+        not (FilePath.hasTrailingPathSeparator fp)+      , declare "System.FilePath considers the path valid." $ FilePath.isValid fp+      , declare "The path does not end in /." $ not ("/." `isSuffixOf` fp)+      , declare "The path does not equal \".\"" $ fp /= "."+      , declare "The path does not a parent directory." $ not (hasParentDir fp)+      , declare "The path can be identically parsed as an absolute file path." $+        parseAbsFile fp == Just p+      ] +-- | A relative path to a file is valid if:+--+-- * Its path is a relative path+-- * Its path does not have a trailing path separator+-- * Its path is valid according to 'System.FilePath's definition.+-- * Its path is not '.'+-- * Its path is not empty+-- * Its path does not end in '/.'+-- * Its path is not '.'+-- * Its path does not contain '..'.+-- * Parsing the path and rendering it again results in the same path. instance Validity (Path Rel File) where-  isValid p@(Path fp)-    =  FilePath.isRelative fp-    && not (FilePath.hasTrailingPathSeparator fp)-    && FilePath.isValid fp-    && fp /= "."-    && not (hasParentDir fp)-    && (parseRelFile fp == Just p)+  validate p@(Path fp) =+    mconcat+      [ declare "The path is relative." $ FilePath.isRelative fp+      , declare "The path has no trailing path separator." $+        not (FilePath.hasTrailingPathSeparator fp)+      , declare "System.FilePath considers the path valid." $ FilePath.isValid fp+      , declare "The path does not equal \".\"" $ fp /= "."+      , declare "The path is not empty" $ not (null fp)+      , declare "The path does not end in /." $ not ("/." `isSuffixOf` fp)+      , declare "The path does not a parent directory." $ not (hasParentDir fp)+      , declare "The path can be identically parsed as a relative file path." $+        parseRelFile fp == Just p+      ] +-- | An absolute path to a directory is valid if:+--+-- * Its path is an absolute path+-- * Its path has a trailing path separator+-- * Its path is valid according to 'System.FilePath's definition.+-- * Its path does not contain '..'.+-- * Parsing the path and rendering it again results in the same path. instance Validity (Path Abs Dir) where-  isValid p@(Path fp)-    =  FilePath.isAbsolute fp-    && FilePath.hasTrailingPathSeparator fp-    && FilePath.isValid fp-    && not (hasParentDir fp)-    && (parseAbsDir fp == Just p)+  validate p@(Path fp) =+    mconcat+      [ declare "The path is absolute." $ FilePath.isAbsolute fp+      , declare "The path has a trailing path separator." $ FilePath.hasTrailingPathSeparator fp+      , declare "System.FilePath considers the path valid." $ FilePath.isValid fp+      , declare "The path does not a parent directory." $ not (hasParentDir fp)+      , declare "The path can be identically parsed as an absolute directory path." $+        parseAbsDir fp == Just p+      ] +-- | A relative path to a directory is valid if:+--+-- * Its path is a relative path+-- * Its path has a trailing path separator+-- * Its path is valid according to 'System.FilePath's definition.+-- * Its path does not contain '..'.+-- * Parsing the path and rendering it again results in the same path. instance Validity (Path Rel Dir) where-  isValid p =-    FilePath.isRelative fp-    && FilePath.hasTrailingPathSeparator fp-    && FilePath.isValid fp-    && not (hasParentDir fp)-    && (parseRelDir fp == Just p)-    where fp = toFilePath p+  validate (Path "") = valid+  validate p@(Path fp) =+    mconcat+      [ declare "The path is relative." $ FilePath.isRelative fp+      , declare "The path has a trailing path separator." $ FilePath.hasTrailingPathSeparator fp+      , declare "System.FilePath considers the path valid." $ FilePath.isValid fp+      , declare "The path is not empty." $ not (null fp)+      , declare "The path does not a parent directory." $ not (hasParentDir fp)+      , declare "The path can be identically parsed as a relative directory path." $+        parseRelDir fp == Just p+      ]  instance GenUnchecked (Path Abs File) where   genUnchecked = Path <$> genFilePath -instance GenValid (Path Abs File)+instance GenValid (Path Abs File) where+  shrinkValid = shrinkValidWith parseAbsFile  instance GenUnchecked (Path Rel File) where   genUnchecked = Path <$> genFilePath -instance GenValid (Path Rel File)+instance GenValid (Path Rel File) where+  shrinkValid = shrinkValidWith parseRelFile  instance GenUnchecked (Path Abs Dir) where   genUnchecked = Path <$> genFilePath -instance GenValid (Path Abs Dir)+instance GenValid (Path Abs Dir) where+  shrinkValid = shrinkValidWith parseAbsDir  instance GenUnchecked (Path Rel Dir) where   genUnchecked = Path <$> genFilePath -instance GenValid (Path Rel Dir)+instance GenValid (Path Rel Dir) where+  shrinkValid = shrinkValidWith parseRelDir +data Extension =+  Extension String+  deriving (Show)++instance Validity Extension where+  validate (Extension ext) =+    mconcat+      [ delve "Extension" ext+      , declare "It is possible to add the extension to \"./\"" $+        isJust $ addExtension ext $(mkRelFile "x")+      ]++instance GenUnchecked Extension where+  genUnchecked = Extension <$> genFilePath+  shrinkUnchecked (Extension e) = Extension <$> shrinkUnchecked e++instance GenValid Extension+ -- | Generates 'FilePath's with a high occurence of @'.'@, @'\/'@ and -- @'\\'@ characters. The resulting 'FilePath's are not guaranteed to -- be valid.@@ -78,20 +154,11 @@ genFilePath = listOf genPathyChar  genPathyChar :: Gen Char-genPathyChar = frequency [(2, arbitrary), (1, elements "./\\")]--shrinkValidAbsFile :: Path Abs File -> [Path Abs File]-shrinkValidAbsFile = shrinkValidWith parseAbsFile--shrinkValidAbsDir :: Path Abs Dir -> [Path Abs Dir]-shrinkValidAbsDir = shrinkValidWith parseAbsDir--shrinkValidRelFile :: Path Rel File -> [Path Rel File]-shrinkValidRelFile = shrinkValidWith parseRelFile--shrinkValidRelDir :: Path Rel Dir -> [Path Rel Dir]-shrinkValidRelDir (Path []) = []-shrinkValidRelDir p = shrinkValidWith parseRelDir p+genPathyChar = frequency [(2, choose (minBound, maxBound)), (1, elements "./\\")]  shrinkValidWith :: (FilePath -> Maybe (Path a b)) -> Path a b -> [Path a b]-shrinkValidWith fun (Path s) = mapMaybe fun $ shrink s+shrinkValidWith fun (Path f) = filter (/= (Path f)) . mapMaybe fun $ shrinkUnchecked f++shrinkValidExtension :: Extension -> [Extension]+shrinkValidExtension (Extension s) =+  map (Extension . drop 1 . toFilePath) $ mapMaybe (flip addExtension $(mkRelFile "x")) (shrink s)
test/Posix.hs view
@@ -12,10 +12,12 @@ import Data.Aeson import qualified Data.ByteString.Lazy.Char8 as LBS import Data.Maybe-import Path+import Path.Posix import Path.Internal import Test.Hspec +import Common (extensionOperations)+ -- | Test suite (Posix version). spec :: Spec spec =@@ -30,8 +32,7 @@      describe "Operations: parent" operationParent      describe "Operations: filename" operationFilename      describe "Operations: dirname" operationDirname-     describe "Operations: addFileExtension" operationAddFileExtension-     describe "Operations: setFileExtension" operationSetFileExtension+     describe "Operations: extensions" (extensionOperations "/")      describe "Restrictions" restrictions      describe "Aeson Instances" aesonInstances      describe "QuasiQuotes" quasiquotes@@ -179,45 +180,6 @@         (toFilePath $(mkRelDir ".") == "./")      it "show $(mkRelDir \".\") == \"\\\"./\\\"\""         (show $(mkRelDir ".") == "\"./\"")--operationAddFileExtension :: Spec-operationAddFileExtension = do-  it "adds extension if there is none" $-    addFileExtension "ext" $(mkAbsFile "/directory/path")-      `shouldReturn` $(mkAbsFile "/directory/path.ext")-  it "adds extension if there is already one" $-    addFileExtension "baz" $(mkRelFile "foo.bar")-      `shouldReturn` $(mkRelFile "foo.bar.baz")-  it "adds extension with dot" $-    addFileExtension ".bar" $(mkRelFile "foo")-      `shouldReturn` $(mkRelFile "foo.bar")-  it "adds extension with dot after dot" $-    $(mkRelFile "foo.") <.> ".bar"-      `shouldReturn` $(mkRelFile "foo..bar")-  it "adds extension without dot after dot" $-    $(mkRelFile "foo.") <.> "bar"-      `shouldReturn` $(mkRelFile "foo..bar")-  it "adds extension with separator" $  -- I'm not sure it's okay-    $(mkRelFile "foo") <.> "evil/extension"-      `shouldReturn` $(mkRelFile "foo.evil/extension")-  it "adds extension to file inside dotted directory" $-    $(mkRelFile "foo.bar/baz") <.> "txt"-      `shouldReturn` $(mkRelFile "foo.bar/baz.txt")-  it "throws InvalidRelFile extension if extenstion ends with /" $-    $(mkRelFile "foo") <.> "evil/"-      `shouldThrow` (== InvalidRelFile "foo.evil/")-  it "throws InvalidAbsFile extension if extenstion ends with /" $-    $(mkAbsFile "/home/cfg") <.> "txt/" -- No Eq instance for PathException-      `shouldThrow` (== InvalidAbsFile "/home/cfg.txt/")--operationSetFileExtension :: Spec-operationSetFileExtension = do-  it "adds extension if there is none" $-    setFileExtension "txt" $(mkRelFile "foo")-      `shouldReturn` $(mkRelFile "foo.txt")-  it "replaces extension if the input path already has one" $-    setFileExtension "txt" $(mkRelFile "foo.bar")-      `shouldReturn` $(mkRelFile "foo.txt")  -- | Tests for the tokenizer. parseAbsDirSpec :: Spec
test/ValidityTest.hs view
@@ -1,8 +1,9 @@-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}  -- | Test suite.- module Main where  import Control.Applicative@@ -13,8 +14,9 @@ import Path import Path.Internal import Test.Hspec+import Test.Hspec.QuickCheck import Test.QuickCheck-import Test.Validity.Property+import Test.Validity  import Path.Gen @@ -24,121 +26,195 @@  -- | Test suite. spec :: Spec-spec = parallel $ do-     describe "Parsing: Path Abs Dir" (parserSpec parseAbsDir)-     describe "Parsing: Path Rel Dir" (parserSpec parseRelDir)-     describe "Parsing: Path Abs File" (parserSpec parseAbsFile)-     describe "Parsing: Path Rel File" (parserSpec parseRelFile)-     describe "Operations: (</>)" operationAppend-     describe "Operations: stripProperPrefix" operationStripDir-     describe "Operations: isProperPrefixOf" operationIsParentOf-     describe "Operations: parent" operationParent-     describe "Operations: filename" operationFilename+spec =+  modifyMaxShrinks (const 100) $+  parallel $ do+    genValidSpec @(Path Abs File)+    shrinkValidSpec @(Path Abs File)+    genValidSpec @(Path Rel File)+    shrinkValidSpec @(Path Rel File)+    genValidSpec @(Path Abs Dir)+    shrinkValidSpec @(Path Abs Dir)+    genValidSpec @(Path Rel Dir)+    shrinkValidSpec @(Path Rel Dir)+    describe "Parsing" $ do+      describe "Path Abs Dir" (parserSpec parseAbsDir)+      describe "Path Rel Dir" (parserSpec parseRelDir)+      describe "Path Abs File" (parserSpec parseAbsFile)+      describe "Path Rel File" (parserSpec parseRelFile)+    describe "Operations" $ do+      describe "(</>)" operationAppend+      describe "stripProperPrefix" operationStripDir+      describe "isProperPrefixOf" operationIsParentOf+      describe "parent" operationParent+      describe "filename" operationFilename+      describe "dirname" operationDirname+    describe "Extensions" extensionsSpec  -- | The 'filename' operation. operationFilename :: Spec operationFilename = do-     it "filename ($(mkAbsDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename)" $-         forAllShrink genValid shrinkValidAbsDir $ \parent ->-             forAllShrink genValid shrinkValidRelFile $ \file ->-                 filename (parent </> file) `shouldBe` filename file--     it "filename ($(mkRelDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename)" $-         forAllShrink genValid shrinkValidRelDir $ \parent ->-             forAllShrink genValid shrinkValidRelFile $ \file ->-                 filename (parent </> file) `shouldBe` filename file--     it "produces a valid path on when passed a valid absolute path" $ do-        producesValidsOnValids (filename :: Path Abs File -> Path Rel File)+  forAllDirs "filename parent </> $(mkRelFile filename)) == filename $(mkRelFile filename)" $ \parent ->+    forAllValid $ \file -> filename (parent </> file) `shouldBe` filename file+  it "produces a valid path on when passed a valid absolute path" $ do+    producesValidsOnValids (filename :: Path Abs File -> Path Rel File)+  it "produces a valid path on when passed a valid relative path" $ do+    producesValidsOnValids (filename :: Path Rel File -> Path Rel File) -     it "produces a valid path on when passed a valid relative path" $ do-        producesValidsOnValids (filename :: Path Rel File -> Path Rel File)+-- | The 'dirname' operation.+operationDirname :: Spec+operationDirname = do+  forAllDirs "dirname parent </> $(mkRelDir dirname)) == dirname $(mkRelDir dirname)" $ \parent ->+    forAllValid $ \dir -> if dir == Path [] then pure () else dirname (parent </> dir) `shouldBe` dirname dir+  it "produces a valid path on when passed a valid absolute path" $ do+    producesValidsOnValids (dirname :: Path Abs Dir -> Path Rel Dir)+  it "produces a valid path on when passed a valid relative path" $ do+    producesValidsOnValids (dirname :: Path Rel Dir -> Path Rel Dir)  -- | The 'parent' operation. operationParent :: Spec operationParent = do-     it "produces a valid path on when passed a valid file path" $ do-        producesValidsOnValids (parent :: Path Abs File -> Path Abs Dir)--     it "produces a valid path on when passed a valid directory path" $ do-        producesValidsOnValids (parent :: Path Abs Dir -> Path Abs Dir)+  it "produces a valid path on when passed a valid file path" $ do+    producesValidsOnValids (parent :: Path Abs File -> Path Abs Dir)+  it "produces a valid path on when passed a valid directory path" $ do+    producesValidsOnValids (parent :: Path Abs Dir -> Path Abs Dir)  -- | The 'isProperPrefixOf' operation. operationIsParentOf :: Spec operationIsParentOf = do-     it "isProperPrefixOf parent (parent </> child)" $-        forAllShrink genValid shrinkValidAbsDir $ \parent ->-            forAllShrink genValid shrinkValidRelFile $ \child ->-                isProperPrefixOf parent (parent </> child)--     it "isProperPrefixOf parent (parent </> child)" $-        forAllShrink genValid shrinkValidAbsDir $ \parent ->-            forAllShrink genValid shrinkValidRelDir $ \child ->-                child == Path [] || isProperPrefixOf parent (parent </> child)--     it "isProperPrefixOf parent (parent </> child)" $-        forAllShrink genValid shrinkValidRelDir $ \parent ->-            forAllShrink genValid shrinkValidRelFile $ \child ->-                isProperPrefixOf parent (parent </> child)--     it "isProperPrefixOf parent (parent </> child)" $-        forAllShrink genValid shrinkValidRelDir $ \parent ->-            forAllShrink genValid shrinkValidRelDir $ \child ->-                child == Path [] || isProperPrefixOf parent (parent </> child)+  forAllParentsAndChildren "isProperPrefixOf parent (parent </> child)" $ \parent child ->+    if child == Path []+      then True -- TODO do we always need this condition?+      else isProperPrefixOf parent (parent </> child)  -- | The 'stripProperPrefix' operation. operationStripDir :: Spec operationStripDir = do-     it "stripProperPrefix parent (parent </> child) = child" $-        forAllShrink genValid shrinkValidAbsDir $ \parent ->-            forAllShrink genValid shrinkValidRelFile $ \child ->-                stripProperPrefix parent (parent </> child) == Just child--     it "stripProperPrefix parent (parent </> child) = child" $-        forAllShrink genValid shrinkValidRelDir $ \parent ->-            forAllShrink genValid shrinkValidRelFile $ \child ->-                stripProperPrefix parent (parent </> child) == Just child--     it "stripProperPrefix parent (parent </> child) = child" $-        forAllShrink genValid shrinkValidAbsDir $ \parent ->-            forAllShrink genValid shrinkValidRelDir $ \child ->-                child == Path []-                || stripProperPrefix parent (parent </> child) == Just child--     it "stripProperPrefix parent (parent </> child) = child" $-        forAllShrink genValid shrinkValidRelDir $ \parent ->-            forAllShrink genValid shrinkValidRelDir $ \child ->-                child == Path []-                || stripProperPrefix parent (parent </> child) == Just child--     it "produces a valid path on when passed a valid absolute file paths" $ do-        producesValidsOnValids2 (stripProperPrefix :: Path Abs Dir -> Path Abs File -> Maybe (Path Rel File))--     it "produces a valid path on when passed a valid absolute directory paths" $ do-        producesValidsOnValids2 (stripProperPrefix :: Path Abs Dir -> Path Abs Dir -> Maybe (Path Rel Dir))--     it "produces a valid path on when passed a valid relative file paths" $ do-        producesValidsOnValids2 (stripProperPrefix :: Path Rel Dir -> Path Rel File-> Maybe (Path Rel File))--     it "produces a valid path on when passed a valid relative directory paths" $ do-        producesValidsOnValids2 (stripProperPrefix :: Path Rel Dir -> Path Rel Dir -> Maybe (Path Rel Dir))+  forAllParentsAndChildren "stripProperPrefix parent (parent </> child) = child" $ \parent child ->+    if child == Path []+      then pure () -- TODO do we always need this condition?+      else stripProperPrefix parent (parent </> child) `shouldBe` Just child+  it "produces a valid path on when passed a valid absolute file paths" $ do+    producesValidsOnValids2+      (stripProperPrefix :: Path Abs Dir -> Path Abs File -> Maybe (Path Rel File))+  it "produces a valid path on when passed a valid absolute directory paths" $ do+    producesValidsOnValids2+      (stripProperPrefix :: Path Abs Dir -> Path Abs Dir -> Maybe (Path Rel Dir))+  it "produces a valid path on when passed a valid relative file paths" $ do+    producesValidsOnValids2+      (stripProperPrefix :: Path Rel Dir -> Path Rel File -> Maybe (Path Rel File))+  it "produces a valid path on when passed a valid relative directory paths" $ do+    producesValidsOnValids2+      (stripProperPrefix :: Path Rel Dir -> Path Rel Dir -> Maybe (Path Rel Dir))  -- | The '</>' operation. operationAppend :: Spec operationAppend = do-     it "produces a valid path on when creating valid absolute file paths" $ do-        producesValidsOnValids2 ((</>) :: Path Abs Dir -> Path Rel File -> Path Abs File)+  it "produces a valid path on when creating valid absolute file paths" $ do+    producesValidsOnValids2 ((</>) :: Path Abs Dir -> Path Rel File -> Path Abs File)+  it "produces a valid path on when creating valid absolute directory paths" $ do+    producesValidsOnValids2 ((</>) :: Path Abs Dir -> Path Rel Dir -> Path Abs Dir)+  it "produces a valid path on when creating valid relative file paths" $ do+    producesValidsOnValids2 ((</>) :: Path Rel Dir -> Path Rel File -> Path Rel File)+  it "produces a valid path on when creating valid relative directory paths" $ do+    producesValidsOnValids2 ((</>) :: Path Rel Dir -> Path Rel Dir -> Path Rel Dir) -     it "produces a valid path on when creating valid absolute directory paths" $ do-        producesValidsOnValids2 ((</>) :: Path Abs Dir -> Path Rel Dir -> Path Abs Dir)+extensionsSpec :: Spec+extensionsSpec = do+  it "if addExtension a b succeeds then parseRelFile b succeeds - 1" $+    forAll genFilePath addExtGensValidFile+     -- skew the generated path towards a valid extension by prefixing a "."+  it "if addExtension a b succeeds then parseRelFile b succeeds - 2" $+    forAll genFilePath $ addExtGensValidFile . ("." ++)+  forAllFiles+    "(toFilePath . fromJust . addExtension ext) file \+        \== toFilePath a ++ b" $ \file ->+    forAllValid $ \(Extension ext) ->+      (toFilePath . fromJust . addExtension ext) file `shouldBe` toFilePath file ++ ext+  forAllFiles "splitExtension output joins to result in the original file" $ \file ->+    case splitExtension file of+      Nothing -> pure ()+      Just (f, ext) -> toFilePath f ++ ext `shouldBe` toFilePath file+  forAllFiles "splitExtension generates a valid filename and valid extension" $ \file ->+    case splitExtension file of+      Nothing -> True+      Just (f, ext) ->+        case parseRelFile ext of+          Nothing -> False+          Just _ ->+            case parseRelFile (toFilePath f) of+              Nothing ->+                case parseAbsFile (toFilePath f) of+                  Nothing -> False+                  Just _ -> True+              Just _ -> True+  forAllFiles "splitExtension >=> uncurry addExtension . swap == return" $ \file ->+    case splitExtension file of+      Nothing -> pure ()+      Just (f, ext) -> addExtension ext f `shouldBe` Just file+  forAllFiles "uncurry addExtension . swap >=> splitExtension == return" $ \file ->+    forAllValid $ \(Extension ext) ->+      (addExtension ext file >>= splitExtension) `shouldReturn` (file, ext)+  forAllFiles "fileExtension == (fmap snd) . splitExtension" $ \file ->+    case splitExtension file of+      Nothing -> pure ()+      Just (_, ext) -> fileExtension file `shouldBe` Just ext+  forAllFiles "flip addExtension file >=> fileExtension == return" $ \file ->+    forAllValid $ \(Extension ext) ->+      (fileExtension . fromJust . addExtension ext) file `shouldReturn` ext+  forAllFiles "(fileExtension >=> flip replaceExtension file) file == return file" $ \file ->+    case fileExtension file of+      Nothing -> pure ()+      Just ext -> replaceExtension ext file `shouldBe` Just file+  where+    addExtGensValidFile p =+      case addExtension p $(mkRelFile "x") of+        Nothing -> True+        Just x ->+          case parseRelFile p of+            Nothing -> False+            _ -> True -     it "produces a valid path on when creating valid relative file paths" $ do-        producesValidsOnValids2 ((</>) :: Path Rel Dir -> Path Rel File -> Path Rel File)+forAllFiles :: Testable a => String -> (forall b. Path b File -> a) -> Spec+forAllFiles n func = do+  it (unwords [n, "Path Abs File"]) $ forAllValid $ \(file :: Path Abs File) -> func file+  it (unwords [n, "Path Rel File"]) $ forAllValid $ \(file :: Path Rel File) -> func file -     it "produces a valid path on when creating valid relative directory paths" $ do-        producesValidsOnValids2 ((</>) :: Path Rel Dir -> Path Rel Dir -> Path Rel Dir)+forAllDirs :: Testable a => String -> (forall b. Path b Dir -> a) -> Spec+forAllDirs n func = do+  it (unwords [n, "Path Abs Dir"]) $ forAllValid $ \(parent :: Path Abs Dir) -> func parent+  it (unwords [n, "Path Rel Dir"]) $ forAllValid $ \(parent :: Path Rel Dir) -> func parent +forAllParentsAndChildren ::+     Testable a => String -> (forall b t. Path b Dir -> Path Rel t -> a) -> Spec+forAllParentsAndChildren n func = do+  it (unwords [n, "Path Abs Dir", "Path Rel Dir"]) $+    forAllValid $ \(parent :: Path Abs Dir) ->+      forAllValid $ \(child :: Path Rel Dir) -> func parent child+  it (unwords [n, "Path Rel Dir", "Path Rel Dir"]) $+    forAllValid $ \(parent :: Path Rel Dir) ->+      forAllValid $ \(child :: Path Rel Dir) -> func parent child+  it (unwords [n, "Path Abs Dir", "Path Rel File"]) $+    forAllValid $ \(parent :: Path Abs Dir) ->+      forAllValid $ \(child :: Path Rel File) -> func parent child+  it (unwords [n, "Path Rel Dir", "Path Rel File"]) $+    forAllValid $ \(parent :: Path Rel Dir) ->+      forAllValid $ \(child :: Path Rel File) -> func parent child++forAllPaths :: Testable a => String -> (forall b t. Path b t -> a) -> Spec+forAllPaths n func = do+  it (unwords [n, "Path Abs Dir"]) $ forAllValid $ \(path :: Path Abs Dir) -> func path+  it (unwords [n, "Path Rel Dir"]) $ forAllValid $ \(path :: Path Rel Dir) -> func path+  it (unwords [n, "Path Abs File"]) $ forAllValid $ \(path :: Path Abs File) -> func path+  it (unwords [n, "Path Rel File"]) $ forAllValid $ \(path :: Path Rel File) -> func path+ parserSpec :: (Show p, Validity p) => (FilePath -> Maybe p) -> Spec parserSpec parser =-     it "Produces valid paths when it succeeds" $-       validIfSucceedsOnGen parser genFilePath+  it "Produces valid paths when it succeeds" $+  forAllShrink genFilePath shrinkUnchecked $ \path ->+    case parser path of+      Nothing -> pure ()+      Just p ->+        case prettyValidate p of+          Left err -> expectationFailure err+          Right _ -> pure ()
test/Windows.hs view
@@ -11,11 +11,14 @@ import Control.Monad import Data.Aeson import qualified Data.ByteString.Lazy.Char8 as LBS+import Data.Function (on) import Data.Maybe-import Path+import Path.Windows import Path.Internal import Test.Hspec +import Common (extensionOperations)+ -- | Test suite (Windows version). spec :: Spec spec =@@ -30,8 +33,7 @@      describe "Operations: parent" operationParent      describe "Operations: filename" operationFilename      describe "Operations: dirname" operationDirname-     describe "Operations: addFileExtension" operationAddFileExtension-     describe "Operations: setFileExtension" operationSetFileExtension+     describe "Operations: extensions" (extensionOperations "C:\\")      describe "Restrictions" restrictions      describe "Aeson Instances" aesonInstances      describe "QuasiQuotes" quasiquotes@@ -57,43 +59,57 @@  -- | The 'dirname' operation. operationDirname :: Spec-operationDirname = do-  it-    "dirname ($(mkAbsDir parent) </> $(mkRelFile dirname)) == dirname $(mkRelFile dirname) (unit test)"-    (dirname ($(mkAbsDir "C:\\chris\\") </> $(mkRelDir "bar")) ==-     dirname $(mkRelDir "bar"))-  it-    "dirname ($(mkRelDir parent) </> $(mkRelFile dirname)) == dirname $(mkRelFile dirname) (unit test)"-    (dirname ($(mkRelDir "home\\chris\\") </> $(mkRelDir "bar")) ==-     dirname $(mkRelDir "bar"))-  it-    "dirname $(mkRelDir .) == $(mkRelDir .)"-    (dirname $(mkRelDir ".") == $(mkRelDir "."))-  it-    "dirname C:\\ must be a Rel path"-    ((parseAbsDir $ show $ dirname (fromJust (parseAbsDir "C:\\"))-     :: Maybe (Path Abs Dir)) == Nothing)+operationDirname =+  do it "dirname ($(mkAbsDir parent) </> $(mkRelFile dirname)) == dirname $(mkRelFile dirname) (absolute)"+        (dirnamesShouldBeEqual+          ($(mkAbsDir "C:\\chris\\") </> $(mkRelDir "bar"))+          $(mkRelDir "bar"))+     it "dirname ($(mkRelDir parent) </> $(mkRelFile dirname)) == dirname $(mkRelFile dirname) (relative)"+        (dirnamesShouldBeEqual+          ($(mkRelDir "home\\chris\\") </> $(mkRelDir "bar"))+          $(mkRelDir "bar"))+     it "dirname ($(mkAbsDir parent) </> $(mkRelFile dirname)) == dirname $(mkRelFile dirname) (UNC)"+        (dirnamesShouldBeEqual+          ($(mkAbsDir "\\\\home\\chris\\") </> $(mkRelDir "bar"))+          $(mkRelDir "bar"))+     it "dirname ($(mkAbsDir parent) </> $(mkRelFile dirname)) == dirname $(mkRelFile dirname) (Unicode)"+        (dirnamesShouldBeEqual+          ($(mkAbsDir "\\\\?\\C:\\home\\chris\\") </> $(mkRelDir "bar"))+          $(mkRelDir "bar"))+     it "dirname $(mkRelDir .) == $(mkRelDir .)"+        (dirnamesShouldBeEqual+          $(mkRelDir ".")+          $(mkRelDir "."))+     it "dirname C:\\ must be a Rel path"+        ((parseAbsDir $ show $ dirname (fromJust (parseAbsDir "C:\\")) :: Maybe (Path Abs Dir)) == Nothing)+  where dirnamesShouldBeEqual = (==) `on` dirname  -- | The 'filename' operation. operationFilename :: Spec operationFilename =-  do it "filename ($(mkAbsDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename) (unit test)"-          (filename ($(mkAbsDir "C:\\chris\\") </>-                             $(mkRelFile "bar.txt")) ==-                                      filename $(mkRelFile "bar.txt"))--     it "filename ($(mkRelDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename) (unit test)"-             (filename ($(mkRelDir "home\\chris\\") </>-                                $(mkRelFile "bar.txt")) ==-                                         filename $(mkRelFile "bar.txt"))+  do it "filename ($(mkAbsDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename) (absolute)"+        (filenamesShouldBeEqual+          ($(mkAbsDir "C:\\chris\\") </> $(mkRelFile "bar.txt"))+          $(mkRelFile "bar.txt"))+     it "filename ($(mkRelDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename) (relative)"+        (filenamesShouldBeEqual+          ($(mkRelDir "home\\chris\\") </> $(mkRelFile "bar.txt"))+          $(mkRelFile "bar.txt"))+     it "filename ($(mkAbsDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename) (UNC)"+        (filenamesShouldBeEqual+          ($(mkAbsDir "\\\\host\\share\\chris\\") </> $(mkRelFile "bar.txt"))+          $(mkRelFile "bar.txt"))+     it "filename ($(mkAbsDir parent) </> $(mkRelFile filename)) == filename $(mkRelFile filename) (Unicode)"+        (filenamesShouldBeEqual+          ($(mkAbsDir "\\\\?\\C:\\home\\chris\\") </> $(mkRelFile "bar.txt"))+          $(mkRelFile "bar.txt"))+  where filenamesShouldBeEqual = (==) `on` filename  -- | The 'parent' operation. operationParent :: Spec operationParent =   do it "parent (parent </> child) == parent"-        (parent ($(mkAbsDir "C:\\foo") </>-                    $(mkRelDir "bar")) ==-         $(mkAbsDir "C:\\foo"))+        (parent ($(mkAbsDir "C:\\foo") </> $(mkRelDir "bar")) == $(mkAbsDir "C:\\foo"))      it "parent \"C:\\\" == \"C:\\\""         (parent $(mkAbsDir "C:\\") == $(mkAbsDir "C:\\"))      it "parent \"C:\\x\" == \"C:\\\""@@ -108,67 +124,103 @@ operationIsProperPrefixOf =   do it "isProperPrefixOf parent (parent </> child) (absolute)"         (isProperPrefixOf-           $(mkAbsDir "C:\\\\\\bar\\")-           ($(mkAbsDir "C:\\\\\\bar\\") </>-            $(mkRelFile "bar\\foo.txt")))-+          $(mkAbsDir "C:\\\\\\bar\\")+          ($(mkAbsDir "C:\\\\\\bar\\") </> $(mkRelFile "bar\\foo.txt")))      it "isProperPrefixOf parent (parent </> child) (relative)"         (isProperPrefixOf-           $(mkRelDir "bar\\")-           ($(mkRelDir "bar\\") </>-            $(mkRelFile "bob\\foo.txt")))-+          $(mkRelDir "bar\\")+          ($(mkRelDir "bar\\") </> $(mkRelFile "bob\\foo.txt")))+     it "isProperPrefixOf parent (parent </> child) (UNC)"+        (isProperPrefixOf+          $(mkAbsDir "\\\\host\\share\\")+          ($(mkAbsDir "\\\\host\\share\\") </> $(mkRelFile "bob\\foo.txt")))+     it "isProperPrefixOf parent (parent </> child) (Unicode)"+        (isProperPrefixOf+          $(mkAbsDir "\\\\?\\C:\\folder\\")+          ($(mkAbsDir "\\\\?\\C:\\folder\\") </> $(mkRelFile "bob\\foo.txt")))      it "not (x `isProperPrefixOf` x)"         (not (isProperPrefixOf $(mkRelDir "x") $(mkRelDir "x")))-      it "not (\\ `isProperPrefixOf` \\)"         (not (isProperPrefixOf $(mkAbsDir "C:\\") $(mkAbsDir "C:\\")))  -- | The 'stripProperPrefix' operation. operationStripProperPrefix :: Spec operationStripProperPrefix =-  do it "stripProperPrefix parent (parent </> child) = child (unit test)"-        (stripProperPrefix $(mkAbsDir "C:\\\\\\bar\\")-                  ($(mkAbsDir "C:\\\\\\bar\\") </>-                   $(mkRelFile "bar\\foo.txt")) ==-         Just $(mkRelFile "bar\\foo.txt"))--     it "stripProperPrefix parent (parent </> child) = child (unit test)"-        (stripProperPrefix $(mkRelDir "bar\\")-                  ($(mkRelDir "bar\\") </>-                   $(mkRelFile "bob\\foo.txt")) ==-         Just $(mkRelFile "bob\\foo.txt"))-+  do it "stripProperPrefix parent (parent </> child) = child (absolute)"+        (remainingPathShouldBe+          $(mkAbsDir "C:\\\\\\bar\\")+          ($(mkAbsDir "C:\\\\\\bar\\") </> $(mkRelFile "bar\\foo.txt"))+          (Just $(mkRelFile "bar\\foo.txt")))+     it "stripProperPrefix parent (parent </> child) = child (relative)"+        (remainingPathShouldBe+          $(mkRelDir "bar\\")+          ($(mkRelDir "bar\\") </> $(mkRelFile "bob\\foo.txt"))+          (Just $(mkRelFile "bob\\foo.txt")))+     it "stripProperPrefix parent (parent </> child) = child (UNC)"+        (remainingPathShouldBe+          $(mkAbsDir "\\\\host\\share\\")+          ($(mkAbsDir "\\\\host\\share\\") </> $(mkRelFile "bob\\foo.txt"))+          (Just $(mkRelFile "bob\\foo.txt")))+     it "stripProperPrefix parent (parent </> child) = child (Unicode)"+        (remainingPathShouldBe+          $(mkAbsDir "\\\\?\\C:\\folder\\")+          ($(mkAbsDir "\\\\?\\C:\\folder\\") </> $(mkRelFile "bob\\foo.txt"))+          (Just $(mkRelFile "bob\\foo.txt")))      it "stripProperPrefix parent parent = _|_"-        (stripProperPrefix $(mkAbsDir "C:\\home\\chris\\foo")-                  $(mkAbsDir "C:\\home\\chris\\foo") ==-         Nothing)+        (remainingPathShouldBe+          $(mkAbsDir "C:\\home\\chris\\foo")+          $(mkAbsDir "C:\\home\\chris\\foo")+          Nothing)+  where remainingPathShouldBe prefix path suffix =+          stripProperPrefix prefix path == suffix  -- | The '</>' operation. operationAppend :: Spec operationAppend =   do it "AbsDir + RelDir = AbsDir"-        ($(mkAbsDir "C:\\home\\") </>-         $(mkRelDir "chris") ==-         $(mkAbsDir "C:\\home\\chris\\"))+        (shouldBe+          ($(mkAbsDir "C:\\home\\") </> $(mkRelDir "chris"))+          $(mkAbsDir "C:\\home\\chris\\"))      it "AbsDir + RelFile = AbsFile"-        ($(mkAbsDir "C:\\home\\") </>-         $(mkRelFile "chris\\test.txt") ==-         $(mkAbsFile "C:\\home\\chris\\test.txt"))+        (shouldBe+          ($(mkAbsDir "C:\\home\\") </> $(mkRelFile "chris\\test.txt"))+          $(mkAbsFile "C:\\home\\chris\\test.txt"))      it "RelDir + RelDir = RelDir"-        ($(mkRelDir "home\\") </>-         $(mkRelDir "chris") ==-         $(mkRelDir "home\\chris"))+        (shouldBe+          ($(mkRelDir "home\\") </> $(mkRelDir "chris"))+          $(mkRelDir "home\\chris"))      it ". + . = ."-        ($(mkRelDir ".\\") </> $(mkRelDir ".") == $(mkRelDir "."))+        (shouldBe+          ($(mkRelDir ".\\") </> $(mkRelDir "."))+          $(mkRelDir "."))      it ". + x = x"-        ($(mkRelDir ".") </> $(mkRelDir "x") == $(mkRelDir "x"))+        (shouldBe+          ($(mkRelDir ".") </> $(mkRelDir "x"))+          $(mkRelDir "x"))      it "x + . = x"-        ($(mkRelDir "x") </> $(mkRelDir ".\\") == $(mkRelDir "x"))+        (shouldBe+          ($(mkRelDir "x") </> $(mkRelDir ".\\"))+          $(mkRelDir "x"))      it "RelDir + RelFile = RelFile"-        ($(mkRelDir "home\\") </>-         $(mkRelFile "chris\\test.txt") ==-         $(mkRelFile "home\\chris\\test.txt"))+        (shouldBe+          ($(mkRelDir "home\\") </> $(mkRelFile "chris\\test.txt"))+          $(mkRelFile "home\\chris\\test.txt"))+     it "AbsDir(UNC) + RelDir = AbsDir(UNC)"+        (shouldBe+          ($(mkAbsDir "\\\\host\\share\\") </> $(mkRelDir "folder\\"))+          $(mkAbsDir "\\\\host\\share\\folder\\"))+     it "AbsDir(UNC) + RelFile = AbsFile(UNC)"+        (shouldBe+          ($(mkAbsDir "\\\\host\\share\\") </> $(mkRelFile "folder\\file.txt"))+          $(mkAbsFile "\\\\host\\share\\folder\\file.txt"))+     it "AbsDir(Unicode) + RelDir = AbsDir(Unicode)"+        (shouldBe+          ($(mkAbsDir "\\\\?\\C:\\folder\\") </> $(mkRelDir "another\\"))+          $(mkAbsDir "\\\\?\\C:\\folder\\another\\"))+     it "AbsDir(Unicode) + RelFile = AbsFile(Unicode)"+        (shouldBe+          ($(mkAbsDir "\\\\?\\C:\\folder\\") </> $(mkRelFile "file.txt"))+          $(mkAbsFile "\\\\?\\C:\\folder\\file.txt"))  operationToFilePath :: Spec operationToFilePath =@@ -177,56 +229,24 @@      it "show $(mkRelDir \".\") == \"\\\".\\\\\"\""         (show $(mkRelDir ".") == "\".\\\\\"") -operationAddFileExtension :: Spec-operationAddFileExtension = do-  it "adds extension if there is none" $-    addFileExtension "ext" $(mkAbsFile "C:\\directory\\path")-      `shouldReturn` $(mkAbsFile "C:\\directory\\path.ext")-  it "adds extension if there is already one" $-    addFileExtension "baz" $(mkRelFile "foo.bar")-      `shouldReturn` $(mkRelFile "foo.bar.baz")-  it "adds extension with dot" $-    addFileExtension ".bar" $(mkRelFile "foo")-      `shouldReturn` $(mkRelFile "foo.bar")-  it "adds extension with dot after dot" $-    $(mkRelFile "foo.") <.> ".bar"-      `shouldReturn` $(mkRelFile "foo..bar")-  it "adds extension without dot after dot" $-    $(mkRelFile "foo.") <.> "bar"-      `shouldReturn` $(mkRelFile "foo..bar")-  it "adds extension with separator" $  -- I'm not sure it's okay-    $(mkRelFile "foo") <.> "evil\\extension"-      `shouldReturn` $(mkRelFile "foo.evil\\extension")-  it "adds extension to file inside dotted directory" $-    $(mkRelFile "foo.bar\\baz") <.> "txt"-      `shouldReturn` $(mkRelFile "foo.bar\\baz.txt")-  it "throws InvalidRelFile extension if extenstion ends with \\" $-    $(mkRelFile "foo") <.> "evil\\"-      `shouldThrow` (== InvalidRelFile "foo.evil\\")-  it "throws InvalidAbsFile extension if extenstion ends with \\" $-    $(mkAbsFile "C:\\home\\cfg") <.> "txt\\"-      `shouldThrow` (== InvalidAbsFile "C:\\home\\cfg.txt\\")--operationSetFileExtension :: Spec-operationSetFileExtension = do-  it "adds extension if there is none" $-    setFileExtension "txt" $(mkRelFile "foo")-      `shouldReturn` $(mkRelFile "foo.txt")-  it "replaces extension if the input path already has one" $-    setFileExtension "txt" $(mkRelFile "foo.bar")-      `shouldReturn` $(mkRelFile "foo.txt")- -- | Tests for the tokenizer. parseAbsDirSpec :: Spec parseAbsDirSpec =   do failing ""      failing ".\\"      failing "foo.txt"+     failing "C:"      succeeding "C:\\" (Path "C:\\")-     succeeding "C:\\\\" (Path "C:\\\\")-     -- succeeding "C:\\\\\\foo\\\\bar\\\\mu\\" (Path "C:\\foo\\bar\\mu\\") FIXME-     -- succeeding "C:\\\\\\foo\\\\bar\\\\mu" (Path "C:\\foo\\bar\\mu\\") FIXME-     -- succeeding "C:\\\\\\foo\\\\bar\\.\\\\mu" (Path "C:\\foo\\bar\\mu\\") FIXME+     succeeding "C:\\\\" (Path "C:\\")+     succeeding "C:\\\\\\foo\\\\bar\\\\mu\\" (Path "C:\\foo\\bar\\mu\\")+     succeeding "C:\\\\\\foo\\\\bar\\\\mu" (Path "C:\\foo\\bar\\mu\\")+     succeeding "C:\\\\\\foo\\\\bar\\.\\\\mu" (Path "C:\\foo\\bar\\mu\\")+     succeeding "\\\\unchost\\share" (Path "\\\\unchost\\share\\")+     succeeding "\\/unchost\\share" (Path "\\\\unchost\\share\\")+     succeeding "\\\\unchost\\share\\\\folder\\" (Path "\\\\unchost\\share\\folder\\")+     succeeding "\\\\?\\C:\\" (Path "\\\\?\\C:\\")+     succeeding "/\\?\\C:\\" (Path "\\\\?\\C:\\")+     succeeding "\\\\?\\C:\\\\\\folder\\\\" (Path "\\\\?\\C:\\folder\\")    where failing x = parserTest parseAbsDir x Nothing         succeeding x with = parserTest parseAbsDir x (Just with)@@ -235,16 +255,17 @@ parseRelDirSpec :: Spec parseRelDirSpec =   do failing ""-     -- failing "/" FIXME-     -- failing "//" FIXME-     -- succeeding "~/" (Path "~/") -- https://github.com/chrisdone/path/issues/19-     -- failing "\\" FIXME-     succeeding ".\\" (Path "")-     succeeding ".\\.\\" (Path "")+     failing "/"+     failing "//"+     failing "\\"      failing "\\\\"      failing "\\\\\\foo\\\\bar\\\\mu\\"      failing "\\\\\\foo\\\\bar\\\\\\\\mu"      failing "\\\\\\foo\\\\bar\\.\\\\mu"+     failing "\\\\unchost\\share"+     failing "\\\\?\\C:\\"+     succeeding ".\\" (Path "")+     succeeding ".\\.\\" (Path "")      succeeding "..." (Path "...\\")      succeeding "foo.bak" (Path "foo.bak\\")      succeeding ".\\foo" (Path "foo\\")@@ -269,10 +290,16 @@      failing "\\"      failing "\\\\"      failing "\\\\\\foo\\\\bar\\\\mu\\"-     -- succeeding "\\..." (Path "\\...") FIXME-     -- succeeding "\\foo.txt" (Path "\\foo.txt") FIXME-     -- succeeding "C:\\\\\\foo\\\\bar\\\\\\\\mu.txt" (Path "C:\\foo\\bar\\mu.txt") FIXME-     -- succeeding "C:\\\\\\foo\\\\bar\\.\\\\mu.txt" (Path "C:\\foo\\bar\\mu.txt") FIXME+     failing "\\..."+     failing "\\foo.txt"+     succeeding "C:\\\\\\foo\\\\bar\\\\\\\\mu.txt" (Path "C:\\foo\\bar\\mu.txt")+     succeeding "C:\\\\\\foo\\\\bar\\.\\\\mu.txt" (Path "C:\\foo\\bar\\mu.txt")+     succeeding "\\\\unchost\\share\\\\file.txt" (Path "\\\\unchost\\share\\file.txt")+     succeeding "\\/unchost\\share\\\\file.txt" (Path "\\\\unchost\\share\\file.txt")+     succeeding "\\\\unchost\\share\\.\\folder\\\\\\file.txt" (Path "\\\\unchost\\share\\folder\\file.txt")+     succeeding "\\\\?\\C:\\file.txt" (Path "\\\\?\\C:\\file.txt")+     succeeding "/\\?\\C:\\file.txt" (Path "\\\\?\\C:\\file.txt")+     succeeding "\\\\?\\C:\\\\\\folder\\.\\\\file.txt" (Path "\\\\?\\C:\\folder\\file.txt")    where failing x = parserTest parseAbsFile x Nothing         succeeding x with = parserTest parseAbsFile x (Just with)@@ -294,6 +321,8 @@      failing "\\\\\\foo\\\\bar\\\\mu\\"      failing "\\\\\\foo\\\\bar\\\\\\\\mu"      failing "\\\\\\foo\\\\bar\\.\\\\mu"+     failing "\\\\unchost\\share\\\\file.txt"+     failing "\\\\?\\C:\\file.txt"      succeeding "a.." (Path "a..")      succeeding "..." (Path "...")      succeeding "foo.txt" (Path "foo.txt")