Fork chrisdone's path library

I wasn't happy with the way it dealt with Dir vs File things. In his
version of the library, a `Path b Dir` always ends with a trailing
path separator and `Path b File` never ends with a trailing path separator.

IMO, it is nonsensical to make a Dir vs File distinction on path level,
although it first seems nice.
Some of the reasons are:
* a path is just that: a path. It is completely disconnected from IO level
  and even if a `Dir`/`File` type theoretically allows us to say "this path
  ought to point to a file", there is literally zero guarantee that it will
  hold true at runtime. So this basically gives a false feeling of a
  type-safe file distinction.
* it's imprecise about Dir vs File distinction, which makes it even worse,
  because a directory is also a file (just not a regular file). Add symlinks
  to that and the confusion is complete.
* it makes the API oddly complicated for use cases where we basically don't
  care (yet) whether something turns out to be a directory or not

Still, it comes also with a few perks:
* it simplifies some functions, because they now have guarantees whether a
  path ends in a trailing path separator or not
* it may be safer for interaction with other library functions, which behave
  differently depending on a trailing path separator (like probably shelly)

Not limited to, but also in order to fix my remarks without breaking any
benefits, I did:
* rename the `Dir`/`File` types to `TPS`/`NoTPS`, so it's clear we are only
  giving information about trailing path separators and not actual file
  types we don't know about yet
* add a `MaybeTPS` type, which does not mess with trailing path separators
  and also gives no guarantees about them... then added `toNoTPS` and
  `toTPS` to allow type-safe conversion
* make some functions accept more general types, so we don't unnecessarily
  force paths with trailing separators for `(</>)` for example... instead
  these functions now examine the paths to still have correct behavior.
  This is really minor overhead. You might say now "but then I can append
  filepath to filepath". Well, as I said... we don't know whether it's a
  "filepath" at all.
* merge `filename` and `dirname` into `basename` and make `parent` be
  `dirname`, so the function names match the name of the POSIX ones,
  which do (almost) the same...
* fix a bug in `basename` (formerly `dirname`) which broke the type
  guarantees
* add a pattern synonym for easier pattern matching without exporting
  the internal Path constructor
This commit is contained in:
Julian Ospald 2016-03-08 22:53:42 +01:00
parent e2974d3152
commit d15e4b8ad9
9 changed files with 617 additions and 984 deletions

2
.gitignore vendored
View File

@ -8,3 +8,5 @@ TAGS
tags tags
*.tag *.tag
.stack-work/ .stack-work/
.cabal-sandbox/
cabal.sandbox.config

View File

@ -1,3 +1,5 @@
0.5.8:
* First version of the fork.
0.5.7: 0.5.7:
* Fix haddock problem. * Fix haddock problem.
0.5.6: 0.5.6:

View File

@ -1,4 +1,5 @@
Copyright (c) 20152016, FP Complete Copyright (c) 20152016, FP Complete
Copyright (c) 2016, Julian Ospald
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

586
README.md
View File

@ -1,518 +1,80 @@
# Path # HPath
Support for well-typed paths in Haskell. Support for well-typed paths in Haskell.
* [Motivation](#motivation)
* [Approach](#approach)
* [Solution](#solution)
* [Implementation](#implementation)
* [The data types](#the-data-types)
* [Parsers](#parsers)
* [Smart constructors](#smart-constructors)
* [Overloaded stings](#overloaded-strings)
* [Operations](#operations)
* [Review](#review)
* [Relative vs absolute confusion](#relative-vs-absolute-confusion)
* [The equality problem](#the-equality-problem)
* [Unpredictable concatenation issues](#unpredictable-concatenation-issues)
* [Confusing files and directories](#confusing-files-and-directories)
* [Self-documentation](#self-documentation)
* [In practice](#in-practice)
* [Doing I/O](#doing-io)
* [Doing textual manipulations](#doing-textual-manipulations)
* [Accepting user input](#accepting-user-input)
* [Comparing with existing path libraries](#comparing-with-existing-path-libraries)
* [filepath and system-filepath](#filepath-and-system-filepath)
* [system-canonicalpath, canonical-filepath, directory-tree](#system-canonicalpath-canonical-filepath-directory-tree)
* [pathtype](#pathtype)
* [data-filepath](#data-filepath)
* [Summary](#summary)
## Motivation ## Motivation
It was after working on a number of projects at FP Complete that use file The motivation came during development of
paths in various ways. We used the system-filepath package, which was [hsfm](https://github.com/hasufell/hsfm)
supposed to solve many path problems by being an opaque path type. It which has a pretty strict File type, but lacks a strict Path type, e.g.
occurred to me that the same kind of bugs kept cropping up: for user input.
The library that came closest to my needs was
[path](https://github.com/chrisdone/path),
but the API turned out to be oddly complicated for my use case, so I
decided to fork it.
## Differences to 'path'
I wasn't happy with the way it dealt with Dir vs File things. In his
version of the library, a `Path b Dir` always ends with a trailing
path separator and `Path b File` never ends with a trailing path separator.
IMO, it is nonsensical to make a Dir vs File distinction on path level,
although it first seems nice.
Some of the reasons are:
* a path is just that: a path. It is completely disconnected from IO level
and even if a `Dir`/`File` type theoretically allows us to say "this path
ought to point to a file", there is literally zero guarantee that it will
hold true at runtime. So this basically gives a false feeling of a
type-safe file distinction.
* it's imprecise about Dir vs File distinction, which makes it even worse,
because a directory is also a file (just not a regular file). Add symlinks
to that and the confusion is complete.
* it makes the API oddly complicated for use cases where we basically don't
care (yet) whether something turns out to be a directory or not
Still, it comes also with a few perks:
* it simplifies some functions, because they now have guarantees whether a
path ends in a trailing path separator or not
* it may be safer for interaction with other library functions, which behave
differently depending on a trailing path separator (like probably shelly)
Not limited to, but also in order to fix my remarks without breaking any
benefits, I did:
* rename the `Dir`/`File` types to `TPS`/`NoTPS`, so it's clear we are only
giving information about trailing path separators and not actual file
types we don't know about yet
* add a `MaybeTPS` type, which does not mess with trailing path separators
and also gives no guarantees about them... then added `toNoTPS` and
`toTPS` to allow type-safe conversion
* make some functions accept more general types, so we don't unnecessarily
force paths with trailing separators for `(</>)` for example... instead
these functions now examine the paths to still have correct behavior.
This is really minor overhead. You might say now "but then I can append
filepath to filepath". Well, as I said... we don't know whether it's a
"filepath" at all.
* merge `filename` and `dirname` into `basename` and make `parent` be
`dirname`, so the function names match the name of the POSIX ones,
which do (almost) the same...
* fix a bug in `basename` (formerly `dirname`) which broke the type
guarantees
* add a pattern synonym for easier pattern matching without exporting
the internal Path constructor
## Consequences
So what does that mean? Well, it means that this library does not and
cannot make any guarantees about what a filepath is meant for or what
it might point to. And it doesn't pretend it can.
So when you strip the trailing path separator of a path that points to a
directory and then shove it into some function which expects a regular
file... then that function will very likely blow up. That's the nature of IO.
There is no type that can save you from interfacing such low-level libraries.
The filesystem is in constant change. What might have been a regular file
2 seconds ago, can now be a directory or a symlink.
That means you need a proper File type that is tied to your IO code.
This is what [hsfm](https://github.com/hasufell/hsfm) does. It currently
is not a library, maybe it will be in the future.
* Expected a path to be absolute but it was relative, or vice-versa.
* Expected two equivalent paths to be equal or order the same, but they did
not (`/home//foo` vs `/home/foo/` vs `/home/bar/../foo`, etc.).
* Unpredictable behaviour with regards to concatenating paths.
* Confusing files and directories.
* Not knowing whether a path was a file or directory or relative or absolute
based on the type alone was a drag.
All of these bugs are preventable.
## Approach
My approach to problems like this is to make a type that encodes the
properties I want and then make it impossible to let those invariants be
broken, without compromise or backdoors to let the wrong value “slip
in”. Once I have a path, I want to be able to trust it fully. This theme
will be seen throughout the things I lay out below.
## Solution
After having to fix bugs due to these in our software, I put my foot down
and made:
* An opaque `Path` type (a newtype wrapper around `String`).
* Smart constructors which are very stringent in the parsing.
* Make the parsers highly normalizing.
* Leave equality and concatenation to basic string equality and
concatenation.
* Include relativity (absolute/relative) and type (directory/file) in the
type itself.
* Use the already cross-platform
[filepath](http://hackage.haskell.org/package/filepath) package for
implementation details.
## Implementation
### The data types
Here is the type:
```haskell
newtype Path b t = Path FilePath
deriving (Typeable)
```
The type variables are:
* `b` — base, the base location of the path; absolute or relative.
* `t` — type, whether file or directory.
The base types can be filled with these:
```haskell
data Abs deriving (Typeable)
data Rel deriving (Typeable)
```
And the type can be filled with these:
```haskell
data File deriving (Typeable)
data Dir deriving (Typeable)
```
(Why not use data kinds like `data Type = File | Dir`? Because that imposes
an extension overhead of adding `{-# LANGUAGE DataKinds #-}` to every module
you might want to write out a path type in. Given that one cannot construct
paths of types other than these, via the operations in the module, its not
a concern for me.)
There is a conversion function to give you back the filepath:
```haskell
toFilePath :: Path b t -> FilePath
toFilePath (Path l) = l
```
Beginning from version 0.5.3, there are type-constrained versions of
`toFilePath` with the following signatures:
```haskell
fromAbsDir :: Path Abs Dir -> FilePath
fromRelDir :: Path Rel Dir -> FilePath
fromAbsFile :: Path Abs File -> FilePath
fromRelFile :: Path Rel File -> FilePath
```
### Parsers
To get a `Path` value, you need to use one of the four parsers:
```haskell
parseAbsDir :: MonadThrow m => FilePath -> m (Path Abs Dir)
parseRelDir :: MonadThrow m => FilePath -> m (Path Rel Dir)
parseAbsFile :: MonadThrow m => FilePath -> m (Path Abs File)
parseRelFile :: MonadThrow m => FilePath -> m (Path Rel File)
```
The following properties apply:
* Absolute parsers will reject non-absolute paths.
* The only delimiter syntax accepted is the path separator; `/` on POSIX and
`\` on Windows.
* Any other delimiter is rejected; `..`, `~/`, `/./`, etc.
* All parsers normalize into single separators: `/home//foo``/home/foo`.
* Directory parsers always normalize with a final trailing `/`. So `/home/foo`
parses into the string `/home/foo/`.
It was discussed briefly whether we should just have a class for parsing
rather than four separate parsing functions. In my experience so far, I have
had type errors where I wrote something `like x <- parseAbsDir
someAbsDirString` because `x` was then passed to a place that expected a
relative directory. In this way, overloading the return value wouldve just
been accepted. So I dont think having a class is a good idea. Being
explicit here doesnt exactly waste our time, either.
Why are these functions in `MonadThrow`? Because it means I can have it
return an `Either`, or a `Maybe`, if Im in pure code, and if Im in `IO`,
and I dont expect parsing to ever fail, I can use it in IO like this:
```haskell
do x <- parseRelFile (fromCabalFileName x)
foo x
```
Thats really convenient and we take advantage of this at FP Complete a lot.
The instances
Equality, ordering and printing are simply re-using the `String` instances:
```haskell
instance Eq (Path b t) where
(==) (Path x) (Path y) = x == y
instance Ord (Path b t) where
compare (Path x) (Path y) = compare x y
instance Show (Path b t) where
show (Path x) = show x
```
Which gives us for free the following equational properties:
```haskell
toFilePath x == toFilePath y ≡ x == y -- Eq instance
toFilePath x `compare` toFilePath y ≡ x `compare` y -- Ord instance
toFilePath x == toFilePath y ≡ show x == show y -- Show instance
```
In other words, the representation and the path you get out at the end are
the same. Two paths that are equal will always give you back the same thing.
### Smart constructors
For when you know what a path will be at compile-time, there are
constructors for that:
```haskell
$(mkAbsDir "/home/chris")
$(mkRelDir "chris")
$(mkAbsFile "/home/chris/x.txt")
$(mkRelFile "chris/x.txt")
```
These will run at compile-time and underneath use the appropriate parser.
### Overloaded strings
No `IsString` instance is provided, because that has no way to statically
determine whether the path is correct, and would otherwise have to be a
partial function.
In practice I have written the wrong path format in a `$(mk… "")` and been
thankful it was caught early.
### Operations
There is path concatenation:
```haskell
(</>) :: Path b Dir -> Path Rel t -> Path b t
```
Get the parent directory of a path:
```haskell
parent :: Path Abs t -> Path Abs Dir
```
Get the filename of a file path:
```haskell
filename :: Path b File -> Path Rel File
```
Get the directory name of a directory path:
```haskell
dirname :: Path b Dir -> Path Rel Dir
```
Stripping the parent directory from a path:
```haskell
stripDir :: MonadThrow m => Path b Dir -> Path b t -> m (Path Rel t)
```
## Review
Lets review my initial list of complaints and see if theyve been
satisfied.
### Relative vs absolute confusion
Paths now distinguish in the type system whether they are relative or
absolute. You cant append two absolute paths, for example:
```haskell
λ> $(mkAbsDir "/home/chris") </> $(mkAbsDir "/home/chris")
<interactive>:23:31-55:
Couldn't match type Abs with Rel
```
### The equality problem
Paths are now stringently normalized. They have to be a valid path, and they
only support single path separators, and all directories are suffixed with a
trailing path separator:
```haskell
λ> $(mkAbsDir "/home/chris//") == $(mkAbsDir "/./home//chris")
True
λ> toFilePath $(mkAbsDir "/home/chris//") ==
toFilePath $(mkAbsDir "/./home//chris")
True
λ> ($(mkAbsDir "/home/chris//"),toFilePath $(mkAbsDir "/./home//chris"))
("/home/chris/","/home/chris/")
```
### Unpredictable concatenation issues
Because of the stringent normalization, path concatenation, as seen above,
is simply string concatenation. This is about as predictable as it can get:
```haskell
λ> toFilePath $(mkAbsDir "/home/chris//")
"/home/chris/"
λ> toFilePath $(mkRelDir "foo//bar")
"foo/bar/"
λ> $(mkAbsDir "/home/chris//") </> $(mkRelDir "foo//bar")
"/home/chris/foo/bar/"
```
### Confusing files and directories
Now that the path type is encoded in the type system, our `</>` operator
prevents improper appending:
```haskell
λ> $(mkAbsDir "/home/chris/") </> $(mkRelFile "foo//bar")
"/home/chris/foo/bar"
λ> $(mkAbsFile "/home/chris") </> $(mkRelFile "foo//bar")
<interactive>:35:1-26:
Couldn't match type File with Dir
```
### Self-documentation
Now I can read the path like:
```haskell
{ fooPath :: Path Rel Dir, ... }
```
And know that this refers to the directory relative to some other path,
meaning I should be careful to consider the current directory when using
this in IO, or that Ill probably need a parent to append to it at some
point.
## In practice
Weve been using this at FP Complete in a number of packages for some months
now, its turned out surprisingly sufficient for most of our path work with
only one bug found. We werent sure initially whether it would just be too
much of a pain to use, but really its quite acceptable given the
advantages. You can see its use all over the
[`stack`](https://github.com/commercialhaskell/stack) codebase.
## Doing I/O
Currently any operations involving I/O can be done by using the existing I/O
library:
```haskell
doesFileExist (toFilePath fp)
readFile (toFilePath fp)
```
etc. This has problems with respect to accidentally running something like:
```haskell
doesFileExist $(mkRelDir "foo")
```
But I/O is currently outside the scope of what this package solves. Once you
leave the realm of the `Path` type invariants are back to your responsibility.
As with the original version of this library, were currently building up a
set of functions in a `Path.IO` module over time that fits our real-world
use-cases. It may or may not appear in the path package eventually. Itll
need cleaning up and considering what should really be included.
**Edit:** There is now
[`path-io`](https://hackage.haskell.org/package/path-io) package that
complements the `path` library and includes complete well-typed interface to
[`directory`](https://hackage.haskell.org/package/directory) and
[`temporary`](https://hackage.haskell.org/package/temporary). There is work
to add more generally useful functions from Stack's `Path.IO` to it and make
Stack depend on the `path-io` package.
## Doing textual manipulations
One problem that crops up sometimes is wanting to manipulate
paths. Currently the way we do it is via the filepath library and re-parsing
the path:
```haskell
parseAbsFile . addExtension "/directory/path" "ext" . toFilePath
```
It doesnt happen too often, in our experience, to the extent this needs to
be more convenient.
## Accepting user input
Sometimes you have user input that contains `../`. The solution we went with
is to have a function like `resolveDir`:
```haskell
resolveDir :: (MonadIO m, MonadThrow m)
=> Path Abs Dir -> FilePath -> m (Path Abs Dir)
```
Which will call `canonicalizePath` which collapses and normalizes a path and
then we parse with regular old `parseAbsDir` and were cooking with
gas. This and others like it might get added to the `path` package.
## Comparing with existing path libraries
### filepath and system-filepath
The [filepath](http://hackage.haskell.org/package/filepath) package is
intended as the complimentary package to be used before parsing into a Path
value, and/or after printing from a Path value. The package itself contains
no type-safety, instead contains a range of cross-platform textual
operations. Definitely reach for this library when you want to do more
involved manipulations.
The `system-filepath` package is deprecated in favour of `filepath`.
### system-canonicalpath, canonical-filepath, directory-tree
The
[`system-canonicalpath`](http://hackage.haskell.org/package/system-canonicalpath)
and the
[`canonical-filepath`](http://hackage.haskell.org/package/canonical-filepath)
packages both are a kind of subset of `path`. They canonicalize a string
into an opaque path, but neither distinguish directories from files or
absolute/relative. Useful if you just want a canonical path but doesnt do
anything else.
The [`directory-tree`](http://hackage.haskell.org/package/directory-tree)
package contains a sum type of dir/file/etc but doesnt distinguish in its
operations relativity or path type.
### pathtype
Finally, we come to a path library that path is similar to: the
[`pathtype`](http://hackage.haskell.org/package/pathtype) library. There are
the same types of `Path Abs File` / `Path Rel Dir`, etc.
The points where this library isnt enough for me are:
* There is an `IsString` instance, which means people will use it, and will
make mistakes.
* Paths are not normalized into a predictable format, leading to me being
unsure when equality will succeed. This is the same problem I encountered
in `system-filepath`. The equality function normalizes, but according to
what properties I can reason about? I dont know.
```haskell
System.Path.Posix> ("/tmp//" :: Path a Dir) == ("/tmp" :: Path a Dir)
True
System.Path.Posix> ("tmp" :: Path a Dir) == ("/tmp" :: Path a Dir)
True
System.Path.Posix> ("/etc/passwd/" :: Path a b) == ("/etc/passwd" :: Path a b)
True
System.Path.Posix> ("/tmp//" :: Path Abs Dir) == ("/tmp/./" :: Path Abs Dir)
False
System.Path.Posix> ("/tmp/../" :: Path Abs Dir) == ("/" :: Path Abs Dir)
False
```
* Empty string should not be allowed, and introduction of `.` due to that
gets weird:
```haskell
System.Path.Posix> fmap getPathString (Right ("." :: Path Rel File))
Right "."
System.Path.Posix> fmap getPathString (mkPathAbsOrRel "")
Right "."
System.Path.Posix> (Right ("." :: Path Rel File)) == (mkPathAbsOrRel "")
False
System.Path.Posix> takeDirectory ("tmp" :: Path Rel Dir)
.
System.Path.Posix> (getPathString ("." :: Path Rel File) ==
getPathString ("" :: Path Rel File))
True
System.Path.Posix> (("." :: Path Rel File) == ("" :: Path Rel File))
False
```
* It has functions like `<.>/addExtension` which lets you insert an
arbitrary string into a path.
* Some functions let you produce nonsense (could be prevented by a stricter
type), for example:
```haskell
System.Path.Posix> takeFileName ("/tmp/" :: Path Abs Dir)
tmp
```
Im being a bit picky here, a bit unfair. But the point is really to show
the kind of things I tried to avoid in `path`. In summary, its just hard to
know where things can go wrong, similar to what was going on in
`system-filepath`.
### data-filepath
The [`data-filepath`](https://hackage.haskell.org/package/data-filepath) is
also very similar, I discovered it after writing my own at work and was
pleased to see its mostly the same. The main differences are:
* Uses `DataKinds` for the relative/absolute and file/dir distinction which
as I said above is an overhead.
* Uses a GADT for the path type, which is fine. In my case I wanted to
retain the original string which functions that work on the `FilePath`
(`String`) type already deal with well. It does change the parsing step
somewhat, because it parses into segments.
* Its more lenient at parsing (allowing `..` and trailing `.`).
The API is a bit awkward to just parse a directory, requires a couple
functions to get it (going via `WeakFilePath`), returning only an `Either`,
and there are no functions like parent. But theres not much to complain
about. Its a fine library, but I didnt feel the need to drop my own in
favor of it. Check it out and decide for yourself.
## Summary
Theres a growing interest in making practical use of well-typed file path
handling. I think everyones wanted it for a while, but few people have
really committed to it in practice. Now that Ive been using `path` for a
while, I cant really go back. Itll be interesting to see what new packages
crop up in the coming year, I expect therell be more.

View File

@ -1,12 +1,12 @@
name: path name: hpath
version: 0.5.7 version: 0.5.8
synopsis: Support for well-typed paths synopsis: Support for well-typed paths
description: Support for will-typed paths. description: Support for will-typed paths.
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Chris Done <chrisdone@fpcomplete.com> author: Julian Ospald <hasufell@posteo.de>
maintainer: Chris Done <chrisdone@fpcomplete.com> maintainer: Julian Ospald <hasufell@posteo.de>
copyright: 20152016 FP Complete copyright: 20152016 FP Complete, Julian Ospald 2016
category: Filesystem category: Filesystem
build-type: Simple build-type: Simple
cabal-version: >=1.8 cabal-version: >=1.8
@ -15,7 +15,7 @@ extra-source-files: README.md, CHANGELOG
library library
hs-source-dirs: src/ hs-source-dirs: src/
ghc-options: -Wall -O2 ghc-options: -Wall -O2
exposed-modules: Path, Path.Internal exposed-modules: HPath, HPath.Internal
build-depends: base >= 4 && <5 build-depends: base >= 4 && <5
, exceptions , exceptions
, filepath , filepath
@ -30,8 +30,8 @@ test-suite test
, base , base
, hspec , hspec
, mtl , mtl
, path , hpath
source-repository head source-repository head
type: git type: git
location: https://github.com/chrisdone/path.git location: https://github.com/hasufell/hpath

415
src/HPath.hs Normal file
View File

@ -0,0 +1,415 @@
-- |
-- Module : HPath
-- Copyright : © 20152016 FP Complete, 2016 Julian Ospald
-- License : BSD 3 clause
--
-- Maintainer : Julian Ospald <hasufell@posteo.de>
-- Stability : experimental
-- Portability : portable
--
-- Support for well-typed paths.
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_HADDOCK ignore-exports #-}
module HPath
(
-- * Types
Abs
,NoTPS
,Path
,Rel
,TPS
-- * PatternSynonyms/ViewPatterns
,pattern Path
-- * Parsing
,PathParseException
,parseAbsMaybeTPS
,parseAbsNoTPS
,parseAbsTPS
,parseRelMaybeTPS
,parseRelNoTPS
,parseRelTPS
-- * Constructors
,mkAbsMaybeTPS
,mkAbsNoTPS
,mkAbsTPS
,mkRelMaybeTPS
,mkRelNoTPS
,mkRelTPS
-- * Operations
,(</>)
,basename
,dirname
,isParentOf
,stripDir
-- * Conversion
,fromAbsMaybeTPS
,fromAbsNoTPS
,fromAbsTPS
,fromRelMaybeTPS
,fromRelNoTPS
,fromRelTPS
,toFilePath
,toNoTPS
,toTPS
)
where
import Control.Exception (Exception)
import Control.Monad.Catch (MonadThrow(..))
import Data.Data
import Data.List
import Data.Maybe
import Language.Haskell.TH
import HPath.Internal
import qualified System.FilePath as FilePath
--------------------------------------------------------------------------------
-- Types
-- | An absolute path.
data Abs deriving (Typeable)
-- | A relative path; one without a root.
data Rel deriving (Typeable)
-- | A path without trailing separator.
data NoTPS deriving (Typeable)
-- | A path with trailing separator.
data TPS deriving (Typeable)
-- | A path without any guarantee about whether it ends in a
-- trailing path separators. Use `toTPS` and `toNoTPS`
-- if that guarantee is required.
data MaybeTPS deriving (Typeable)
-- | Exception when parsing a location.
data PathParseException
= InvalidAbsTPS FilePath
| InvalidRelTPS FilePath
| InvalidAbsNoTPS FilePath
| InvalidRelNoTPS FilePath
| InvalidAbsMaybeTPS FilePath
| InvalidRelMaybeTPS FilePath
| Couldn'tStripPrefixTPS FilePath FilePath
deriving (Show,Typeable)
instance Exception PathParseException
--------------------------------------------------------------------------------
-- PatternSynonyms
pattern Path x <- (MkPath x)
--------------------------------------------------------------------------------
-- Parsers
-- | Get a location for an absolute path. Produces a normalized
-- path which always ends in a path separator.
--
-- Throws: 'PathParseException'
--
parseAbsTPS :: MonadThrow m
=> FilePath -> m (Path Abs TPS)
parseAbsTPS filepath =
if FilePath.isAbsolute filepath &&
not (null (normalizeTPS filepath)) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
FilePath.isValid filepath
then return (MkPath (normalizeTPS filepath))
else throwM (InvalidAbsTPS filepath)
-- | Get a location for a relative path. Produces a normalized
-- path which always ends in a path separator.
--
-- Note that @filepath@ may contain any number of @./@ but may not consist
-- solely of @./@. It also may not contain a single @..@ anywhere.
--
-- Throws: 'PathParseException'
--
parseRelTPS :: MonadThrow m
=> FilePath -> m (Path Rel TPS)
parseRelTPS filepath =
if not (FilePath.isAbsolute filepath) &&
not (null filepath) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeTPS filepath)) &&
filepath /= "." && filepath /= ".." &&
FilePath.isValid filepath
then return (MkPath (normalizeTPS filepath))
else throwM (InvalidRelTPS filepath)
-- | Get a location for an absolute path, which must not end with a trailing
-- path separator.
--
-- Throws: 'PathParseException'
--
parseAbsNoTPS :: MonadThrow m
=> FilePath -> m (Path Abs NoTPS)
parseAbsNoTPS filepath =
if FilePath.isAbsolute filepath &&
not (FilePath.hasTrailingPathSeparator filepath) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeNoTPS filepath)) &&
FilePath.isValid filepath
then return (MkPath (normalizeNoTPS filepath))
else throwM (InvalidAbsNoTPS filepath)
-- | Get a location for a relative path, which must not end with a trailing
-- path separator.
--
-- Note that @filepath@ may contain any number of @./@ but may not contain a
-- single @..@ anywhere.
--
-- Throws: 'PathParseException'
--
parseRelNoTPS :: MonadThrow m
=> FilePath -> m (Path Rel NoTPS)
parseRelNoTPS filepath =
if not (FilePath.isAbsolute filepath ||
FilePath.hasTrailingPathSeparator filepath) &&
not (null filepath) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeNoTPS filepath)) &&
filepath /= "." && filepath /= ".." &&
FilePath.isValid filepath
then return (MkPath (normalizeNoTPS filepath))
else throwM (InvalidRelNoTPS filepath)
-- | Get a location for an absolute path that may or may not end in a trailing
-- path separator. Use `toTPS` and `toNoTPS` if that guarantee is required.
--
-- Throws: 'PathParseException'
--
parseAbsMaybeTPS :: MonadThrow m
=> FilePath -> m (Path Abs MaybeTPS)
parseAbsMaybeTPS filepath =
if FilePath.isAbsolute filepath &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeNoTPS filepath)) &&
FilePath.isValid filepath
then return (MkPath (normalizeNoTPS filepath))
else throwM (InvalidAbsMaybeTPS filepath)
-- | Get a location for a relative path that may or may not end in a trailing
-- path separator. Use `toTPS` and `toNoTPS` if that guarantee is required.
--
-- Note that @filepath@ may contain any number of @./@ but may not contain a
-- single @..@ anywhere.
--
-- Throws: 'PathParseException'
--
parseRelMaybeTPS :: MonadThrow m
=> FilePath -> m (Path Rel MaybeTPS)
parseRelMaybeTPS filepath =
if not (FilePath.isAbsolute filepath) &&
not (null filepath) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeNoTPS filepath)) &&
filepath /= "." && filepath /= ".." &&
FilePath.isValid filepath
then return (MkPath (normalizeNoTPS filepath))
else throwM (InvalidRelMaybeTPS filepath)
-- | Helper function: check if the filepath has any parent directories in it.
-- This handles the logic of checking for different path separators on Windows.
hasParentDir :: FilePath -> Bool
hasParentDir filepath' =
("/.." `isSuffixOf` filepath) ||
("/../" `isInfixOf` filepath) ||
("../" `isPrefixOf` filepath)
where
filepath =
case FilePath.pathSeparator of
'/' -> filepath'
x -> map (\y -> if x == y then '/' else y) filepath'
--------------------------------------------------------------------------------
-- Constructors
-- | Make a 'Path Abs TPS'.
--
-- 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).
mkAbsTPS :: FilePath -> Q Exp
mkAbsTPS s =
case parseAbsTPS s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Abs TPS|]
-- | Make a 'Path Rel TPS'.
mkRelTPS :: FilePath -> Q Exp
mkRelTPS s =
case parseRelTPS s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Rel TPS|]
-- | Make a 'Path Abs NoTPS'.
--
-- 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).
mkAbsNoTPS :: FilePath -> Q Exp
mkAbsNoTPS s =
case parseAbsNoTPS s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Abs NoTPS|]
-- | Make a 'Path Rel NoTPS'.
mkRelNoTPS :: FilePath -> Q Exp
mkRelNoTPS s =
case parseRelNoTPS s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Rel NoTPS|]
-- | Make a 'Path Rel MaybeTPS'.
mkAbsMaybeTPS :: FilePath -> Q Exp
mkAbsMaybeTPS s =
case parseAbsMaybeTPS s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Abs MaybeTPS|]
-- | Make a 'Path Rel MaybeTPS'.
mkRelMaybeTPS :: FilePath -> Q Exp
mkRelMaybeTPS s =
case parseRelMaybeTPS s of
Left err -> error (show err)
Right (MkPath str) ->
[|MkPath $(return (LitE (StringL str))) :: Path Rel MaybeTPS|]
--------------------------------------------------------------------------------
-- Conversion
-- | Convert to a 'FilePath' type.
--
-- All TPS data types have a trailing slash, so if you want no trailing
-- slash, you can use 'System.FilePath.dropTrailingPathSeparator' from
-- the filepath package.
toFilePath :: Path b t -> FilePath
toFilePath (MkPath l) = l
fromAbsTPS :: Path Abs TPS -> FilePath
fromAbsTPS = toFilePath
fromRelTPS :: Path Rel TPS -> FilePath
fromRelTPS = toFilePath
fromAbsNoTPS :: Path Abs NoTPS -> FilePath
fromAbsNoTPS = toFilePath
fromRelNoTPS :: Path Rel NoTPS -> FilePath
fromRelNoTPS = toFilePath
fromAbsMaybeTPS :: Path Abs MaybeTPS -> FilePath
fromAbsMaybeTPS = toFilePath
fromRelMaybeTPS :: Path Rel MaybeTPS -> FilePath
fromRelMaybeTPS = toFilePath
toTPS :: Path b MaybeTPS -> Path b TPS
toTPS (MkPath l) = MkPath (FilePath.addTrailingPathSeparator l)
toNoTPS :: Path b MaybeTPS -> Path b NoTPS
toNoTPS (MkPath l) = MkPath (FilePath.dropTrailingPathSeparator l)
--------------------------------------------------------------------------------
-- Operations
-- | Append two paths.
--
-- The second argument must always be a relative path, which ensures
-- that undefinable things like `"/abc" </> "/def"` cannot happen.
--
-- Technically, the first argument can be a path that points to a non-directory,
-- because this library is IO-agnostic and makes no assumptions about
-- file types.
(</>) :: Path b t1 -> Path Rel t2 -> Path b t2
(</>) (MkPath a) (MkPath b) = MkPath (a' ++ b)
where
a' = FilePath.addTrailingPathSeparator a
-- | Strip directory from path, making it relative to that directory.
-- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
--
-- The bases must match.
--
stripDir :: MonadThrow m
=> Path b t1 -> Path b t2 -> m (Path Rel t2)
stripDir (MkPath p) (MkPath l) =
case stripPrefix p' l of
Nothing -> throwM (Couldn'tStripPrefixTPS p' l)
Just "" -> throwM (Couldn'tStripPrefixTPS p' l)
Just ok -> return (MkPath ok)
where
p' = FilePath.addTrailingPathSeparator p
-- | Is p a parent of the given location? Implemented in terms of
-- 'stripDir'. The bases must match.
isParentOf :: Path b t1 -> Path b t2 -> Bool
isParentOf p l =
isJust (stripDir p l)
-- | Extract the directory name of a path.
--
-- The following properties hold:
--
-- @dirname (p \<\/> a) == dirname p@
--
dirname :: Path Abs t -> Path Abs TPS
dirname (MkPath fp) = MkPath (normalizeTPS (FilePath.takeDirectory $ FilePath.dropTrailingPathSeparator fp))
-- | Extract the file part of a path.
--
-- Throws InvalidRelTPS if it's passed e.g. '/', because there is no
-- basename for that and it would break the `Path Rel t` type.
--
-- The following properties hold:
--
-- @basename (p \<\/> a) == basename a@
--
basename :: MonadThrow m => Path b t -> m (Path Rel t)
basename (MkPath l)
| not (FilePath.isAbsolute rl) = return $ MkPath rl
| otherwise = throwM (InvalidRelTPS rl)
where
rl = case FilePath.hasTrailingPathSeparator l of
True -> last (FilePath.splitPath l)
False -> normalizeNoTPS (FilePath.takeFileName l)
--------------------------------------------------------------------------------
-- Internal functions
-- | Internal use for normalizing a path while always adding
-- a trailing path separator.
normalizeTPS :: FilePath -> FilePath
normalizeTPS =
clean . FilePath.addTrailingPathSeparator . FilePath.normalise
where clean "./" = ""
clean ('/':'/':xs) = clean ('/':xs)
clean x = x
-- | Internal use for normalizing a path without adding or removing
-- a trailing path separator.
normalizeNoTPS :: FilePath -> FilePath
normalizeNoTPS =
clean . FilePath.normalise
where clean "./" = ""
clean ('/':'/':xs) = clean ('/':xs)
clean x = x

View File

@ -2,7 +2,7 @@
-- | Internal types and functions. -- | Internal types and functions.
module Path.Internal module HPath.Internal
(Path(..)) (Path(..))
where where
@ -13,12 +13,12 @@ import Data.Data
-- --
-- Internally is a string. The string can be of two formats only: -- Internally is a string. The string can be of two formats only:
-- --
-- 1. File format: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@ -- 1. without trailing path separator: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
-- 2. Directory format: @foo\/@, @\/foo\/bar\/@ -- 2. with trailing path separator: @foo\/@, @\/foo\/bar\/@
-- --
-- All directories end in a trailing separator. There are no duplicate -- There are no duplicate
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc. -- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
newtype Path b t = Path FilePath data Path b t = MkPath FilePath
deriving (Typeable) deriving (Typeable)
-- | String equality. -- | String equality.
@ -27,7 +27,7 @@ newtype Path b t = Path FilePath
-- --
-- @show x == show y ≡ x == y@ -- @show x == show y ≡ x == y@
instance Eq (Path b t) where instance Eq (Path b t) where
(==) (Path x) (Path y) = x == y (==) (MkPath x) (MkPath y) = x == y
-- | String ordering. -- | String ordering.
-- --
@ -35,7 +35,7 @@ instance Eq (Path b t) where
-- --
-- @show x \`compare\` show y ≡ x \`compare\` y@ -- @show x \`compare\` show y ≡ x \`compare\` y@
instance Ord (Path b t) where instance Ord (Path b t) where
compare (Path x) (Path y) = compare x y compare (MkPath x) (MkPath y) = compare x y
-- | Same as 'Path.toFilePath'. -- | Same as 'Path.toFilePath'.
-- --
@ -43,7 +43,8 @@ instance Ord (Path b t) where
-- --
-- @x == y ≡ show x == show y@ -- @x == y ≡ show x == show y@
instance Show (Path b t) where instance Show (Path b t) where
show (Path x) = show x show (MkPath x) = show x
instance NFData (Path b t) where instance NFData (Path b t) where
rnf (Path x) = rnf x rnf (MkPath x) = rnf x

View File

@ -1,350 +0,0 @@
-- |
-- Module : Path
-- Copyright : © 20152016 FP Complete
-- License : BSD 3 clause
--
-- Maintainer : Chris Done <chrisdone@fpcomplete.com>
-- Stability : experimental
-- Portability : portable
--
-- Support for well-typed paths.
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE EmptyDataDecls #-}
module Path
(-- * Types
Path
,Abs
,Rel
,File
,Dir
-- * Parsing
,parseAbsDir
,parseRelDir
,parseAbsFile
,parseRelFile
,PathParseException
-- * Constructors
,mkAbsDir
,mkRelDir
,mkAbsFile
,mkRelFile
-- * Operations
,(</>)
,stripDir
,isParentOf
,parent
,filename
,dirname
-- * Conversion
,toFilePath
,fromAbsDir
,fromRelDir
,fromAbsFile
,fromRelFile
)
where
import Control.Exception (Exception)
import Control.Monad.Catch (MonadThrow(..))
import Data.Data
import Data.List
import Data.Maybe
import Language.Haskell.TH
import Path.Internal
import qualified System.FilePath as FilePath
--------------------------------------------------------------------------------
-- Types
-- | An absolute path.
data Abs deriving (Typeable)
-- | A relative path; one without a root.
data Rel deriving (Typeable)
-- | A file path.
data File deriving (Typeable)
-- | A directory path.
data Dir deriving (Typeable)
-- | Exception when parsing a location.
data PathParseException
= InvalidAbsDir FilePath
| InvalidRelDir FilePath
| InvalidAbsFile FilePath
| InvalidRelFile FilePath
| Couldn'tStripPrefixDir FilePath FilePath
deriving (Show,Typeable)
instance Exception PathParseException
--------------------------------------------------------------------------------
-- Parsers
-- | Get a location for an absolute directory. Produces a normalized
-- path which always ends in a path separator.
--
-- Throws: 'PathParseException'
--
parseAbsDir :: MonadThrow m
=> FilePath -> m (Path Abs Dir)
parseAbsDir filepath =
if FilePath.isAbsolute filepath &&
not (null (normalizeDir filepath)) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
FilePath.isValid filepath
then return (Path (normalizeDir filepath))
else throwM (InvalidAbsDir filepath)
-- | Get a location for a relative directory. Produces a normalized
-- path which always ends in a path separator.
--
-- Note that @filepath@ may contain any number of @./@ but may not consist solely of @./@. It also may not contain a single @..@ anywhere.
--
-- Throws: 'PathParseException'
--
parseRelDir :: MonadThrow m
=> FilePath -> m (Path Rel Dir)
parseRelDir filepath =
if not (FilePath.isAbsolute filepath) &&
not (null filepath) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeDir filepath)) &&
filepath /= "." && filepath /= ".." &&
FilePath.isValid filepath
then return (Path (normalizeDir filepath))
else throwM (InvalidRelDir filepath)
-- | Get a location for an absolute file.
--
-- Throws: 'PathParseException'
--
parseAbsFile :: MonadThrow m
=> FilePath -> m (Path Abs File)
parseAbsFile filepath =
if FilePath.isAbsolute filepath &&
not (FilePath.hasTrailingPathSeparator filepath) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeFile filepath)) &&
FilePath.isValid filepath
then return (Path (normalizeFile filepath))
else throwM (InvalidAbsFile filepath)
-- | Get a location for a relative file.
--
-- Note that @filepath@ may contain any number of @./@ but may not contain a single @..@ anywhere.
--
-- Throws: 'PathParseException'
--
parseRelFile :: MonadThrow m
=> FilePath -> m (Path Rel File)
parseRelFile filepath =
if not (FilePath.isAbsolute filepath ||
FilePath.hasTrailingPathSeparator filepath) &&
not (null filepath) &&
not ("~/" `isPrefixOf` filepath) &&
not (hasParentDir filepath) &&
not (null (normalizeFile filepath)) &&
filepath /= "." && filepath /= ".." &&
FilePath.isValid filepath
then return (Path (normalizeFile filepath))
else throwM (InvalidRelFile filepath)
-- | Helper function: check if the filepath has any parent directories in it.
-- This handles the logic of checking for different path separators on Windows.
hasParentDir :: FilePath -> Bool
hasParentDir filepath' =
("/.." `isSuffixOf` filepath) ||
("/../" `isInfixOf` filepath) ||
("../" `isPrefixOf` filepath)
where
filepath =
case FilePath.pathSeparator of
'/' -> filepath'
x -> map (\y -> if x == y then '/' else y) filepath'
--------------------------------------------------------------------------------
-- 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|]
--------------------------------------------------------------------------------
-- Conversion
-- | Convert to a 'FilePath' type.
--
-- All directories have a trailing slash, so if you want no trailing
-- slash, you can use 'System.FilePath.dropTrailingPathSeparator' from
-- the filepath package.
toFilePath :: Path b t -> FilePath
toFilePath (Path l) = l
-- | 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
--------------------------------------------------------------------------------
-- 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 …)@
--
(</>) :: Path b Dir -> Path Rel t -> Path b t
(</>) (Path a) (Path b) = Path (a ++ b)
-- | Strip directory from path, making it relative to that directory.
-- Throws 'Couldn'tStripPrefixDir' if directory is not a parent of the path.
--
-- The following properties hold:
--
-- @stripDir x (x \<\/> y) = y@
--
-- Cases which are proven not possible:
--
-- @stripDir (a :: Path Abs …) (b :: Path Rel …)@
--
-- @stripDir (a :: Path Rel …) (b :: Path Abs …)@
--
-- In other words the bases must match.
--
stripDir :: MonadThrow m
=> Path b Dir -> Path b t -> m (Path Rel t)
stripDir (Path p) (Path l) =
case stripPrefix p l of
Nothing -> throwM (Couldn'tStripPrefixDir p l)
Just "" -> throwM (Couldn'tStripPrefixDir p l)
Just ok -> return (Path ok)
-- | Is p a parent of the given location? Implemented in terms of
-- 'stripDir'. The bases must match.
isParentOf :: Path b Dir -> Path b t -> Bool
isParentOf p l =
isJust (stripDir p l)
-- | Take the absolute parent directory from the absolute path.
--
-- The following properties hold:
--
-- @parent (x \<\/> y) == x@
--
-- On the root, getting the parent is idempotent:
--
-- @parent (parent \"\/\") = \"\/\"@
--
parent :: Path Abs t -> Path Abs Dir
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 (normalizeFile (FilePath.takeFileName l))
-- | Extract the last directory name of a path.
--
-- The following properties hold:
--
-- @dirname (p \<\/> a) == dirname a@
--
dirname :: Path b Dir -> Path Rel Dir
dirname (Path l) =
Path (last (FilePath.splitPath l))
--------------------------------------------------------------------------------
-- Internal functions
-- | Internal use for normalizing a directory.
normalizeDir :: FilePath -> FilePath
normalizeDir =
clean . FilePath.addTrailingPathSeparator . FilePath.normalise
where clean "./" = ""
clean ('/':'/':xs) = clean ('/':xs)
clean x = x
-- | Internal use for normalizing a fileectory.
normalizeFile :: FilePath -> FilePath
normalizeFile =
clean . FilePath.normalise
where clean "./" = ""
clean ('/':'/':xs) = clean ('/':xs)
clean x = x

View File

@ -8,8 +8,8 @@ import Control.Applicative
import Control.Monad import Control.Monad
import Data.Maybe import Data.Maybe
import Data.Monoid import Data.Monoid
import Path import HPath
import Path.Internal import HPath.Internal
import Test.Hspec import Test.Hspec
-- | Test suite entry point, returns exit failure if any test fails. -- | Test suite entry point, returns exit failure if any test fails.
@ -19,15 +19,15 @@ main = hspec spec
-- | Test suite. -- | Test suite.
spec :: Spec spec :: Spec
spec = spec =
do describe "Parsing: Path Abs Dir" parseAbsDirSpec do describe "Parsing: Path Abs Dir" parseAbsTPSSpec
describe "Parsing: Path Rel Dir" parseRelDirSpec describe "Parsing: Path Rel Dir" parseRelTPSSpec
describe "Parsing: Path Abs File" parseAbsFileSpec describe "Parsing: Path Abs File" parseAbsNoTPSSpec
describe "Parsing: Path Rel File" parseRelFileSpec describe "Parsing: Path Rel File" parseRelNoTPSSpec
describe "Operations: (</>)" operationAppend describe "Operations: (</>)" operationAppend
describe "Operations: stripDir" operationStripDir describe "Operations: stripDir" operationStripDir
describe "Operations: isParentOf" operationIsParentOf describe "Operations: isParentOf" operationIsParentOf
describe "Operations: parent" operationParent describe "Operations: dirname" operationDirname
describe "Operations: filename" operationFilename describe "Operations: basename" operationBasename
describe "Restrictions" restrictions describe "Restrictions" restrictions
-- | Restricting the input of any tricks. -- | Restricting the input of any tricks.
@ -44,107 +44,107 @@ restrictions =
parseFails "/foo/bar/.." parseFails "/foo/bar/.."
where parseFails x = where parseFails x =
it (show x ++ " should be rejected") it (show x ++ " should be rejected")
(isNothing (void (parseAbsDir x) <|> (isNothing (void (parseAbsTPS x) <|>
void (parseRelDir x) <|> void (parseRelTPS x) <|>
void (parseAbsFile x) <|> void (parseAbsNoTPS x) <|>
void (parseRelFile x))) void (parseRelNoTPS x)))
-- | The 'filename' operation. -- | The 'basename' operation.
operationFilename :: Spec operationBasename :: Spec
operationFilename = operationBasename =
do it "filename ($(mkAbsDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)" do it "basename ($(mkAbsTPS parent) </> basename $(mkRelNoTPS filename)) == $(mkRelNoTPS filename)"
(filename ($(mkAbsDir "/home/chris/") </> ((basename =<< ($(mkAbsTPS "/home/hasufell/") </>)
filename $(mkRelFile "bar.txt")) == <$> basename $(mkRelNoTPS "bar.txt")) ==
$(mkRelFile "bar.txt")) Just $(mkRelNoTPS "bar.txt"))
it "filename ($(mkRelDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)" it "basename ($(mkRelTPS parent) </> basename $(mkRelNoTPS filename)) == $(mkRelNoTPS filename)"
(filename ($(mkRelDir "home/chris/") </> ((basename =<< ($(mkRelTPS "home/hasufell/") </>)
filename $(mkRelFile "bar.txt")) == <$> basename $(mkRelNoTPS "bar.txt")) ==
$(mkRelFile "bar.txt")) Just $(mkRelNoTPS "bar.txt"))
-- | The 'parent' operation. -- | The 'dirname' operation.
operationParent :: Spec operationDirname :: Spec
operationParent = operationDirname =
do it "parent (parent </> child) == parent" do it "dirname (parent </> child) == parent"
(parent ($(mkAbsDir "/foo") </> (dirname ($(mkAbsTPS "/foo") </>
$(mkRelDir "bar")) == $(mkRelTPS "bar")) ==
$(mkAbsDir "/foo")) $(mkAbsTPS "/foo"))
it "parent \"\" == \"\"" it "dirname \"\" == \"\""
(parent $(mkAbsDir "/") == (dirname $(mkAbsTPS "/") ==
$(mkAbsDir "/")) $(mkAbsTPS "/"))
it "parent (parent \"\") == \"\"" it "dirname (parent \"\") == \"\""
(parent (parent $(mkAbsDir "/")) == (dirname (dirname $(mkAbsTPS "/")) ==
$(mkAbsDir "/")) $(mkAbsTPS "/"))
-- | The 'isParentOf' operation. -- | The 'isParentOf' operation.
operationIsParentOf :: Spec operationIsParentOf :: Spec
operationIsParentOf = operationIsParentOf =
do it "isParentOf parent (parent </> child)" do it "isParentOf parent (parent </> child)"
(isParentOf (isParentOf
$(mkAbsDir "///bar/") $(mkAbsTPS "///bar/")
($(mkAbsDir "///bar/") </> ($(mkAbsTPS "///bar/") </>
$(mkRelFile "bar/foo.txt"))) $(mkRelNoTPS "bar/foo.txt")))
it "isParentOf parent (parent </> child)" it "isParentOf parent (parent </> child)"
(isParentOf (isParentOf
$(mkRelDir "bar/") $(mkRelTPS "bar/")
($(mkRelDir "bar/") </> ($(mkRelTPS "bar/") </>
$(mkRelFile "bob/foo.txt"))) $(mkRelNoTPS "bob/foo.txt")))
-- | The 'stripDir' operation. -- | The 'stripDir' operation.
operationStripDir :: Spec operationStripDir :: Spec
operationStripDir = operationStripDir =
do it "stripDir parent (parent </> child) = child" do it "stripDir parent (parent </> child) = child"
(stripDir $(mkAbsDir "///bar/") (stripDir $(mkAbsTPS "///bar/")
($(mkAbsDir "///bar/") </> ($(mkAbsTPS "///bar/") </>
$(mkRelFile "bar/foo.txt")) == $(mkRelNoTPS "bar/foo.txt")) ==
Just $(mkRelFile "bar/foo.txt")) Just $(mkRelNoTPS "bar/foo.txt"))
it "stripDir parent (parent </> child) = child" it "stripDir parent (parent </> child) = child"
(stripDir $(mkRelDir "bar/") (stripDir $(mkRelTPS "bar/")
($(mkRelDir "bar/") </> ($(mkRelTPS "bar/") </>
$(mkRelFile "bob/foo.txt")) == $(mkRelNoTPS "bob/foo.txt")) ==
Just $(mkRelFile "bob/foo.txt")) Just $(mkRelNoTPS "bob/foo.txt"))
it "stripDir parent parent = _|_" it "stripDir parent parent = _|_"
(stripDir $(mkAbsDir "/home/chris/foo") (stripDir $(mkAbsTPS "/home/hasufell/foo")
$(mkAbsDir "/home/chris/foo") == $(mkAbsTPS "/home/hasufell/foo") ==
Nothing) Nothing)
-- | The '</>' operation. -- | The '</>' operation.
operationAppend :: Spec operationAppend :: Spec
operationAppend = operationAppend =
do it "AbsDir + RelDir = AbsDir" do it "AbsDir + RelDir = AbsDir"
($(mkAbsDir "/home/") </> ($(mkAbsTPS "/home/") </>
$(mkRelDir "chris") == $(mkRelTPS "hasufell") ==
$(mkAbsDir "/home/chris/")) $(mkAbsTPS "/home/hasufell/"))
it "AbsDir + RelFile = AbsFile" it "AbsDir + RelFile = AbsFile"
($(mkAbsDir "/home/") </> ($(mkAbsTPS "/home/") </>
$(mkRelFile "chris/test.txt") == $(mkRelNoTPS "hasufell/test.txt") ==
$(mkAbsFile "/home/chris/test.txt")) $(mkAbsNoTPS "/home/hasufell/test.txt"))
it "RelDir + RelDir = RelDir" it "RelDir + RelDir = RelDir"
($(mkRelDir "home/") </> ($(mkRelTPS "home/") </>
$(mkRelDir "chris") == $(mkRelTPS "hasufell") ==
$(mkRelDir "home/chris")) $(mkRelTPS "home/hasufell"))
it "RelDir + RelFile = RelFile" it "RelDir + RelFile = RelFile"
($(mkRelDir "home/") </> ($(mkRelTPS "home/") </>
$(mkRelFile "chris/test.txt") == $(mkRelNoTPS "hasufell/test.txt") ==
$(mkRelFile "home/chris/test.txt")) $(mkRelNoTPS "home/hasufell/test.txt"))
-- | Tests for the tokenizer. -- | Tests for the tokenizer.
parseAbsDirSpec :: Spec parseAbsTPSSpec :: Spec
parseAbsDirSpec = parseAbsTPSSpec =
do failing "" do failing ""
failing "./" failing "./"
failing "~/" failing "~/"
failing "foo.txt" failing "foo.txt"
succeeding "/" (Path "/") succeeding "/" (MkPath "/")
succeeding "//" (Path "/") succeeding "//" (MkPath "/")
succeeding "///foo//bar//mu/" (Path "/foo/bar/mu/") succeeding "///foo//bar//mu/" (MkPath "/foo/bar/mu/")
succeeding "///foo//bar////mu" (Path "/foo/bar/mu/") succeeding "///foo//bar////mu" (MkPath "/foo/bar/mu/")
succeeding "///foo//bar/.//mu" (Path "/foo/bar/mu/") succeeding "///foo//bar/.//mu" (MkPath "/foo/bar/mu/")
where failing x = parserTest parseAbsDir x Nothing where failing x = parserTest parseAbsTPS x Nothing
succeeding x with = parserTest parseAbsDir x (Just with) succeeding x with = parserTest parseAbsTPS x (Just with)
-- | Tests for the tokenizer. -- | Tests for the tokenizer.
parseRelDirSpec :: Spec parseRelTPSSpec :: Spec
parseRelDirSpec = parseRelTPSSpec =
do failing "" do failing ""
failing "/" failing "/"
failing "//" failing "//"
@ -156,20 +156,20 @@ parseRelDirSpec =
failing "///foo//bar//mu/" failing "///foo//bar//mu/"
failing "///foo//bar////mu" failing "///foo//bar////mu"
failing "///foo//bar/.//mu" failing "///foo//bar/.//mu"
succeeding "..." (Path ".../") succeeding "..." (MkPath ".../")
succeeding "foo.bak" (Path "foo.bak/") succeeding "foo.bak" (MkPath "foo.bak/")
succeeding "./foo" (Path "foo/") succeeding "./foo" (MkPath "foo/")
succeeding "././foo" (Path "foo/") succeeding "././foo" (MkPath "foo/")
succeeding "./foo/./bar" (Path "foo/bar/") succeeding "./foo/./bar" (MkPath "foo/bar/")
succeeding "foo//bar//mu//" (Path "foo/bar/mu/") succeeding "foo//bar//mu//" (MkPath "foo/bar/mu/")
succeeding "foo//bar////mu" (Path "foo/bar/mu/") succeeding "foo//bar////mu" (MkPath "foo/bar/mu/")
succeeding "foo//bar/.//mu" (Path "foo/bar/mu/") succeeding "foo//bar/.//mu" (MkPath "foo/bar/mu/")
where failing x = parserTest parseRelDir x Nothing where failing x = parserTest parseRelTPS x Nothing
succeeding x with = parserTest parseRelDir x (Just with) succeeding x with = parserTest parseRelTPS x (Just with)
-- | Tests for the tokenizer. -- | Tests for the tokenizer.
parseAbsFileSpec :: Spec parseAbsNoTPSSpec :: Spec
parseAbsFileSpec = parseAbsNoTPSSpec =
do failing "" do failing ""
failing "./" failing "./"
failing "~/" failing "~/"
@ -177,16 +177,16 @@ parseAbsFileSpec =
failing "/" failing "/"
failing "//" failing "//"
failing "///foo//bar//mu/" failing "///foo//bar//mu/"
succeeding "/..." (Path "/...") succeeding "/..." (MkPath "/...")
succeeding "/foo.txt" (Path "/foo.txt") succeeding "/foo.txt" (MkPath "/foo.txt")
succeeding "///foo//bar////mu.txt" (Path "/foo/bar/mu.txt") succeeding "///foo//bar////mu.txt" (MkPath "/foo/bar/mu.txt")
succeeding "///foo//bar/.//mu.txt" (Path "/foo/bar/mu.txt") succeeding "///foo//bar/.//mu.txt" (MkPath "/foo/bar/mu.txt")
where failing x = parserTest parseAbsFile x Nothing where failing x = parserTest parseAbsNoTPS x Nothing
succeeding x with = parserTest parseAbsFile x (Just with) succeeding x with = parserTest parseAbsNoTPS x (Just with)
-- | Tests for the tokenizer. -- | Tests for the tokenizer.
parseRelFileSpec :: Spec parseRelNoTPSSpec :: Spec
parseRelFileSpec = parseRelNoTPSSpec =
do failing "" do failing ""
failing "/" failing "/"
failing "//" failing "//"
@ -197,16 +197,16 @@ parseRelFileSpec =
failing "///foo//bar//mu/" failing "///foo//bar//mu/"
failing "///foo//bar////mu" failing "///foo//bar////mu"
failing "///foo//bar/.//mu" failing "///foo//bar/.//mu"
succeeding "..." (Path "...") succeeding "..." (MkPath "...")
succeeding "foo.txt" (Path "foo.txt") succeeding "foo.txt" (MkPath "foo.txt")
succeeding "./foo.txt" (Path "foo.txt") succeeding "./foo.txt" (MkPath "foo.txt")
succeeding "././foo.txt" (Path "foo.txt") succeeding "././foo.txt" (MkPath "foo.txt")
succeeding "./foo/./bar.txt" (Path "foo/bar.txt") succeeding "./foo/./bar.txt" (MkPath "foo/bar.txt")
succeeding "foo//bar//mu.txt" (Path "foo/bar/mu.txt") succeeding "foo//bar//mu.txt" (MkPath "foo/bar/mu.txt")
succeeding "foo//bar////mu.txt" (Path "foo/bar/mu.txt") succeeding "foo//bar////mu.txt" (MkPath "foo/bar/mu.txt")
succeeding "foo//bar/.//mu.txt" (Path "foo/bar/mu.txt") succeeding "foo//bar/.//mu.txt" (MkPath "foo/bar/mu.txt")
where failing x = parserTest parseRelFile x Nothing where failing x = parserTest parseRelNoTPS x Nothing
succeeding x with = parserTest parseRelFile x (Just with) succeeding x with = parserTest parseRelNoTPS x (Just with)
-- | Parser test. -- | Parser test.
parserTest :: (Show a1,Show a,Eq a1) parserTest :: (Show a1,Show a,Eq a1)