Compare commits
2 Commits
dev
...
constructo
| Author | SHA1 | Date | |
|---|---|---|---|
| 29923d1023 | |||
| c3c96ed371 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,5 +8,3 @@ TAGS
|
|||||||
tags
|
tags
|
||||||
*.tag
|
*.tag
|
||||||
.stack-work/
|
.stack-work/
|
||||||
.cabal-sandbox/
|
|
||||||
cabal.sandbox.config
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
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:
|
||||||
|
|||||||
1
LICENSE
1
LICENSE
@@ -1,5 +1,4 @@
|
|||||||
Copyright (c) 2015–2016, FP Complete
|
Copyright (c) 2015–2016, 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
|
||||||
|
|||||||
538
README.md
538
README.md
@@ -1,42 +1,518 @@
|
|||||||
# HPath
|
# Path
|
||||||
|
|
||||||
Support for well-typed paths in Haskell. Also provides ByteString based filepath
|
Support for well-typed paths in Haskell.
|
||||||
manipulation.
|
|
||||||
|
* [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
|
||||||
|
|
||||||
The motivation came during development of
|
It was after working on a number of projects at FP Complete that use file
|
||||||
[hsfm](https://github.com/hasufell/hsfm)
|
paths in various ways. We used the system-filepath package, which was
|
||||||
which has a pretty strict File type, but lacks a strict Path type, e.g.
|
supposed to solve many path problems by being an opaque path type. It
|
||||||
for user input.
|
occurred to me that the same kind of bugs kept cropping up:
|
||||||
|
|
||||||
The library that came closest to my needs was
|
* Expected a path to be absolute but it was relative, or vice-versa.
|
||||||
[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.
|
|
||||||
|
|
||||||
Similarly, [posix-paths](https://github.com/JohnLato/posix-paths)
|
* Expected two equivalent paths to be equal or order the same, but they did
|
||||||
was exactly what I wanted for the low-level operations, but upstream seems dead,
|
not (`/home//foo` vs `/home/foo/` vs `/home/bar/../foo`, etc.).
|
||||||
so it is forked as well and merged into this library.
|
|
||||||
|
|
||||||
## Differences to 'path'
|
* Unpredictable behaviour with regards to concatenating paths.
|
||||||
|
|
||||||
* doesn't attempt to fake IO-related information into the path, so whether a path points to a file or directory is up to your IO-code to decide...
|
* Confusing files and directories.
|
||||||
* trailing path separators will be preserved if they exist, no messing with that
|
|
||||||
* uses safe ByteString for filepaths under the hood instead of unsafe String
|
|
||||||
* fixes broken [dirname](https://github.com/chrisdone/path/issues/18)
|
|
||||||
* renames dirname/filename to basename/dirname to match the POSIX shell functions
|
|
||||||
* introduces a new `Path Fn` for safe filename guarantees and a `RelC` class
|
|
||||||
* allows pattern matching via unidirectional PatternSynonym
|
|
||||||
* uses simple doctest for testing
|
|
||||||
* allows `~/` as relative path, because on posix level `~` is just a regular filename that does _NOT_ point to `$HOME`
|
|
||||||
* remove TH, it sucks
|
|
||||||
|
|
||||||
## Differences to 'posix-paths'
|
* Not knowing whether a path was a file or directory or relative or absolute
|
||||||
|
based on the type alone was a drag.
|
||||||
|
|
||||||
* `hasTrailingPathSeparator` behaves in the same way as `System.FilePath`
|
All of these bugs are preventable.
|
||||||
* `dropTrailingPathSeparator` behaves in the same way as `System.FilePath`
|
|
||||||
* added various functions like `isValid`, `normalise` and `equalFilePath`
|
|
||||||
* uses the `word8` package for save word8 literals instead of `OverloadedStrings`
|
|
||||||
* has custom versions of `openFd` and `getDirectoryContents`
|
|
||||||
|
|
||||||
|
## 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, it’s 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 would’ve just
|
||||||
|
been accepted. So I don’t think having a class is a good idea. Being
|
||||||
|
explicit here doesn’t 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 I’m in pure code, and if I’m in `IO`,
|
||||||
|
and I don’t expect parsing to ever fail, I can use it in IO like this:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
do x <- parseRelFile (fromCabalFileName x)
|
||||||
|
foo x
|
||||||
|
…
|
||||||
|
```
|
||||||
|
|
||||||
|
That’s 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
|
||||||
|
|
||||||
|
Let’s review my initial list of complaints and see if they’ve been
|
||||||
|
satisfied.
|
||||||
|
|
||||||
|
### Relative vs absolute confusion
|
||||||
|
|
||||||
|
Paths now distinguish in the type system whether they are relative or
|
||||||
|
absolute. You can’t 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 I’ll probably need a parent to append to it at some
|
||||||
|
point.
|
||||||
|
|
||||||
|
## In practice
|
||||||
|
|
||||||
|
We’ve been using this at FP Complete in a number of packages for some months
|
||||||
|
now, it’s turned out surprisingly sufficient for most of our path work with
|
||||||
|
only one bug found. We weren’t sure initially whether it would just be too
|
||||||
|
much of a pain to use, but really it’s 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, we’re 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. It’ll
|
||||||
|
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 doesn’t 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 we’re 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 doesn’t do
|
||||||
|
anything else.
|
||||||
|
|
||||||
|
The [`directory-tree`](http://hackage.haskell.org/package/directory-tree)
|
||||||
|
package contains a sum type of dir/file/etc but doesn’t 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 isn’t 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 don’t 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
|
||||||
|
```
|
||||||
|
|
||||||
|
I’m 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, it’s 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 it’s 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.
|
||||||
|
|
||||||
|
* It’s 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 there’s not much to complain
|
||||||
|
about. It’s a fine library, but I didn’t feel the need to drop my own in
|
||||||
|
favor of it. Check it out and decide for yourself.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
There’s a growing interest in making practical use of well-typed file path
|
||||||
|
handling. I think everyone’s wanted it for a while, but few people have
|
||||||
|
really committed to it in practice. Now that I’ve been using `path` for a
|
||||||
|
while, I can’t really go back. It’ll be interesting to see what new packages
|
||||||
|
crop up in the coming year, I expect there’ll be more.
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# LANGUAGE TupleSections #-}
|
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
|
||||||
|
|
||||||
{-# OPTIONS_GHC -Wall #-}
|
|
||||||
import Control.Applicative
|
|
||||||
import Control.Monad
|
|
||||||
import System.Directory
|
|
||||||
import System.FilePath ((</>))
|
|
||||||
import System.Posix.ByteString.FilePath
|
|
||||||
import System.Posix.Directory.ByteString as PosixBS
|
|
||||||
import System.Posix.Directory.Traversals
|
|
||||||
import qualified System.Posix.FilePath as PosixBS
|
|
||||||
import System.Posix.Files.ByteString
|
|
||||||
|
|
||||||
import Control.Exception
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
|
||||||
|
|
||||||
import System.Environment (getArgs, withArgs)
|
|
||||||
import System.IO.Error
|
|
||||||
import System.IO.Unsafe
|
|
||||||
import System.Process (system)
|
|
||||||
import Criterion.Main
|
|
||||||
|
|
||||||
|
|
||||||
-- | Based on code from 'Real World Haskell', at
|
|
||||||
-- http://book.realworldhaskell.org/read/io-case-study-a-library-for-searching-the-filesystem.html#id620419
|
|
||||||
listFilesRecursive :: FilePath -> IO [FilePath]
|
|
||||||
listFilesRecursive topdir = do
|
|
||||||
names <- System.Directory.getDirectoryContents topdir
|
|
||||||
let properNames = filter (`notElem` [".", ".."]) names
|
|
||||||
paths <- forM properNames $ \name -> do
|
|
||||||
let path = topdir </> name
|
|
||||||
isDir <- doesDirectoryExist path
|
|
||||||
if isDir
|
|
||||||
then listFilesRecursive path
|
|
||||||
else return [path]
|
|
||||||
return (topdir : concat paths)
|
|
||||||
|
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
getDirectoryContentsBS :: RawFilePath -> IO [RawFilePath]
|
|
||||||
getDirectoryContentsBS path =
|
|
||||||
modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
|
|
||||||
(`ioeSetLocation` "getDirectoryContentsBS")) $ do
|
|
||||||
bracket
|
|
||||||
(PosixBS.openDirStream path)
|
|
||||||
PosixBS.closeDirStream
|
|
||||||
loop
|
|
||||||
where
|
|
||||||
loop dirp = do
|
|
||||||
e <- PosixBS.readDirStream dirp
|
|
||||||
if BS.null e then return [] else do
|
|
||||||
es <- loop dirp
|
|
||||||
return (e:es)
|
|
||||||
|
|
||||||
|
|
||||||
-- | similar to 'listFilesRecursive, but uses RawFilePaths
|
|
||||||
listFilesRecursiveBS :: RawFilePath -> IO [RawFilePath]
|
|
||||||
listFilesRecursiveBS topdir = do
|
|
||||||
names <- getDirectoryContentsBS topdir
|
|
||||||
let properNames = filter (`notElem` [".", ".."]) names
|
|
||||||
paths <- forM properNames $ \name -> unsafeInterleaveIO $ do
|
|
||||||
let path = PosixBS.combine topdir name
|
|
||||||
isDir <- isDirectory <$> getFileStatus path
|
|
||||||
if isDir
|
|
||||||
then listFilesRecursiveBS path
|
|
||||||
else return [path]
|
|
||||||
return (topdir : concat paths)
|
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
benchTraverse :: RawFilePath -> IO ()
|
|
||||||
benchTraverse = traverseDirectory (\() p -> BS.putStrLn p) ()
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
args <- getArgs
|
|
||||||
let (d,otherArgs) = case args of
|
|
||||||
[] -> ("/usr/local",[])
|
|
||||||
x:xs -> (x,xs)
|
|
||||||
withArgs otherArgs $ defaultMain
|
|
||||||
[ bench "traverse (FilePath)" $ nfIO $ listFilesRecursive d >>= mapM_ putStrLn
|
|
||||||
, bench "traverse (RawFilePath)" $ nfIO $ listFilesRecursiveBS (BS.pack d) >>= mapM_ BS.putStrLn
|
|
||||||
, bench "allDirectoryContents" $ nfIO $ allDirectoryContents (BS.pack d) >>= mapM_ BS.putStrLn
|
|
||||||
, bench "allDirectoryContents'" $ nfIO $ allDirectoryContents' (BS.pack d) >>= mapM_ BS.putStrLn
|
|
||||||
, bench "traverseDirectory" $ nfIO $ benchTraverse (BS.pack d)
|
|
||||||
, bench "unix find" $ nfIO $ void $ system ("find " ++ d)
|
|
||||||
]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#include "dirutils.h"
|
|
||||||
unsigned int
|
|
||||||
__posixdir_d_type(struct dirent* d)
|
|
||||||
{
|
|
||||||
return(d -> d_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#ifndef POSIXPATHS_CBITS_DIRUTILS_H
|
|
||||||
#define POSIXPATHS_CBITS_DIRUTILS_H
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
|
|
||||||
extern unsigned int
|
|
||||||
__posixdir_d_type(struct dirent* d)
|
|
||||||
;
|
|
||||||
#endif
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Main where
|
|
||||||
|
|
||||||
import Control.Applicative
|
|
||||||
|
|
||||||
import Test.DocTest
|
|
||||||
import Test.HUnit
|
|
||||||
|
|
||||||
main =
|
|
||||||
doctest
|
|
||||||
["-isrc"
|
|
||||||
, "-XOverloadedStrings"
|
|
||||||
, "src/HPath.hs"
|
|
||||||
]
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Main where
|
|
||||||
|
|
||||||
import Control.Applicative
|
|
||||||
import System.Posix.Directory.Traversals
|
|
||||||
|
|
||||||
import Test.DocTest
|
|
||||||
import Test.HUnit
|
|
||||||
|
|
||||||
main = do
|
|
||||||
doctest
|
|
||||||
[ "-isrc"
|
|
||||||
, "-XOverloadedStrings"
|
|
||||||
, "System.Posix.FilePath"
|
|
||||||
]
|
|
||||||
runTestTT unitTests
|
|
||||||
|
|
||||||
|
|
||||||
unitTests :: Test
|
|
||||||
unitTests = test
|
|
||||||
[ TestCase $ do
|
|
||||||
r <- (==) <$> allDirectoryContents "." <*> allDirectoryContents' "."
|
|
||||||
assertBool "allDirectoryContents == allDirectoryContents'" r
|
|
||||||
]
|
|
||||||
83
hpath.cabal
83
hpath.cabal
@@ -1,83 +0,0 @@
|
|||||||
name: hpath
|
|
||||||
version: 0.5.8
|
|
||||||
synopsis: Support for well-typed paths
|
|
||||||
description: Support for will-typed paths.
|
|
||||||
license: BSD3
|
|
||||||
license-file: LICENSE
|
|
||||||
author: Julian Ospald <hasufell@posteo.de>
|
|
||||||
maintainer: Julian Ospald <hasufell@posteo.de>
|
|
||||||
copyright: 2015–2016 FP Complete, Julian Ospald 2016
|
|
||||||
category: Filesystem
|
|
||||||
build-type: Simple
|
|
||||||
cabal-version: >=1.14
|
|
||||||
extra-source-files: README.md
|
|
||||||
CHANGELOG
|
|
||||||
cbits/dirutils.h
|
|
||||||
doctests.hs
|
|
||||||
benchmarks/*.hs
|
|
||||||
|
|
||||||
library
|
|
||||||
hs-source-dirs: src/
|
|
||||||
default-language: Haskell2010
|
|
||||||
ghc-options: -Wall -O2
|
|
||||||
c-sources: cbits/dirutils.c
|
|
||||||
exposed-modules: HPath,
|
|
||||||
HPath.Internal,
|
|
||||||
System.Posix.Directory.Foreign,
|
|
||||||
System.Posix.Directory.Traversals,
|
|
||||||
System.Posix.FilePath
|
|
||||||
build-depends: base >= 4.2 && <5
|
|
||||||
, bytestring >= 0.9.2.0
|
|
||||||
, deepseq
|
|
||||||
, exceptions
|
|
||||||
, hspec
|
|
||||||
, unix >= 2.5
|
|
||||||
, utf8-string
|
|
||||||
, word8
|
|
||||||
|
|
||||||
|
|
||||||
test-suite doctests-hpath
|
|
||||||
default-language: Haskell2010
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
ghc-options: -threaded
|
|
||||||
main-is: doctests-hpath.hs
|
|
||||||
build-depends: base
|
|
||||||
, HUnit
|
|
||||||
, QuickCheck
|
|
||||||
, doctest >= 0.8
|
|
||||||
, hpath
|
|
||||||
|
|
||||||
test-suite doctests-posix
|
|
||||||
default-language: Haskell2010
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
ghc-options: -threaded
|
|
||||||
main-is: doctests-posix.hs
|
|
||||||
build-depends: base,
|
|
||||||
bytestring,
|
|
||||||
unix,
|
|
||||||
hpath,
|
|
||||||
doctest >= 0.8,
|
|
||||||
HUnit,
|
|
||||||
QuickCheck
|
|
||||||
|
|
||||||
benchmark bench.hs
|
|
||||||
default-language: Haskell2010
|
|
||||||
type: exitcode-stdio-1.0
|
|
||||||
hs-source-dirs: benchmarks
|
|
||||||
main-is: Bench.hs
|
|
||||||
|
|
||||||
build-depends:
|
|
||||||
base,
|
|
||||||
hpath,
|
|
||||||
bytestring,
|
|
||||||
unix,
|
|
||||||
directory >= 1.1 && < 1.3,
|
|
||||||
filepath >= 1.2 && < 1.4,
|
|
||||||
process >= 1.0 && < 1.3,
|
|
||||||
criterion >= 0.6 && < 0.9
|
|
||||||
ghc-options: -O2
|
|
||||||
|
|
||||||
source-repository head
|
|
||||||
type: git
|
|
||||||
location: https://github.com/hasufell/hpath
|
|
||||||
|
|
||||||
37
path.cabal
Normal file
37
path.cabal
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: path
|
||||||
|
version: 0.5.7
|
||||||
|
synopsis: Support for well-typed paths
|
||||||
|
description: Support for will-typed paths.
|
||||||
|
license: BSD3
|
||||||
|
license-file: LICENSE
|
||||||
|
author: Chris Done <chrisdone@fpcomplete.com>
|
||||||
|
maintainer: Chris Done <chrisdone@fpcomplete.com>
|
||||||
|
copyright: 2015–2016 FP Complete
|
||||||
|
category: Filesystem
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.8
|
||||||
|
extra-source-files: README.md, CHANGELOG
|
||||||
|
|
||||||
|
library
|
||||||
|
hs-source-dirs: src/
|
||||||
|
ghc-options: -Wall -O2
|
||||||
|
exposed-modules: Path, Path.Internal
|
||||||
|
build-depends: base >= 4 && <5
|
||||||
|
, exceptions
|
||||||
|
, filepath
|
||||||
|
, template-haskell
|
||||||
|
, deepseq
|
||||||
|
|
||||||
|
test-suite test
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
main-is: Main.hs
|
||||||
|
hs-source-dirs: test
|
||||||
|
build-depends: HUnit
|
||||||
|
, base
|
||||||
|
, hspec
|
||||||
|
, mtl
|
||||||
|
, path
|
||||||
|
|
||||||
|
source-repository head
|
||||||
|
type: git
|
||||||
|
location: https://github.com/chrisdone/path.git
|
||||||
492
src/HPath.hs
492
src/HPath.hs
@@ -1,492 +0,0 @@
|
|||||||
-- |
|
|
||||||
-- Module : HPath
|
|
||||||
-- Copyright : © 2015–2016 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 CPP #-}
|
|
||||||
{-# LANGUAGE DeriveDataTypeable #-}
|
|
||||||
{-# LANGUAGE EmptyDataDecls #-}
|
|
||||||
{-# LANGUAGE PatternSynonyms #-}
|
|
||||||
{-# OPTIONS_HADDOCK ignore-exports #-}
|
|
||||||
|
|
||||||
module HPath
|
|
||||||
(
|
|
||||||
-- * Types
|
|
||||||
Abs
|
|
||||||
,Path
|
|
||||||
,Rel
|
|
||||||
,Fn
|
|
||||||
,PathParseException
|
|
||||||
-- * PatternSynonyms/ViewPatterns
|
|
||||||
,pattern Path
|
|
||||||
-- * Path Parsing
|
|
||||||
,parseAbs
|
|
||||||
,parseFn
|
|
||||||
,parseRel
|
|
||||||
-- * Path Conversion
|
|
||||||
,canonicalizePath
|
|
||||||
,fromAbs
|
|
||||||
,fromRel
|
|
||||||
,normalize
|
|
||||||
,toFilePath
|
|
||||||
-- * Path Operations
|
|
||||||
,(</>)
|
|
||||||
,basename
|
|
||||||
,dirname
|
|
||||||
,isParentOf
|
|
||||||
,getAllParents
|
|
||||||
,stripDir
|
|
||||||
-- * Path IO helpers
|
|
||||||
,withAbsPath
|
|
||||||
,withRelPath
|
|
||||||
,withFnPath
|
|
||||||
-- * ByteString/Word8 constants
|
|
||||||
,nullByte
|
|
||||||
,pathDot
|
|
||||||
,pathDot'
|
|
||||||
,pathSeparator'
|
|
||||||
-- * ByteString operations
|
|
||||||
,fpToString
|
|
||||||
,userStringToFP
|
|
||||||
-- * ByteString Query functions
|
|
||||||
,hiddenFile
|
|
||||||
-- * Queries
|
|
||||||
,hasParentDir
|
|
||||||
,isFileName
|
|
||||||
-- * String based functions
|
|
||||||
)
|
|
||||||
where
|
|
||||||
|
|
||||||
import Control.Exception (Exception)
|
|
||||||
import Control.Monad.Catch (MonadThrow(..))
|
|
||||||
import Data.ByteString(ByteString)
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import Data.ByteString.UTF8 (fromString, toString)
|
|
||||||
import Data.Data
|
|
||||||
import qualified Data.List as L
|
|
||||||
import Data.Maybe
|
|
||||||
import Data.Word8
|
|
||||||
import HPath.Internal
|
|
||||||
import System.Posix.FilePath hiding ((</>))
|
|
||||||
import System.Posix.Directory.Traversals(realpath)
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- Types
|
|
||||||
|
|
||||||
-- | An absolute path.
|
|
||||||
data Abs deriving (Typeable)
|
|
||||||
|
|
||||||
-- | A relative path; one without a root.
|
|
||||||
data Rel deriving (Typeable)
|
|
||||||
|
|
||||||
-- | A filename, without any '/'.
|
|
||||||
data Fn deriving (Typeable)
|
|
||||||
|
|
||||||
-- | Exception when parsing a location.
|
|
||||||
data PathParseException
|
|
||||||
= InvalidAbs ByteString
|
|
||||||
| InvalidRel ByteString
|
|
||||||
| InvalidFn ByteString
|
|
||||||
| Couldn'tStripPrefixTPS ByteString ByteString
|
|
||||||
deriving (Show,Typeable)
|
|
||||||
instance Exception PathParseException
|
|
||||||
|
|
||||||
data PathException = RootDirHasNoBasename
|
|
||||||
deriving (Show,Typeable)
|
|
||||||
instance Exception PathException
|
|
||||||
|
|
||||||
instance RelC Rel
|
|
||||||
instance RelC Fn
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- PatternSynonyms
|
|
||||||
|
|
||||||
pattern Path x <- (MkPath x)
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- Path Parsers
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- | Get a location for an absolute path. Produces a normalised path.
|
|
||||||
--
|
|
||||||
-- Throws: 'PathParseException'
|
|
||||||
--
|
|
||||||
-- >>> parseAbs "/abc" :: Maybe (Path Abs)
|
|
||||||
-- Just "/abc"
|
|
||||||
-- >>> parseAbs "/" :: Maybe (Path Abs)
|
|
||||||
-- Just "/"
|
|
||||||
-- >>> parseAbs "/abc/def" :: Maybe (Path Abs)
|
|
||||||
-- Just "/abc/def"
|
|
||||||
-- >>> parseAbs "/abc/def/.///" :: Maybe (Path Abs)
|
|
||||||
-- Just "/abc/def/"
|
|
||||||
-- >>> parseAbs "abc" :: Maybe (Path Abs)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseAbs "" :: Maybe (Path Abs)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseAbs "/abc/../foo" :: Maybe (Path Abs)
|
|
||||||
-- Nothing
|
|
||||||
parseAbs :: MonadThrow m
|
|
||||||
=> ByteString -> m (Path Abs)
|
|
||||||
parseAbs filepath =
|
|
||||||
if isAbsolute filepath &&
|
|
||||||
isValid filepath &&
|
|
||||||
not (hasParentDir filepath)
|
|
||||||
then return (MkPath $ normalise filepath)
|
|
||||||
else throwM (InvalidAbs filepath)
|
|
||||||
|
|
||||||
|
|
||||||
-- | Get a location for a relative path. Produces a normalised
|
|
||||||
-- path.
|
|
||||||
--
|
|
||||||
-- Note that @filepath@ may contain any number of @./@ but may not consist
|
|
||||||
-- solely of @./@. It also may not contain a single @..@ anywhere.
|
|
||||||
--
|
|
||||||
-- Throws: 'PathParseException'
|
|
||||||
--
|
|
||||||
-- >>> parseRel "abc" :: Maybe (Path Rel)
|
|
||||||
-- Just "abc"
|
|
||||||
-- >>> parseRel "def/" :: Maybe (Path Rel)
|
|
||||||
-- Just "def/"
|
|
||||||
-- >>> parseRel "abc/def" :: Maybe (Path Rel)
|
|
||||||
-- Just "abc/def"
|
|
||||||
-- >>> parseRel "abc/def/." :: Maybe (Path Rel)
|
|
||||||
-- Just "abc/def/"
|
|
||||||
-- >>> parseRel "/abc" :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseRel "" :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseRel "abc/../foo" :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseRel "." :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseRel ".." :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
parseRel :: MonadThrow m
|
|
||||||
=> ByteString -> m (Path Rel)
|
|
||||||
parseRel filepath =
|
|
||||||
if not (isAbsolute filepath) &&
|
|
||||||
filepath /= pathDot' && filepath /= pathDoubleDot &&
|
|
||||||
not (hasParentDir filepath) &&
|
|
||||||
isValid filepath
|
|
||||||
then return (MkPath $ normalise filepath)
|
|
||||||
else throwM (InvalidRel filepath)
|
|
||||||
|
|
||||||
|
|
||||||
-- | Parses a filename. Filenames must not contain slashes.
|
|
||||||
-- Excludes '.' and '..'.
|
|
||||||
--
|
|
||||||
-- Throws: 'PathParseException'
|
|
||||||
--
|
|
||||||
-- >>> parseFn "abc" :: Maybe (Path Fn)
|
|
||||||
-- Just "abc"
|
|
||||||
-- >>> parseFn "..." :: Maybe (Path Fn)
|
|
||||||
-- Just "..."
|
|
||||||
-- >>> parseFn "def/" :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseFn "abc/def" :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseFn "abc/def/." :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseFn "/abc" :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseFn "" :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseFn "abc/../foo" :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseFn "." :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> parseFn ".." :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
parseFn :: MonadThrow m
|
|
||||||
=> ByteString -> m (Path Fn)
|
|
||||||
parseFn filepath =
|
|
||||||
if isFileName filepath &&
|
|
||||||
filepath /= pathDot' && filepath /= pathDoubleDot &&
|
|
||||||
isValid filepath
|
|
||||||
then return (MkPath filepath)
|
|
||||||
else throwM (InvalidFn filepath)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- Path Conversion
|
|
||||||
|
|
||||||
-- | Convert to a ByteString type.
|
|
||||||
toFilePath :: Path b -> ByteString
|
|
||||||
toFilePath (MkPath l) = l
|
|
||||||
|
|
||||||
fromAbs :: Path Abs -> ByteString
|
|
||||||
fromAbs = toFilePath
|
|
||||||
|
|
||||||
fromRel :: RelC r => Path r -> ByteString
|
|
||||||
fromRel = toFilePath
|
|
||||||
|
|
||||||
normalize :: Path t -> Path t
|
|
||||||
normalize (MkPath l) = MkPath $ normalise l
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- Path 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.
|
|
||||||
--
|
|
||||||
-- >>> (MkPath "/") </> (MkPath "file" :: Path Rel)
|
|
||||||
-- "/file"
|
|
||||||
-- >>> (MkPath "/path/to") </> (MkPath "file" :: Path Rel)
|
|
||||||
-- "/path/to/file"
|
|
||||||
-- >>> (MkPath "/") </> (MkPath "file/lal" :: Path Rel)
|
|
||||||
-- "/file/lal"
|
|
||||||
-- >>> (MkPath "/") </> (MkPath "file/" :: Path Rel)
|
|
||||||
-- "/file/"
|
|
||||||
(</>) :: RelC r => Path b -> Path r -> Path b
|
|
||||||
(</>) (MkPath a) (MkPath b) = MkPath (a' `BS.append` b)
|
|
||||||
where
|
|
||||||
a' = if BS.last a == pathSeparator
|
|
||||||
then a
|
|
||||||
else 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.
|
|
||||||
--
|
|
||||||
-- >>> (MkPath "/lal/lad") `stripDir` (MkPath "/lal/lad/fad") :: Maybe (Path Rel)
|
|
||||||
-- Just "fad"
|
|
||||||
-- >>> (MkPath "lal/lad") `stripDir` (MkPath "lal/lad/fad") :: Maybe (Path Rel)
|
|
||||||
-- Just "fad"
|
|
||||||
-- >>> (MkPath "/") `stripDir` (MkPath "/") :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> (MkPath "/lal/lad/fad") `stripDir` (MkPath "/lal/lad") :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
-- >>> (MkPath "fad") `stripDir` (MkPath "fad") :: Maybe (Path Rel)
|
|
||||||
-- Nothing
|
|
||||||
stripDir :: MonadThrow m
|
|
||||||
=> Path b -> Path b -> m (Path Rel)
|
|
||||||
stripDir (MkPath p) (MkPath l) =
|
|
||||||
case stripPrefix p' l of
|
|
||||||
Nothing -> throwM (Couldn'tStripPrefixTPS p' l)
|
|
||||||
Just ok -> if BS.null ok
|
|
||||||
then throwM (Couldn'tStripPrefixTPS p' l)
|
|
||||||
else return (MkPath ok)
|
|
||||||
where
|
|
||||||
p' = addTrailingPathSeparator p
|
|
||||||
|
|
||||||
-- | Is p a parent of the given location? Implemented in terms of
|
|
||||||
-- 'stripDir'. The bases must match.
|
|
||||||
--
|
|
||||||
-- >>> (MkPath "/lal/lad") `isParentOf` (MkPath "/lal/lad/fad")
|
|
||||||
-- True
|
|
||||||
-- >>> (MkPath "lal/lad") `isParentOf` (MkPath "lal/lad/fad")
|
|
||||||
-- True
|
|
||||||
-- >>> (MkPath "/") `isParentOf` (MkPath "/")
|
|
||||||
-- False
|
|
||||||
-- >>> (MkPath "/lal/lad/fad") `isParentOf` (MkPath "/lal/lad")
|
|
||||||
-- False
|
|
||||||
-- >>> (MkPath "fad") `isParentOf` (MkPath "fad")
|
|
||||||
-- False
|
|
||||||
isParentOf :: Path b -> Path b -> Bool
|
|
||||||
isParentOf p l = isJust (stripDir p l :: Maybe (Path Rel))
|
|
||||||
|
|
||||||
|
|
||||||
-- |Get all parents of a path.
|
|
||||||
--
|
|
||||||
-- >>> getAllParents (MkPath "/abs/def/dod")
|
|
||||||
-- ["/abs/def","/abs","/"]
|
|
||||||
-- >>> getAllParents (MkPath "/")
|
|
||||||
-- []
|
|
||||||
getAllParents :: Path Abs -> [Path Abs]
|
|
||||||
getAllParents (MkPath p)
|
|
||||||
| np == pathSeparator' = []
|
|
||||||
| otherwise = dirname (MkPath np) : getAllParents (dirname $ MkPath np)
|
|
||||||
where
|
|
||||||
np = dropTrailingPathSeparator . normalise $ p
|
|
||||||
|
|
||||||
|
|
||||||
-- | Extract the directory name of a path.
|
|
||||||
--
|
|
||||||
-- The following properties hold:
|
|
||||||
--
|
|
||||||
-- @dirname (p \<\/> a) == dirname p@
|
|
||||||
--
|
|
||||||
-- >>> dirname (MkPath "/abc/def/dod")
|
|
||||||
-- "/abc/def"
|
|
||||||
-- >>> dirname (MkPath "/")
|
|
||||||
-- "/"
|
|
||||||
dirname :: Path Abs -> Path Abs
|
|
||||||
dirname (MkPath fp) = MkPath (takeDirectory $ dropTrailingPathSeparator fp)
|
|
||||||
|
|
||||||
-- | Extract the file part of a path.
|
|
||||||
--
|
|
||||||
--
|
|
||||||
-- The following properties hold:
|
|
||||||
--
|
|
||||||
-- @basename (p \<\/> a) == basename a@
|
|
||||||
--
|
|
||||||
-- Throws: `PathException` if given the root path "/"
|
|
||||||
--
|
|
||||||
-- >>> basename (MkPath "/abc/def/dod") :: Maybe (Path Fn)
|
|
||||||
-- Just "dod"
|
|
||||||
-- >>> basename (MkPath "/") :: Maybe (Path Fn)
|
|
||||||
-- Nothing
|
|
||||||
basename :: MonadThrow m => Path b -> m (Path Fn)
|
|
||||||
basename (MkPath l)
|
|
||||||
| not (isAbsolute rl) = return $ MkPath rl
|
|
||||||
| otherwise = throwM RootDirHasNoBasename
|
|
||||||
where
|
|
||||||
rl = last . splitPath . dropTrailingPathSeparator $ l
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- Path IO helpers
|
|
||||||
|
|
||||||
|
|
||||||
-- | May fail on `realpath`.
|
|
||||||
canonicalizePath :: Path Abs -> IO (Path Abs)
|
|
||||||
canonicalizePath (MkPath l) = do
|
|
||||||
nl <- realpath l
|
|
||||||
return $ MkPath nl
|
|
||||||
|
|
||||||
|
|
||||||
withAbsPath :: Path Abs -> (ByteString -> IO a) -> IO a
|
|
||||||
withAbsPath (MkPath p) action = action p
|
|
||||||
|
|
||||||
|
|
||||||
withRelPath :: Path Rel -> (ByteString -> IO a) -> IO a
|
|
||||||
withRelPath (MkPath p) action = action p
|
|
||||||
|
|
||||||
|
|
||||||
withFnPath :: Path Fn -> (ByteString -> IO a) -> IO a
|
|
||||||
withFnPath (MkPath p) action = action p
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- ByteString Query functions
|
|
||||||
|
|
||||||
|
|
||||||
-- | Whether the file is a hidden file.
|
|
||||||
--
|
|
||||||
-- >>> hiddenFile (MkPath ".foo")
|
|
||||||
-- True
|
|
||||||
-- >>> hiddenFile (MkPath "..foo.bar")
|
|
||||||
-- True
|
|
||||||
-- >>> hiddenFile (MkPath "...")
|
|
||||||
-- True
|
|
||||||
-- >>> hiddenFile (MkPath "dod")
|
|
||||||
-- False
|
|
||||||
-- >>> hiddenFile (MkPath "dod.bar")
|
|
||||||
-- False
|
|
||||||
hiddenFile :: Path Fn -> Bool
|
|
||||||
hiddenFile (MkPath fp)
|
|
||||||
| fp == pathDoubleDot = False
|
|
||||||
| fp == pathDot' = False
|
|
||||||
| otherwise = pathDot' `BS.isPrefixOf` fp
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- ByteString/Word8 constants
|
|
||||||
|
|
||||||
pathSeparator' :: ByteString
|
|
||||||
pathSeparator' = BS.singleton pathSeparator
|
|
||||||
|
|
||||||
|
|
||||||
pathDot :: Word8
|
|
||||||
pathDot = _period
|
|
||||||
|
|
||||||
|
|
||||||
pathDot' :: ByteString
|
|
||||||
pathDot' = BS.singleton pathDot
|
|
||||||
|
|
||||||
|
|
||||||
pathDoubleDot :: ByteString
|
|
||||||
pathDoubleDot = pathDot `BS.cons` pathDot'
|
|
||||||
|
|
||||||
|
|
||||||
nullByte :: Word8
|
|
||||||
nullByte = _nul
|
|
||||||
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- ByteString Operations
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- |Uses UTF-8 decoding to convert the bytestring into a String.
|
|
||||||
fpToString :: ByteString -> String
|
|
||||||
fpToString = toString
|
|
||||||
|
|
||||||
|
|
||||||
-- |Uses UTF-8 encoding to convert a user provided String into
|
|
||||||
-- a ByteString, which represents a filepath.
|
|
||||||
userStringToFP :: String -> ByteString
|
|
||||||
userStringToFP = fromString
|
|
||||||
|
|
||||||
|
|
||||||
#if MIN_VERSION_bytestring(0,10,8)
|
|
||||||
#else
|
|
||||||
stripPrefix :: ByteString -> ByteString -> Maybe ByteString
|
|
||||||
stripPrefix a b = BS.pack `fmap` L.stripPrefix (BS.unpack a) (BS.unpack b)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- ByteString Query functions
|
|
||||||
|
|
||||||
-- | Helper function: check if the filepath has any parent directories in it.
|
|
||||||
--
|
|
||||||
-- >>> hasParentDir "/.."
|
|
||||||
-- True
|
|
||||||
-- >>> hasParentDir "foo/bar/.."
|
|
||||||
-- True
|
|
||||||
-- >>> hasParentDir "foo/../bar/."
|
|
||||||
-- True
|
|
||||||
-- >>> hasParentDir "foo/bar"
|
|
||||||
-- False
|
|
||||||
-- >>> hasParentDir "foo"
|
|
||||||
-- False
|
|
||||||
-- >>> hasParentDir ""
|
|
||||||
-- False
|
|
||||||
-- >>> hasParentDir ".."
|
|
||||||
-- False
|
|
||||||
hasParentDir :: ByteString -> Bool
|
|
||||||
hasParentDir filepath =
|
|
||||||
((pathSeparator `BS.cons` pathDoubleDot) `BS.isSuffixOf` filepath) ||
|
|
||||||
((pathSeparator' `BS.append` pathDoubleDot `BS.append` pathSeparator')
|
|
||||||
`BS.isInfixOf` filepath) ||
|
|
||||||
((pathDoubleDot `BS.append` pathSeparator') `BS.isPrefixOf` filepath)
|
|
||||||
|
|
||||||
|
|
||||||
-- | Is the given filename a valid filename?
|
|
||||||
--
|
|
||||||
-- >>> isFileName "lal"
|
|
||||||
-- True
|
|
||||||
-- >>> isFileName "."
|
|
||||||
-- True
|
|
||||||
-- >>> isFileName ".."
|
|
||||||
-- True
|
|
||||||
-- >>> isFileName ""
|
|
||||||
-- False
|
|
||||||
-- >>> isFileName "\0"
|
|
||||||
-- False
|
|
||||||
-- >>> isFileName "/random_ path:*"
|
|
||||||
-- False
|
|
||||||
isFileName :: ByteString -> Bool
|
|
||||||
isFileName filepath =
|
|
||||||
not (pathSeparator' `BS.isInfixOf` filepath) &&
|
|
||||||
not (BS.null filepath) &&
|
|
||||||
not (nullByte `BS.elem` filepath)
|
|
||||||
|
|
||||||
336
src/Path.hs
Normal file
336
src/Path.hs
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
-- |
|
||||||
|
-- Module : Path
|
||||||
|
-- Copyright : © 2015–2016 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
|
||||||
|
-- * Parsing
|
||||||
|
,parseAbsDir
|
||||||
|
,parseRelDir
|
||||||
|
,parseAbsFile
|
||||||
|
,parseRelFile
|
||||||
|
,PathParseException
|
||||||
|
-- * Constructors
|
||||||
|
,mkAbsDir
|
||||||
|
,mkRelDir
|
||||||
|
,mkAbsFile
|
||||||
|
,mkRelFile
|
||||||
|
-- * Operations
|
||||||
|
,(</>)
|
||||||
|
,stripDir
|
||||||
|
,isParentOf
|
||||||
|
,parent
|
||||||
|
,filename
|
||||||
|
-- * Conversion
|
||||||
|
,toFilePath
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Exception when parsing a location.
|
||||||
|
data PathParseException
|
||||||
|
= InvalidAbsDir FilePath
|
||||||
|
| InvalidRelDir FilePath
|
||||||
|
| InvalidAbsFile FilePath
|
||||||
|
| InvalidRelFile FilePath
|
||||||
|
| Couldn'tStripPrefixDir FilePath FilePath
|
||||||
|
| InvalidTypeCombination
|
||||||
|
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)
|
||||||
|
parseAbsDir filepath =
|
||||||
|
if FilePath.isAbsolute filepath &&
|
||||||
|
not (null (normalizeDir filepath)) &&
|
||||||
|
not ("~/" `isPrefixOf` filepath) &&
|
||||||
|
not (hasParentDir filepath) &&
|
||||||
|
FilePath.isValid filepath
|
||||||
|
then return (DPath (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)
|
||||||
|
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 (DPath (normalizeDir filepath))
|
||||||
|
else throwM (InvalidRelDir filepath)
|
||||||
|
|
||||||
|
-- | Get a location for an absolute file.
|
||||||
|
--
|
||||||
|
-- Throws: 'PathParseException'
|
||||||
|
--
|
||||||
|
parseAbsFile :: MonadThrow m
|
||||||
|
=> FilePath -> m (Path Abs)
|
||||||
|
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 (FPath (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)
|
||||||
|
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 (FPath (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 (DPath str) ->
|
||||||
|
[|DPath $(return (LitE (StringL str))) :: Path Abs|]
|
||||||
|
_ -> error "Invalid Type"
|
||||||
|
|
||||||
|
-- | Make a 'Path Rel Dir'.
|
||||||
|
mkRelDir :: FilePath -> Q Exp
|
||||||
|
mkRelDir s =
|
||||||
|
case parseRelDir s of
|
||||||
|
Left err -> error (show err)
|
||||||
|
Right (DPath str) ->
|
||||||
|
[|DPath $(return (LitE (StringL str))) :: Path Rel|]
|
||||||
|
_ -> error "Invalid Type"
|
||||||
|
|
||||||
|
-- | 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 (FPath str) ->
|
||||||
|
[|FPath $(return (LitE (StringL str))) :: Path Abs|]
|
||||||
|
_ -> error "Invalid Type"
|
||||||
|
|
||||||
|
-- | Make a 'Path Rel File'.
|
||||||
|
mkRelFile :: FilePath -> Q Exp
|
||||||
|
mkRelFile s =
|
||||||
|
case parseRelFile s of
|
||||||
|
Left err -> error (show err)
|
||||||
|
Right (FPath str) ->
|
||||||
|
[|FPath $(return (LitE (StringL str))) :: Path Rel|]
|
||||||
|
_ -> error "Invalid Type"
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- 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 -> FilePath
|
||||||
|
toFilePath (FPath l) = l
|
||||||
|
toFilePath (DPath l) = l
|
||||||
|
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- 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 …)@
|
||||||
|
--
|
||||||
|
(</>) :: MonadThrow m => Path b -> Path Rel -> m (Path b)
|
||||||
|
(</>) (DPath a) (FPath b) = return $ FPath (a ++ b)
|
||||||
|
(</>) (DPath a) (DPath b) = return $ DPath (a ++ b)
|
||||||
|
(</>) _ _ = throwM InvalidTypeCombination
|
||||||
|
|
||||||
|
-- | 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 -> Path b -> m (Path Rel)
|
||||||
|
stripDir (DPath p) (FPath l) =
|
||||||
|
case stripPrefix p l of
|
||||||
|
Nothing -> throwM (Couldn'tStripPrefixDir p l)
|
||||||
|
Just "" -> throwM (Couldn'tStripPrefixDir p l)
|
||||||
|
Just ok -> return (FPath ok)
|
||||||
|
stripDir (DPath p) (DPath l) =
|
||||||
|
case stripPrefix p l of
|
||||||
|
Nothing -> throwM (Couldn'tStripPrefixDir p l)
|
||||||
|
Just "" -> throwM (Couldn'tStripPrefixDir p l)
|
||||||
|
Just ok -> return (DPath ok)
|
||||||
|
stripDir _ _ = throwM InvalidTypeCombination
|
||||||
|
|
||||||
|
|
||||||
|
-- | Is p a parent of the given location? Implemented in terms of
|
||||||
|
-- 'stripDir'. The bases must match.
|
||||||
|
-- Returns False if the first argument is not a directory path.
|
||||||
|
isParentOf :: Path b -> Path b -> Bool
|
||||||
|
isParentOf p@(DPath _) l@(DPath _) = isJust (stripDir p l)
|
||||||
|
isParentOf p@(DPath _) l@(FPath _) = isJust (stripDir p l)
|
||||||
|
isParentOf _ _ = False
|
||||||
|
|
||||||
|
-- | 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 -> Path Abs
|
||||||
|
parent (DPath fp) =
|
||||||
|
DPath (normalizeDir (FilePath.takeDirectory (FilePath.dropTrailingPathSeparator fp)))
|
||||||
|
parent (FPath fp) =
|
||||||
|
DPath (normalizeDir (FilePath.takeDirectory (FilePath.dropTrailingPathSeparator fp)))
|
||||||
|
|
||||||
|
-- | Extract the file/directory part of a path.
|
||||||
|
--
|
||||||
|
-- The following properties hold:
|
||||||
|
--
|
||||||
|
-- @filename (p \<\/> a) == filename a@
|
||||||
|
--
|
||||||
|
filename :: Path b -> Path Rel
|
||||||
|
filename (FPath l) =
|
||||||
|
FPath (normalizeFile (FilePath.takeFileName l))
|
||||||
|
filename (DPath l) =
|
||||||
|
DPath (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
|
||||||
|
|
||||||
@@ -1,26 +1,29 @@
|
|||||||
{-# LANGUAGE DeriveDataTypeable #-}
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
|
||||||
|
-- TODO: - remove undefined in Ord instance
|
||||||
|
-- - use viewpatterns/patternsynonyms so we don't need to
|
||||||
|
-- export the constructors
|
||||||
|
|
||||||
-- | Internal types and functions.
|
-- | Internal types and functions.
|
||||||
|
|
||||||
module HPath.Internal
|
module Path.Internal
|
||||||
(Path(..)
|
(Path(..))
|
||||||
,RelC)
|
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.DeepSeq (NFData (..))
|
import Control.DeepSeq (NFData (..))
|
||||||
import Data.ByteString (ByteString)
|
|
||||||
import Data.Data
|
import Data.Data
|
||||||
|
|
||||||
-- | Path of some base and type.
|
-- | Path of some base and type.
|
||||||
--
|
--
|
||||||
-- 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. without trailing path separator: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
|
-- 1. File format: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
|
||||||
-- 2. with trailing path separator: @foo\/@, @\/foo\/bar\/@
|
-- 2. Directory format: @foo\/@, @\/foo\/bar\/@
|
||||||
--
|
--
|
||||||
-- There are no duplicate
|
-- All directories end in a trailing separator. There are no duplicate
|
||||||
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
|
-- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
|
||||||
data Path b = MkPath ByteString
|
data Path b = FPath FilePath
|
||||||
|
| DPath FilePath
|
||||||
deriving (Typeable)
|
deriving (Typeable)
|
||||||
|
|
||||||
-- | String equality.
|
-- | String equality.
|
||||||
@@ -29,7 +32,9 @@ data Path b = MkPath ByteString
|
|||||||
--
|
--
|
||||||
-- @show x == show y ≡ x == y@
|
-- @show x == show y ≡ x == y@
|
||||||
instance Eq (Path b) where
|
instance Eq (Path b) where
|
||||||
(==) (MkPath x) (MkPath y) = x == y
|
(==) (FPath x) (FPath y) = x == y
|
||||||
|
(==) (DPath x) (DPath y) = x == y
|
||||||
|
(==) _ _ = False
|
||||||
|
|
||||||
-- | String ordering.
|
-- | String ordering.
|
||||||
--
|
--
|
||||||
@@ -37,7 +42,9 @@ instance Eq (Path b) where
|
|||||||
--
|
--
|
||||||
-- @show x \`compare\` show y ≡ x \`compare\` y@
|
-- @show x \`compare\` show y ≡ x \`compare\` y@
|
||||||
instance Ord (Path b) where
|
instance Ord (Path b) where
|
||||||
compare (MkPath x) (MkPath y) = compare x y
|
compare (FPath x) (FPath y) = compare x y
|
||||||
|
compare (DPath x) (DPath y) = compare x y
|
||||||
|
compare _ _ = undefined
|
||||||
|
|
||||||
-- | Same as 'Path.toFilePath'.
|
-- | Same as 'Path.toFilePath'.
|
||||||
--
|
--
|
||||||
@@ -45,11 +52,10 @@ instance Ord (Path b) where
|
|||||||
--
|
--
|
||||||
-- @x == y ≡ show x == show y@
|
-- @x == y ≡ show x == show y@
|
||||||
instance Show (Path b) where
|
instance Show (Path b) where
|
||||||
show (MkPath x) = show x
|
show (FPath x) = show x
|
||||||
|
show (DPath x) = show x
|
||||||
|
|
||||||
instance NFData (Path b) where
|
instance NFData (Path b) where
|
||||||
rnf (MkPath x) = rnf x
|
rnf (FPath x) = rnf x
|
||||||
|
rnf (DPath x) = rnf x
|
||||||
|
|
||||||
class RelC m
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
module System.Posix.Directory.Foreign where
|
|
||||||
|
|
||||||
import Data.Bits
|
|
||||||
import Data.List (foldl')
|
|
||||||
import Foreign.C.Types
|
|
||||||
|
|
||||||
#include <limits.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
|
|
||||||
newtype DirType = DirType Int deriving (Eq, Show)
|
|
||||||
data Flags = Flags Int | UnsupportedFlag String deriving (Eq, Show)
|
|
||||||
|
|
||||||
unFlags :: Flags -> Int
|
|
||||||
unFlags (Flags i) = i
|
|
||||||
unFlags (UnsupportedFlag name) = error (name ++ " is not supported on this platform")
|
|
||||||
|
|
||||||
-- |Returns @True@ if posix-paths was compiled with support for the provided
|
|
||||||
-- flag. (As of this writing, the only flag for which this check may be
|
|
||||||
-- necessary is 'oCloexec'; all other flags will always yield @True@.)
|
|
||||||
isSupported :: Flags -> Bool
|
|
||||||
isSupported (Flags _) = True
|
|
||||||
isSupported _ = False
|
|
||||||
|
|
||||||
-- |@O_CLOEXEC@ is not supported on every POSIX platform. Use
|
|
||||||
-- @'isSupported' oCloexec@ to determine if support for @O_CLOEXEC@ was
|
|
||||||
-- compiled into your version of posix-paths. (If not, using @oCloexec@ will
|
|
||||||
-- throw an exception.)
|
|
||||||
oCloexec :: Flags
|
|
||||||
#ifdef O_CLOEXEC
|
|
||||||
oCloexec = Flags #{const O_CLOEXEC}
|
|
||||||
#else
|
|
||||||
{-# WARNING oCloexec
|
|
||||||
"This version of posix-paths was compiled without @O_CLOEXEC@ support." #-}
|
|
||||||
oCloexec = UnsupportedFlag "O_CLOEXEC"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- If these enum declarations occur earlier in the file, haddock
|
|
||||||
-- gets royally confused about the above doc comments.
|
|
||||||
-- Probably http://trac.haskell.org/haddock/ticket/138
|
|
||||||
|
|
||||||
#{enum DirType, DirType, DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, DT_UNKNOWN}
|
|
||||||
|
|
||||||
#{enum Flags, Flags, O_APPEND, O_ASYNC, O_CREAT, O_DIRECTORY, O_EXCL, O_NOCTTY, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_WRONLY, O_RDWR, O_SYNC, O_TRUNC}
|
|
||||||
|
|
||||||
pathMax :: Int
|
|
||||||
pathMax = #{const PATH_MAX}
|
|
||||||
|
|
||||||
unionFlags :: [Flags] -> CInt
|
|
||||||
unionFlags = fromIntegral . foldl' ((. unFlags) . (.|.)) 0
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# LANGUAGE TupleSections #-}
|
|
||||||
{-# LANGUAGE ViewPatterns #-}
|
|
||||||
|
|
||||||
{-# OPTIONS_GHC -Wall #-}
|
|
||||||
module System.Posix.Directory.Traversals (
|
|
||||||
|
|
||||||
getDirectoryContents
|
|
||||||
, getDirectoryContents'
|
|
||||||
|
|
||||||
, allDirectoryContents
|
|
||||||
, allDirectoryContents'
|
|
||||||
, traverseDirectory
|
|
||||||
|
|
||||||
-- lower-level stuff
|
|
||||||
, readDirEnt
|
|
||||||
, packDirStream
|
|
||||||
, unpackDirStream
|
|
||||||
, openFd
|
|
||||||
|
|
||||||
, realpath
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Control.Applicative
|
|
||||||
import Control.Monad
|
|
||||||
import System.Posix.FilePath ((</>))
|
|
||||||
import System.Posix.Directory.Foreign
|
|
||||||
|
|
||||||
import qualified System.Posix as Posix
|
|
||||||
import System.IO.Error
|
|
||||||
import Control.Exception
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
|
||||||
import System.Posix.ByteString.FilePath
|
|
||||||
import System.Posix.Directory.ByteString as PosixBS
|
|
||||||
import System.Posix.Files.ByteString
|
|
||||||
|
|
||||||
import System.IO.Unsafe
|
|
||||||
import Unsafe.Coerce (unsafeCoerce)
|
|
||||||
import Foreign.C.Error
|
|
||||||
import Foreign.C.String
|
|
||||||
import Foreign.C.Types
|
|
||||||
import Foreign.Marshal.Alloc (alloca,allocaBytes)
|
|
||||||
import Foreign.Ptr
|
|
||||||
import Foreign.Storable
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------
|
|
||||||
|
|
||||||
-- | Get all files from a directory and its subdirectories.
|
|
||||||
--
|
|
||||||
-- Upon entering a directory, 'allDirectoryContents' will get all entries
|
|
||||||
-- strictly. However the returned list is lazy in that directories will only
|
|
||||||
-- be accessed on demand.
|
|
||||||
allDirectoryContents :: RawFilePath -> IO [RawFilePath]
|
|
||||||
allDirectoryContents topdir = do
|
|
||||||
namesAndTypes <- getDirectoryContents topdir
|
|
||||||
let properNames = filter ((`notElem` [".", ".."]) . snd) namesAndTypes
|
|
||||||
paths <- forM properNames $ \(typ,name) -> unsafeInterleaveIO $ do
|
|
||||||
let path = topdir </> name
|
|
||||||
case () of
|
|
||||||
() | typ == dtDir -> allDirectoryContents path
|
|
||||||
| typ == dtUnknown -> do
|
|
||||||
isDir <- isDirectory <$> getFileStatus path
|
|
||||||
if isDir
|
|
||||||
then allDirectoryContents path
|
|
||||||
else return [path]
|
|
||||||
| otherwise -> return [path]
|
|
||||||
return (topdir : concat paths)
|
|
||||||
|
|
||||||
-- | Get all files from a directory and its subdirectories strictly.
|
|
||||||
allDirectoryContents' :: RawFilePath -> IO [RawFilePath]
|
|
||||||
allDirectoryContents' = fmap reverse . traverseDirectory (\acc fp -> return (fp:acc)) []
|
|
||||||
-- this uses traverseDirectory because it's more efficient than forcing the
|
|
||||||
-- lazy version.
|
|
||||||
|
|
||||||
-- | Recursively apply the 'action' to the parent directory and all
|
|
||||||
-- files/subdirectories.
|
|
||||||
--
|
|
||||||
-- This function allows for memory-efficient traversals.
|
|
||||||
traverseDirectory :: (s -> RawFilePath -> IO s) -> s -> RawFilePath -> IO s
|
|
||||||
traverseDirectory act s0 topdir = toploop
|
|
||||||
where
|
|
||||||
toploop = do
|
|
||||||
isDir <- isDirectory <$> getFileStatus topdir
|
|
||||||
s' <- act s0 topdir
|
|
||||||
if isDir then actOnDirContents topdir s' loop
|
|
||||||
else return s'
|
|
||||||
loop typ path acc = do
|
|
||||||
isDir <- case () of
|
|
||||||
() | typ == dtDir -> return True
|
|
||||||
| typ == dtUnknown -> isDirectory <$> getFileStatus path
|
|
||||||
| otherwise -> return False
|
|
||||||
if isDir
|
|
||||||
then act acc path >>= \acc' -> actOnDirContents path acc' loop
|
|
||||||
else act acc path
|
|
||||||
|
|
||||||
actOnDirContents :: RawFilePath
|
|
||||||
-> b
|
|
||||||
-> (DirType -> RawFilePath -> b -> IO b)
|
|
||||||
-> IO b
|
|
||||||
actOnDirContents pathRelToTop b f =
|
|
||||||
modifyIOError ((`ioeSetFileName` (BS.unpack pathRelToTop)) .
|
|
||||||
(`ioeSetLocation` "findBSTypRel")) $ do
|
|
||||||
bracket
|
|
||||||
(openDirStream pathRelToTop)
|
|
||||||
(Posix.closeDirStream)
|
|
||||||
(\dirp -> loop dirp b)
|
|
||||||
where
|
|
||||||
loop dirp b' = do
|
|
||||||
(typ,e) <- readDirEnt dirp
|
|
||||||
if (e == "")
|
|
||||||
then return b'
|
|
||||||
else do
|
|
||||||
if (e == "." || e == "..")
|
|
||||||
then loop dirp b'
|
|
||||||
else f typ (pathRelToTop </> e) b' >>= loop dirp
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------
|
|
||||||
-- dodgy stuff
|
|
||||||
|
|
||||||
type CDir = ()
|
|
||||||
type CDirent = ()
|
|
||||||
|
|
||||||
-- Posix doesn't export DirStream, so to re-use that type we need to use
|
|
||||||
-- unsafeCoerce. It's just a newtype, so this is a legitimate usage.
|
|
||||||
-- ugly trick.
|
|
||||||
unpackDirStream :: DirStream -> Ptr CDir
|
|
||||||
unpackDirStream = unsafeCoerce
|
|
||||||
|
|
||||||
packDirStream :: Ptr CDir -> DirStream
|
|
||||||
packDirStream = unsafeCoerce
|
|
||||||
|
|
||||||
-- the __hscore_* functions are defined in the unix package. We can import them and let
|
|
||||||
-- the linker figure it out.
|
|
||||||
foreign import ccall unsafe "__hscore_readdir"
|
|
||||||
c_readdir :: Ptr CDir -> Ptr (Ptr CDirent) -> IO CInt
|
|
||||||
|
|
||||||
foreign import ccall unsafe "__hscore_free_dirent"
|
|
||||||
c_freeDirEnt :: Ptr CDirent -> IO ()
|
|
||||||
|
|
||||||
foreign import ccall unsafe "__hscore_d_name"
|
|
||||||
c_name :: Ptr CDirent -> IO CString
|
|
||||||
|
|
||||||
foreign import ccall unsafe "__posixdir_d_type"
|
|
||||||
c_type :: Ptr CDirent -> IO DirType
|
|
||||||
|
|
||||||
foreign import ccall "realpath"
|
|
||||||
c_realpath :: CString -> CString -> IO CString
|
|
||||||
|
|
||||||
foreign import ccall unsafe "fdopendir"
|
|
||||||
c_fdopendir :: Posix.Fd -> IO (Ptr ())
|
|
||||||
|
|
||||||
foreign import ccall unsafe "open"
|
|
||||||
c_open :: CString -> CInt -> Posix.CMode -> IO CInt
|
|
||||||
|
|
||||||
----------------------------------------------------------
|
|
||||||
-- less dodgy but still lower-level
|
|
||||||
|
|
||||||
|
|
||||||
readDirEnt :: DirStream -> IO (DirType, RawFilePath)
|
|
||||||
readDirEnt (unpackDirStream -> dirp) =
|
|
||||||
alloca $ \ptr_dEnt -> loop ptr_dEnt
|
|
||||||
where
|
|
||||||
loop ptr_dEnt = do
|
|
||||||
resetErrno
|
|
||||||
r <- c_readdir dirp ptr_dEnt
|
|
||||||
if (r == 0)
|
|
||||||
then do
|
|
||||||
dEnt <- peek ptr_dEnt
|
|
||||||
if (dEnt == nullPtr)
|
|
||||||
then return (dtUnknown,BS.empty)
|
|
||||||
else do
|
|
||||||
dName <- c_name dEnt >>= peekFilePath
|
|
||||||
dType <- c_type dEnt
|
|
||||||
c_freeDirEnt dEnt
|
|
||||||
return (dType, dName)
|
|
||||||
else do
|
|
||||||
errno <- getErrno
|
|
||||||
if (errno == eINTR)
|
|
||||||
then loop ptr_dEnt
|
|
||||||
else do
|
|
||||||
let (Errno eo) = errno
|
|
||||||
if (eo == 0)
|
|
||||||
then return (dtUnknown,BS.empty)
|
|
||||||
else throwErrno "readDirEnt"
|
|
||||||
|
|
||||||
|
|
||||||
getDirectoryContents :: RawFilePath -> IO [(DirType, RawFilePath)]
|
|
||||||
getDirectoryContents path =
|
|
||||||
modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
|
|
||||||
(`ioeSetLocation` "System.Posix.Directory.Traversals.getDirectoryContents")) $ do
|
|
||||||
bracket
|
|
||||||
(PosixBS.openDirStream path)
|
|
||||||
PosixBS.closeDirStream
|
|
||||||
loop
|
|
||||||
where
|
|
||||||
loop dirp = do
|
|
||||||
t@(_typ,e) <- readDirEnt dirp
|
|
||||||
if BS.null e then return [] else do
|
|
||||||
es <- loop dirp
|
|
||||||
return (t:es)
|
|
||||||
|
|
||||||
|
|
||||||
fdOpendir :: Posix.Fd -> IO DirStream
|
|
||||||
fdOpendir fd =
|
|
||||||
packDirStream <$> throwErrnoIfNull "fdOpendir" (c_fdopendir fd)
|
|
||||||
|
|
||||||
|
|
||||||
getDirectoryContents' :: Posix.Fd -> IO [(DirType, RawFilePath)]
|
|
||||||
getDirectoryContents' fd =
|
|
||||||
bracket
|
|
||||||
(fdOpendir fd)
|
|
||||||
PosixBS.closeDirStream
|
|
||||||
loop
|
|
||||||
where
|
|
||||||
loop dirp = do
|
|
||||||
t@(_typ,e) <- readDirEnt dirp
|
|
||||||
if BS.null e then return [] else do
|
|
||||||
es <- loop dirp
|
|
||||||
return (t:es)
|
|
||||||
|
|
||||||
|
|
||||||
open_ :: CString
|
|
||||||
-> Posix.OpenMode
|
|
||||||
-> [Flags]
|
|
||||||
-> Maybe Posix.FileMode
|
|
||||||
-> IO Posix.Fd
|
|
||||||
open_ str how optional_flags maybe_mode = do
|
|
||||||
fd <- c_open str all_flags mode_w
|
|
||||||
return (Posix.Fd fd)
|
|
||||||
where
|
|
||||||
all_flags = unionFlags $ optional_flags ++ [open_mode] ++ creat
|
|
||||||
|
|
||||||
|
|
||||||
(creat, mode_w) = case maybe_mode of
|
|
||||||
Nothing -> ([],0)
|
|
||||||
Just x -> ([oCreat], x)
|
|
||||||
|
|
||||||
open_mode = case how of
|
|
||||||
Posix.ReadOnly -> oRdonly
|
|
||||||
Posix.WriteOnly -> oWronly
|
|
||||||
Posix.ReadWrite -> oRdwr
|
|
||||||
|
|
||||||
|
|
||||||
-- |Open and optionally create this file. See 'System.Posix.Files'
|
|
||||||
-- for information on how to use the 'FileMode' type.
|
|
||||||
openFd :: RawFilePath
|
|
||||||
-> Posix.OpenMode
|
|
||||||
-> [Flags]
|
|
||||||
-> Maybe Posix.FileMode
|
|
||||||
-> IO Posix.Fd
|
|
||||||
openFd name how optional_flags maybe_mode =
|
|
||||||
withFilePath name $ \str ->
|
|
||||||
throwErrnoPathIfMinus1Retry "openFd" name $
|
|
||||||
open_ str how optional_flags maybe_mode
|
|
||||||
|
|
||||||
|
|
||||||
-- | return the canonicalized absolute pathname
|
|
||||||
--
|
|
||||||
-- like canonicalizePath, but uses realpath(3)
|
|
||||||
realpath :: RawFilePath -> IO RawFilePath
|
|
||||||
realpath inp = do
|
|
||||||
allocaBytes pathMax $ \tmp -> do
|
|
||||||
void $ BS.useAsCString inp $ \cstr -> throwErrnoIfNull "realpath" $ c_realpath cstr tmp
|
|
||||||
BS.packCString tmp
|
|
||||||
@@ -1,535 +0,0 @@
|
|||||||
{-# LANGUAGE TupleSections #-}
|
|
||||||
|
|
||||||
{-# OPTIONS_GHC -Wall #-}
|
|
||||||
|
|
||||||
-- | The equivalent of "System.FilePath" on raw (byte string) file paths.
|
|
||||||
--
|
|
||||||
-- Not all functions of "System.FilePath" are implemented yet. Feel free to contribute!
|
|
||||||
module System.Posix.FilePath (
|
|
||||||
|
|
||||||
pathSeparator
|
|
||||||
, isPathSeparator
|
|
||||||
, searchPathSeparator
|
|
||||||
, isSearchPathSeparator
|
|
||||||
, extSeparator
|
|
||||||
, isExtSeparator
|
|
||||||
|
|
||||||
, splitExtension
|
|
||||||
, takeExtension
|
|
||||||
, replaceExtension
|
|
||||||
, dropExtension
|
|
||||||
, addExtension
|
|
||||||
, hasExtension
|
|
||||||
, (<.>)
|
|
||||||
, splitExtensions
|
|
||||||
, dropExtensions
|
|
||||||
, takeExtensions
|
|
||||||
|
|
||||||
, splitFileName
|
|
||||||
, takeFileName
|
|
||||||
, replaceFileName
|
|
||||||
, dropFileName
|
|
||||||
, takeBaseName
|
|
||||||
, replaceBaseName
|
|
||||||
, takeDirectory
|
|
||||||
, replaceDirectory
|
|
||||||
, combine
|
|
||||||
, (</>)
|
|
||||||
, splitPath
|
|
||||||
, joinPath
|
|
||||||
, normalise
|
|
||||||
, splitDirectories
|
|
||||||
|
|
||||||
, hasTrailingPathSeparator
|
|
||||||
, addTrailingPathSeparator
|
|
||||||
, dropTrailingPathSeparator
|
|
||||||
|
|
||||||
, isRelative
|
|
||||||
, isAbsolute
|
|
||||||
, isValid
|
|
||||||
, equalFilePath
|
|
||||||
|
|
||||||
, module System.Posix.ByteString.FilePath
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Data.ByteString (ByteString)
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import System.Posix.ByteString.FilePath
|
|
||||||
|
|
||||||
import Data.Maybe (isJust)
|
|
||||||
import Data.Word8
|
|
||||||
|
|
||||||
import Control.Arrow (second)
|
|
||||||
|
|
||||||
-- $setup
|
|
||||||
-- >>> import Data.Char
|
|
||||||
-- >>> import Test.QuickCheck
|
|
||||||
-- >>> import Control.Applicative
|
|
||||||
-- >>> import qualified Data.ByteString as BS
|
|
||||||
-- >>> import Data.ByteString (ByteString)
|
|
||||||
-- >>> instance Arbitrary ByteString where arbitrary = BS.pack <$> arbitrary
|
|
||||||
-- >>> instance CoArbitrary ByteString where coarbitrary = coarbitrary . BS.unpack
|
|
||||||
--
|
|
||||||
-- >>> let _chr :: Word8 -> Char; _chr = chr . fromIntegral
|
|
||||||
|
|
||||||
|
|
||||||
-- | Path separator character
|
|
||||||
pathSeparator :: Word8
|
|
||||||
pathSeparator = _slash
|
|
||||||
|
|
||||||
-- | Check if a character is the path separator
|
|
||||||
--
|
|
||||||
-- prop> \n -> (_chr n == '/') == isPathSeparator n
|
|
||||||
isPathSeparator :: Word8 -> Bool
|
|
||||||
isPathSeparator = (== pathSeparator)
|
|
||||||
|
|
||||||
-- | Search path separator
|
|
||||||
searchPathSeparator :: Word8
|
|
||||||
searchPathSeparator = _colon
|
|
||||||
|
|
||||||
-- | Check if a character is the search path separator
|
|
||||||
--
|
|
||||||
-- prop> \n -> (_chr n == ':') == isSearchPathSeparator n
|
|
||||||
isSearchPathSeparator :: Word8 -> Bool
|
|
||||||
isSearchPathSeparator = (== searchPathSeparator)
|
|
||||||
|
|
||||||
-- | File extension separator
|
|
||||||
extSeparator :: Word8
|
|
||||||
extSeparator = _period
|
|
||||||
|
|
||||||
-- | Check if a character is the file extension separator
|
|
||||||
--
|
|
||||||
-- prop> \n -> (_chr n == '.') == isExtSeparator n
|
|
||||||
isExtSeparator :: Word8 -> Bool
|
|
||||||
isExtSeparator = (== extSeparator)
|
|
||||||
|
|
||||||
------------------------
|
|
||||||
-- extension stuff
|
|
||||||
|
|
||||||
-- | Split a 'RawFilePath' into a path+filename and extension
|
|
||||||
--
|
|
||||||
-- >>> splitExtension "file.exe"
|
|
||||||
-- ("file",".exe")
|
|
||||||
-- >>> splitExtension "file"
|
|
||||||
-- ("file","")
|
|
||||||
-- >>> splitExtension "/path/file.tar.gz"
|
|
||||||
-- ("/path/file.tar",".gz")
|
|
||||||
--
|
|
||||||
-- prop> \path -> uncurry (BS.append) (splitExtension path) == path
|
|
||||||
splitExtension :: RawFilePath -> (RawFilePath, ByteString)
|
|
||||||
splitExtension x = if BS.null basename
|
|
||||||
then (x,BS.empty)
|
|
||||||
else (BS.append path (BS.init basename),BS.cons extSeparator fileExt)
|
|
||||||
where
|
|
||||||
(path,file) = splitFileNameRaw x
|
|
||||||
(basename,fileExt) = BS.breakEnd isExtSeparator file
|
|
||||||
|
|
||||||
-- | Get the final extension from a 'RawFilePath'
|
|
||||||
--
|
|
||||||
-- >>> takeExtension "file.exe"
|
|
||||||
-- ".exe"
|
|
||||||
-- >>> takeExtension "file"
|
|
||||||
-- ""
|
|
||||||
-- >>> takeExtension "/path/file.tar.gz"
|
|
||||||
-- ".gz"
|
|
||||||
takeExtension :: RawFilePath -> ByteString
|
|
||||||
takeExtension = snd . splitExtension
|
|
||||||
|
|
||||||
-- | Change a file's extension
|
|
||||||
--
|
|
||||||
-- prop> \path -> let ext = takeExtension path in replaceExtension path ext == path
|
|
||||||
replaceExtension :: RawFilePath -> ByteString -> RawFilePath
|
|
||||||
replaceExtension path ext = dropExtension path <.> ext
|
|
||||||
|
|
||||||
-- | Drop the final extension from a 'RawFilePath'
|
|
||||||
--
|
|
||||||
-- >>> dropExtension "file.exe"
|
|
||||||
-- "file"
|
|
||||||
-- >>> dropExtension "file"
|
|
||||||
-- "file"
|
|
||||||
-- >>> dropExtension "/path/file.tar.gz"
|
|
||||||
-- "/path/file.tar"
|
|
||||||
dropExtension :: RawFilePath -> RawFilePath
|
|
||||||
dropExtension = fst . splitExtension
|
|
||||||
|
|
||||||
-- | Add an extension to a 'RawFilePath'
|
|
||||||
--
|
|
||||||
-- >>> addExtension "file" ".exe"
|
|
||||||
-- "file.exe"
|
|
||||||
-- >>> addExtension "file.tar" ".gz"
|
|
||||||
-- "file.tar.gz"
|
|
||||||
-- >>> addExtension "/path/" ".ext"
|
|
||||||
-- "/path/.ext"
|
|
||||||
addExtension :: RawFilePath -> ByteString -> RawFilePath
|
|
||||||
addExtension file ext
|
|
||||||
| BS.null ext = file
|
|
||||||
| isExtSeparator (BS.head ext) = BS.append file ext
|
|
||||||
| otherwise = BS.intercalate (BS.singleton extSeparator) [file, ext]
|
|
||||||
|
|
||||||
|
|
||||||
-- | Operator version of 'addExtension'
|
|
||||||
(<.>) :: RawFilePath -> ByteString -> RawFilePath
|
|
||||||
(<.>) = addExtension
|
|
||||||
|
|
||||||
-- | Check if a 'RawFilePath' has an extension
|
|
||||||
--
|
|
||||||
-- >>> hasExtension "file"
|
|
||||||
-- False
|
|
||||||
-- >>> hasExtension "file.tar"
|
|
||||||
-- True
|
|
||||||
-- >>> hasExtension "/path.part1/"
|
|
||||||
-- False
|
|
||||||
hasExtension :: RawFilePath -> Bool
|
|
||||||
hasExtension = isJust . BS.elemIndex extSeparator . takeFileName
|
|
||||||
|
|
||||||
-- | Split a 'RawFilePath' on the first extension
|
|
||||||
--
|
|
||||||
-- >>> splitExtensions "/path/file.tar.gz"
|
|
||||||
-- ("/path/file",".tar.gz")
|
|
||||||
--
|
|
||||||
-- prop> \path -> uncurry addExtension (splitExtensions path) == path
|
|
||||||
splitExtensions :: RawFilePath -> (RawFilePath, ByteString)
|
|
||||||
splitExtensions x = if BS.null basename
|
|
||||||
then (path,fileExt)
|
|
||||||
else (BS.append path basename,fileExt)
|
|
||||||
where
|
|
||||||
(path,file) = splitFileNameRaw x
|
|
||||||
(basename,fileExt) = BS.break isExtSeparator file
|
|
||||||
|
|
||||||
-- | Remove all extensions from a 'RawFilePath'
|
|
||||||
--
|
|
||||||
-- >>> dropExtensions "/path/file.tar.gz"
|
|
||||||
-- "/path/file"
|
|
||||||
dropExtensions :: RawFilePath -> RawFilePath
|
|
||||||
dropExtensions = fst . splitExtensions
|
|
||||||
|
|
||||||
-- | Take all extensions from a 'RawFilePath'
|
|
||||||
--
|
|
||||||
-- >>> takeExtensions "/path/file.tar.gz"
|
|
||||||
-- ".tar.gz"
|
|
||||||
takeExtensions :: RawFilePath -> ByteString
|
|
||||||
takeExtensions = snd . splitExtensions
|
|
||||||
|
|
||||||
------------------------
|
|
||||||
-- more stuff
|
|
||||||
|
|
||||||
-- | Split a 'RawFilePath' into (path,file). 'combine' is the inverse
|
|
||||||
--
|
|
||||||
-- >>> splitFileName "path/file.txt"
|
|
||||||
-- ("path/","file.txt")
|
|
||||||
-- >>> splitFileName "path/"
|
|
||||||
-- ("path/","")
|
|
||||||
-- >>> splitFileName "file.txt"
|
|
||||||
-- ("./","file.txt")
|
|
||||||
--
|
|
||||||
-- prop> \path -> uncurry combine (splitFileName path) == path || fst (splitFileName path) == "./"
|
|
||||||
splitFileName :: RawFilePath -> (RawFilePath, RawFilePath)
|
|
||||||
splitFileName x = if BS.null path
|
|
||||||
then (dotSlash, file)
|
|
||||||
else (path,file)
|
|
||||||
where
|
|
||||||
(path,file) = splitFileNameRaw x
|
|
||||||
dotSlash = _period `BS.cons` (BS.singleton pathSeparator)
|
|
||||||
|
|
||||||
|
|
||||||
-- | Get the file name
|
|
||||||
--
|
|
||||||
-- >>> takeFileName "path/file.txt"
|
|
||||||
-- "file.txt"
|
|
||||||
-- >>> takeFileName "path/"
|
|
||||||
-- ""
|
|
||||||
takeFileName :: RawFilePath -> RawFilePath
|
|
||||||
takeFileName = snd . splitFileName
|
|
||||||
|
|
||||||
-- | Change the file name
|
|
||||||
--
|
|
||||||
-- prop> \path -> replaceFileName path (takeFileName path) == path
|
|
||||||
replaceFileName :: RawFilePath -> ByteString -> RawFilePath
|
|
||||||
replaceFileName x y = fst (splitFileNameRaw x) </> y
|
|
||||||
|
|
||||||
-- | Drop the file name
|
|
||||||
--
|
|
||||||
-- >>> dropFileName "path/file.txt"
|
|
||||||
-- "path/"
|
|
||||||
-- >>> dropFileName "file.txt"
|
|
||||||
-- "./"
|
|
||||||
dropFileName :: RawFilePath -> RawFilePath
|
|
||||||
dropFileName = fst . splitFileName
|
|
||||||
|
|
||||||
-- | Get the file name, without a trailing extension
|
|
||||||
--
|
|
||||||
-- >>> takeBaseName "path/file.tar.gz"
|
|
||||||
-- "file.tar"
|
|
||||||
-- >>> takeBaseName ""
|
|
||||||
-- ""
|
|
||||||
takeBaseName :: RawFilePath -> ByteString
|
|
||||||
takeBaseName = dropExtension . takeFileName
|
|
||||||
|
|
||||||
-- | Change the base name
|
|
||||||
--
|
|
||||||
-- >>> replaceBaseName "path/file.tar.gz" "bob"
|
|
||||||
-- "path/bob.gz"
|
|
||||||
--
|
|
||||||
-- prop> \path -> replaceBaseName path (takeBaseName path) == path
|
|
||||||
replaceBaseName :: RawFilePath -> ByteString -> RawFilePath
|
|
||||||
replaceBaseName path name = combineRaw dir (name <.> ext)
|
|
||||||
where
|
|
||||||
(dir,file) = splitFileNameRaw path
|
|
||||||
ext = takeExtension file
|
|
||||||
|
|
||||||
-- | Get the directory, moving up one level if it's already a directory
|
|
||||||
--
|
|
||||||
-- >>> takeDirectory "path/file.txt"
|
|
||||||
-- "path"
|
|
||||||
-- >>> takeDirectory "file"
|
|
||||||
-- "."
|
|
||||||
-- >>> takeDirectory "/path/to/"
|
|
||||||
-- "/path/to"
|
|
||||||
-- >>> takeDirectory "/path/to"
|
|
||||||
-- "/path"
|
|
||||||
takeDirectory :: RawFilePath -> RawFilePath
|
|
||||||
takeDirectory x = case () of
|
|
||||||
() | x == BS.singleton pathSeparator -> x
|
|
||||||
| BS.null res && not (BS.null file) -> file
|
|
||||||
| otherwise -> res
|
|
||||||
where
|
|
||||||
res = fst $ BS.spanEnd isPathSeparator file
|
|
||||||
file = dropFileName x
|
|
||||||
|
|
||||||
-- | Change the directory component of a 'RawFilePath'
|
|
||||||
--
|
|
||||||
-- prop> \path -> replaceDirectory path (takeDirectory path) `equalFilePath` path || takeDirectory path == "."
|
|
||||||
replaceDirectory :: RawFilePath -> ByteString -> RawFilePath
|
|
||||||
replaceDirectory file dir = combineRaw dir (takeFileName file)
|
|
||||||
|
|
||||||
-- | Join two paths together
|
|
||||||
--
|
|
||||||
-- >>> combine "/" "file"
|
|
||||||
-- "/file"
|
|
||||||
-- >>> combine "/path/to" "file"
|
|
||||||
-- "/path/to/file"
|
|
||||||
-- >>> combine "file" "/absolute/path"
|
|
||||||
-- "/absolute/path"
|
|
||||||
combine :: RawFilePath -> RawFilePath -> RawFilePath
|
|
||||||
combine a b | not (BS.null b) && isPathSeparator (BS.head b) = b
|
|
||||||
| otherwise = combineRaw a b
|
|
||||||
|
|
||||||
-- | Operator version of combine
|
|
||||||
(</>) :: RawFilePath -> RawFilePath -> RawFilePath
|
|
||||||
(</>) = combine
|
|
||||||
|
|
||||||
-- | Split a path into a list of components:
|
|
||||||
--
|
|
||||||
-- >>> splitPath "/path/to/file.txt"
|
|
||||||
-- ["/","path/","to/","file.txt"]
|
|
||||||
--
|
|
||||||
-- prop> \path -> BS.concat (splitPath path) == path
|
|
||||||
splitPath :: RawFilePath -> [RawFilePath]
|
|
||||||
splitPath = splitter
|
|
||||||
where
|
|
||||||
splitter x
|
|
||||||
| BS.null x = []
|
|
||||||
| otherwise = case BS.elemIndex pathSeparator x of
|
|
||||||
Nothing -> [x]
|
|
||||||
Just ix -> case BS.findIndex (not . isPathSeparator) $ BS.drop (ix+1) x of
|
|
||||||
Nothing -> [x]
|
|
||||||
Just runlen -> uncurry (:) . second splitter $ BS.splitAt (ix+1+runlen) x
|
|
||||||
|
|
||||||
-- | Like 'splitPath', but without trailing slashes
|
|
||||||
--
|
|
||||||
-- >>> splitDirectories "/path/to/file.txt"
|
|
||||||
-- ["/","path","to","file.txt"]
|
|
||||||
-- >>> splitDirectories ""
|
|
||||||
-- []
|
|
||||||
splitDirectories :: RawFilePath -> [RawFilePath]
|
|
||||||
splitDirectories x
|
|
||||||
| BS.null x = []
|
|
||||||
| isPathSeparator (BS.head x) = let (root,rest) = BS.splitAt 1 x
|
|
||||||
in root : splitter rest
|
|
||||||
| otherwise = splitter x
|
|
||||||
where
|
|
||||||
splitter = filter (not . BS.null) . BS.split pathSeparator
|
|
||||||
|
|
||||||
-- | Join a split path back together
|
|
||||||
--
|
|
||||||
-- prop> \path -> joinPath (splitPath path) == path
|
|
||||||
--
|
|
||||||
-- >>> joinPath ["path","to","file.txt"]
|
|
||||||
-- "path/to/file.txt"
|
|
||||||
joinPath :: [RawFilePath] -> RawFilePath
|
|
||||||
joinPath = foldr (</>) BS.empty
|
|
||||||
|
|
||||||
|
|
||||||
-- |Normalise a file.
|
|
||||||
--
|
|
||||||
-- >>> normalise "/file/\\test////"
|
|
||||||
-- "/file/\\test/"
|
|
||||||
-- >>> normalise "/file/./test"
|
|
||||||
-- "/file/test"
|
|
||||||
-- >>> normalise "/test/file/../bob/fred/"
|
|
||||||
-- "/test/file/../bob/fred/"
|
|
||||||
-- >>> normalise "../bob/fred/"
|
|
||||||
-- "../bob/fred/"
|
|
||||||
-- >>> normalise "./bob/fred/"
|
|
||||||
-- "bob/fred/"
|
|
||||||
-- >>> normalise "./bob////.fred/./...///./..///#."
|
|
||||||
-- "bob/.fred/.../../#."
|
|
||||||
-- >>> normalise "."
|
|
||||||
-- "."
|
|
||||||
-- >>> normalise "./"
|
|
||||||
-- "./"
|
|
||||||
-- >>> normalise "./."
|
|
||||||
-- "./"
|
|
||||||
-- >>> normalise "/./"
|
|
||||||
-- "/"
|
|
||||||
-- >>> normalise "/"
|
|
||||||
-- "/"
|
|
||||||
-- >>> normalise "bob/fred/."
|
|
||||||
-- "bob/fred/"
|
|
||||||
-- >>> normalise "//home"
|
|
||||||
-- "/home"
|
|
||||||
normalise :: RawFilePath -> RawFilePath
|
|
||||||
normalise filepath =
|
|
||||||
result `BS.append`
|
|
||||||
(if addPathSeparator
|
|
||||||
then BS.singleton pathSeparator
|
|
||||||
else BS.empty)
|
|
||||||
where
|
|
||||||
result = let n = f filepath
|
|
||||||
in if BS.null n
|
|
||||||
then BS.singleton _period
|
|
||||||
else n
|
|
||||||
addPathSeparator = isDirPath filepath &&
|
|
||||||
not (hasTrailingPathSeparator result)
|
|
||||||
isDirPath xs = hasTrailingPathSeparator xs
|
|
||||||
|| not (BS.null xs) && BS.last xs == _period
|
|
||||||
&& hasTrailingPathSeparator (BS.init xs)
|
|
||||||
f = joinPath . dropDots . propSep . splitDirectories
|
|
||||||
propSep :: [ByteString] -> [ByteString]
|
|
||||||
propSep (x:xs)
|
|
||||||
| BS.all (== pathSeparator) x = BS.singleton pathSeparator : xs
|
|
||||||
| otherwise = x : xs
|
|
||||||
propSep [] = []
|
|
||||||
dropDots :: [ByteString] -> [ByteString]
|
|
||||||
dropDots = filter (BS.singleton _period /=)
|
|
||||||
|
|
||||||
|
|
||||||
------------------------
|
|
||||||
-- trailing path separators
|
|
||||||
|
|
||||||
-- | Check if the last character of a 'RawFilePath' is '/'.
|
|
||||||
--
|
|
||||||
-- >>> hasTrailingPathSeparator "/path/"
|
|
||||||
-- True
|
|
||||||
-- >>> hasTrailingPathSeparator "/"
|
|
||||||
-- True
|
|
||||||
-- >>> hasTrailingPathSeparator "/path"
|
|
||||||
-- False
|
|
||||||
hasTrailingPathSeparator :: RawFilePath -> Bool
|
|
||||||
hasTrailingPathSeparator x
|
|
||||||
| BS.null x = False
|
|
||||||
| otherwise = isPathSeparator $ BS.last x
|
|
||||||
|
|
||||||
-- | Add a trailing path separator.
|
|
||||||
--
|
|
||||||
-- >>> addTrailingPathSeparator "/path"
|
|
||||||
-- "/path/"
|
|
||||||
-- >>> addTrailingPathSeparator "/path/"
|
|
||||||
-- "/path/"
|
|
||||||
-- >>> addTrailingPathSeparator "/"
|
|
||||||
-- "/"
|
|
||||||
addTrailingPathSeparator :: RawFilePath -> RawFilePath
|
|
||||||
addTrailingPathSeparator x = if hasTrailingPathSeparator x
|
|
||||||
then x
|
|
||||||
else x `BS.snoc` pathSeparator
|
|
||||||
|
|
||||||
-- | Remove a trailing path separator
|
|
||||||
--
|
|
||||||
-- >>> dropTrailingPathSeparator "/path/"
|
|
||||||
-- "/path"
|
|
||||||
-- >>> dropTrailingPathSeparator "/path////"
|
|
||||||
-- "/path"
|
|
||||||
-- >>> dropTrailingPathSeparator "/"
|
|
||||||
-- "/"
|
|
||||||
-- >>> dropTrailingPathSeparator "//"
|
|
||||||
-- "/"
|
|
||||||
dropTrailingPathSeparator :: RawFilePath -> RawFilePath
|
|
||||||
dropTrailingPathSeparator x
|
|
||||||
| x == BS.singleton pathSeparator = x
|
|
||||||
| otherwise = if hasTrailingPathSeparator x
|
|
||||||
then dropTrailingPathSeparator $ BS.init x
|
|
||||||
else x
|
|
||||||
|
|
||||||
------------------------
|
|
||||||
-- Filename/system stuff
|
|
||||||
|
|
||||||
-- | Check if a path is absolute
|
|
||||||
--
|
|
||||||
-- >>> isAbsolute "/path"
|
|
||||||
-- True
|
|
||||||
-- >>> isAbsolute "path"
|
|
||||||
-- False
|
|
||||||
-- >>> isAbsolute ""
|
|
||||||
-- False
|
|
||||||
isAbsolute :: RawFilePath -> Bool
|
|
||||||
isAbsolute x
|
|
||||||
| BS.length x > 0 = isPathSeparator (BS.head x)
|
|
||||||
| otherwise = False
|
|
||||||
|
|
||||||
-- | Check if a path is relative
|
|
||||||
--
|
|
||||||
-- prop> \path -> isRelative path /= isAbsolute path
|
|
||||||
isRelative :: RawFilePath -> Bool
|
|
||||||
isRelative = not . isAbsolute
|
|
||||||
|
|
||||||
-- | Is a FilePath valid, i.e. could you create a file like it?
|
|
||||||
--
|
|
||||||
-- >>> isValid ""
|
|
||||||
-- False
|
|
||||||
-- >>> isValid "\0"
|
|
||||||
-- False
|
|
||||||
-- >>> isValid "/random_ path:*"
|
|
||||||
-- True
|
|
||||||
isValid :: RawFilePath -> Bool
|
|
||||||
isValid filepath
|
|
||||||
| BS.null filepath = False
|
|
||||||
| _nul `BS.elem` filepath = False
|
|
||||||
| otherwise = True
|
|
||||||
|
|
||||||
-- |Equality of two filepaths. The filepaths are normalised
|
|
||||||
-- and trailing path separators are dropped.
|
|
||||||
--
|
|
||||||
-- >>> equalFilePath "foo" "foo"
|
|
||||||
-- True
|
|
||||||
-- >>> equalFilePath "foo" "foo/"
|
|
||||||
-- True
|
|
||||||
-- >>> equalFilePath "foo" "./foo"
|
|
||||||
-- True
|
|
||||||
-- >>> equalFilePath "foo" "/foo"
|
|
||||||
-- False
|
|
||||||
-- >>> equalFilePath "foo" "FOO"
|
|
||||||
-- False
|
|
||||||
-- >>> equalFilePath "foo" "../foo"
|
|
||||||
-- False
|
|
||||||
--
|
|
||||||
-- prop> \p -> equalFilePath p p
|
|
||||||
equalFilePath :: RawFilePath -> RawFilePath -> Bool
|
|
||||||
equalFilePath p1 p2 = f p1 == f p2
|
|
||||||
where
|
|
||||||
f x = dropTrailingPathSeparator $ normalise x
|
|
||||||
|
|
||||||
------------------------
|
|
||||||
-- internal stuff
|
|
||||||
|
|
||||||
-- Just split the input FileName without adding/normalizing or changing
|
|
||||||
-- anything.
|
|
||||||
splitFileNameRaw :: RawFilePath -> (RawFilePath, RawFilePath)
|
|
||||||
splitFileNameRaw x = BS.breakEnd isPathSeparator x
|
|
||||||
|
|
||||||
-- | Combine two paths, assuming rhs is NOT absolute.
|
|
||||||
combineRaw :: RawFilePath -> RawFilePath -> RawFilePath
|
|
||||||
combineRaw a b | BS.null a = b
|
|
||||||
| BS.null b = a
|
|
||||||
| isPathSeparator (BS.last a) = BS.append a b
|
|
||||||
| otherwise = BS.intercalate (BS.singleton pathSeparator) [a, b]
|
|
||||||
|
|
||||||
225
test/Main.hs
Normal file
225
test/Main.hs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
-- | Test suite.
|
||||||
|
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Maybe
|
||||||
|
import Data.Monoid
|
||||||
|
import Path
|
||||||
|
import Path.Internal
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
-- | Test suite entry point, returns exit failure if any test fails.
|
||||||
|
main :: IO ()
|
||||||
|
main = hspec spec
|
||||||
|
|
||||||
|
-- | Test suite.
|
||||||
|
spec :: Spec
|
||||||
|
spec =
|
||||||
|
do describe "Parsing: Path Abs Dir" parseAbsDirSpec
|
||||||
|
describe "Parsing: Path Rel Dir" parseRelDirSpec
|
||||||
|
describe "Parsing: Path Abs File" parseAbsFileSpec
|
||||||
|
describe "Parsing: Path Rel File" parseRelFileSpec
|
||||||
|
describe "Operations: (</>)" operationAppend
|
||||||
|
describe "Operations: stripDir" operationStripDir
|
||||||
|
describe "Operations: isParentOf" operationIsParentOf
|
||||||
|
describe "Operations: parent" operationParent
|
||||||
|
describe "Operations: filename" operationFilename
|
||||||
|
describe "Restrictions" restrictions
|
||||||
|
|
||||||
|
-- | Restricting the input of any tricks.
|
||||||
|
restrictions :: Spec
|
||||||
|
restrictions =
|
||||||
|
do parseFails "~/"
|
||||||
|
parseFails "~/foo"
|
||||||
|
parseFails "~/foo/bar"
|
||||||
|
parseFails "../"
|
||||||
|
parseFails ".."
|
||||||
|
parseFails "."
|
||||||
|
parseFails "/.."
|
||||||
|
parseFails "/foo/../bar/"
|
||||||
|
parseFails "/foo/bar/.."
|
||||||
|
where parseFails x =
|
||||||
|
it (show x ++ " should be rejected")
|
||||||
|
(isNothing (void (parseAbsDir x) <|>
|
||||||
|
void (parseRelDir x) <|>
|
||||||
|
void (parseAbsFile x) <|>
|
||||||
|
void (parseRelFile x)))
|
||||||
|
|
||||||
|
-- | The 'filename' operation.
|
||||||
|
operationFilename :: Spec
|
||||||
|
operationFilename =
|
||||||
|
do it "filename ($(mkAbsDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
|
||||||
|
(filename ($(mkAbsDir "/home/chris/") </>
|
||||||
|
filename $(mkRelFile "bar.txt")) ==
|
||||||
|
$(mkRelFile "bar.txt"))
|
||||||
|
it "filename ($(mkRelDir parent) </> filename $(mkRelFile filename)) == $(mkRelFile filename)"
|
||||||
|
(filename ($(mkRelDir "home/chris/") </>
|
||||||
|
filename $(mkRelFile "bar.txt")) ==
|
||||||
|
$(mkRelFile "bar.txt"))
|
||||||
|
|
||||||
|
-- | The 'parent' operation.
|
||||||
|
operationParent :: Spec
|
||||||
|
operationParent =
|
||||||
|
do it "parent (parent </> child) == parent"
|
||||||
|
(parent ($(mkAbsDir "/foo") </>
|
||||||
|
$(mkRelDir "bar")) ==
|
||||||
|
$(mkAbsDir "/foo"))
|
||||||
|
it "parent \"\" == \"\""
|
||||||
|
(parent $(mkAbsDir "/") ==
|
||||||
|
$(mkAbsDir "/"))
|
||||||
|
it "parent (parent \"\") == \"\""
|
||||||
|
(parent (parent $(mkAbsDir "/")) ==
|
||||||
|
$(mkAbsDir "/"))
|
||||||
|
|
||||||
|
-- | The 'isParentOf' operation.
|
||||||
|
operationIsParentOf :: Spec
|
||||||
|
operationIsParentOf =
|
||||||
|
do it "isParentOf parent (parent </> child)"
|
||||||
|
(isParentOf
|
||||||
|
$(mkAbsDir "///bar/")
|
||||||
|
($(mkAbsDir "///bar/") </>
|
||||||
|
$(mkRelFile "bar/foo.txt")))
|
||||||
|
it "isParentOf parent (parent </> child)"
|
||||||
|
(isParentOf
|
||||||
|
$(mkRelDir "bar/")
|
||||||
|
($(mkRelDir "bar/") </>
|
||||||
|
$(mkRelFile "bob/foo.txt")))
|
||||||
|
|
||||||
|
-- | The 'stripDir' operation.
|
||||||
|
operationStripDir :: Spec
|
||||||
|
operationStripDir =
|
||||||
|
do it "stripDir parent (parent </> child) = child"
|
||||||
|
(stripDir $(mkAbsDir "///bar/")
|
||||||
|
($(mkAbsDir "///bar/") </>
|
||||||
|
$(mkRelFile "bar/foo.txt")) ==
|
||||||
|
Just $(mkRelFile "bar/foo.txt"))
|
||||||
|
it "stripDir parent (parent </> child) = child"
|
||||||
|
(stripDir $(mkRelDir "bar/")
|
||||||
|
($(mkRelDir "bar/") </>
|
||||||
|
$(mkRelFile "bob/foo.txt")) ==
|
||||||
|
Just $(mkRelFile "bob/foo.txt"))
|
||||||
|
it "stripDir parent parent = _|_"
|
||||||
|
(stripDir $(mkAbsDir "/home/chris/foo")
|
||||||
|
$(mkAbsDir "/home/chris/foo") ==
|
||||||
|
Nothing)
|
||||||
|
|
||||||
|
-- | The '</>' operation.
|
||||||
|
operationAppend :: Spec
|
||||||
|
operationAppend =
|
||||||
|
do it "AbsDir + RelDir = AbsDir"
|
||||||
|
($(mkAbsDir "/home/") </>
|
||||||
|
$(mkRelDir "chris") ==
|
||||||
|
$(mkAbsDir "/home/chris/"))
|
||||||
|
it "AbsDir + RelFile = AbsFile"
|
||||||
|
($(mkAbsDir "/home/") </>
|
||||||
|
$(mkRelFile "chris/test.txt") ==
|
||||||
|
$(mkAbsFile "/home/chris/test.txt"))
|
||||||
|
it "RelDir + RelDir = RelDir"
|
||||||
|
($(mkRelDir "home/") </>
|
||||||
|
$(mkRelDir "chris") ==
|
||||||
|
$(mkRelDir "home/chris"))
|
||||||
|
it "RelDir + RelFile = RelFile"
|
||||||
|
($(mkRelDir "home/") </>
|
||||||
|
$(mkRelFile "chris/test.txt") ==
|
||||||
|
$(mkRelFile "home/chris/test.txt"))
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseAbsDirSpec :: Spec
|
||||||
|
parseAbsDirSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "./"
|
||||||
|
failing "~/"
|
||||||
|
failing "foo.txt"
|
||||||
|
succeeding "/" (Path "/")
|
||||||
|
succeeding "//" (Path "/")
|
||||||
|
succeeding "///foo//bar//mu/" (Path "/foo/bar/mu/")
|
||||||
|
succeeding "///foo//bar////mu" (Path "/foo/bar/mu/")
|
||||||
|
succeeding "///foo//bar/.//mu" (Path "/foo/bar/mu/")
|
||||||
|
where failing x = parserTest parseAbsDir x Nothing
|
||||||
|
succeeding x with = parserTest parseAbsDir x (Just with)
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseRelDirSpec :: Spec
|
||||||
|
parseRelDirSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "/"
|
||||||
|
failing "//"
|
||||||
|
failing "~/"
|
||||||
|
failing "/"
|
||||||
|
failing "./"
|
||||||
|
failing "././"
|
||||||
|
failing "//"
|
||||||
|
failing "///foo//bar//mu/"
|
||||||
|
failing "///foo//bar////mu"
|
||||||
|
failing "///foo//bar/.//mu"
|
||||||
|
succeeding "..." (Path ".../")
|
||||||
|
succeeding "foo.bak" (Path "foo.bak/")
|
||||||
|
succeeding "./foo" (Path "foo/")
|
||||||
|
succeeding "././foo" (Path "foo/")
|
||||||
|
succeeding "./foo/./bar" (Path "foo/bar/")
|
||||||
|
succeeding "foo//bar//mu//" (Path "foo/bar/mu/")
|
||||||
|
succeeding "foo//bar////mu" (Path "foo/bar/mu/")
|
||||||
|
succeeding "foo//bar/.//mu" (Path "foo/bar/mu/")
|
||||||
|
where failing x = parserTest parseRelDir x Nothing
|
||||||
|
succeeding x with = parserTest parseRelDir x (Just with)
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseAbsFileSpec :: Spec
|
||||||
|
parseAbsFileSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "./"
|
||||||
|
failing "~/"
|
||||||
|
failing "./foo.txt"
|
||||||
|
failing "/"
|
||||||
|
failing "//"
|
||||||
|
failing "///foo//bar//mu/"
|
||||||
|
succeeding "/..." (Path "/...")
|
||||||
|
succeeding "/foo.txt" (Path "/foo.txt")
|
||||||
|
succeeding "///foo//bar////mu.txt" (Path "/foo/bar/mu.txt")
|
||||||
|
succeeding "///foo//bar/.//mu.txt" (Path "/foo/bar/mu.txt")
|
||||||
|
where failing x = parserTest parseAbsFile x Nothing
|
||||||
|
succeeding x with = parserTest parseAbsFile x (Just with)
|
||||||
|
|
||||||
|
-- | Tests for the tokenizer.
|
||||||
|
parseRelFileSpec :: Spec
|
||||||
|
parseRelFileSpec =
|
||||||
|
do failing ""
|
||||||
|
failing "/"
|
||||||
|
failing "//"
|
||||||
|
failing "~/"
|
||||||
|
failing "/"
|
||||||
|
failing "./"
|
||||||
|
failing "//"
|
||||||
|
failing "///foo//bar//mu/"
|
||||||
|
failing "///foo//bar////mu"
|
||||||
|
failing "///foo//bar/.//mu"
|
||||||
|
succeeding "..." (Path "...")
|
||||||
|
succeeding "foo.txt" (Path "foo.txt")
|
||||||
|
succeeding "./foo.txt" (Path "foo.txt")
|
||||||
|
succeeding "././foo.txt" (Path "foo.txt")
|
||||||
|
succeeding "./foo/./bar.txt" (Path "foo/bar.txt")
|
||||||
|
succeeding "foo//bar//mu.txt" (Path "foo/bar/mu.txt")
|
||||||
|
succeeding "foo//bar////mu.txt" (Path "foo/bar/mu.txt")
|
||||||
|
succeeding "foo//bar/.//mu.txt" (Path "foo/bar/mu.txt")
|
||||||
|
where failing x = parserTest parseRelFile x Nothing
|
||||||
|
succeeding x with = parserTest parseRelFile x (Just with)
|
||||||
|
|
||||||
|
-- | Parser test.
|
||||||
|
parserTest :: (Show a1,Show a,Eq a1)
|
||||||
|
=> (a -> Maybe a1) -> a -> Maybe a1 -> SpecWith ()
|
||||||
|
parserTest parser input expected =
|
||||||
|
it ((case expected of
|
||||||
|
Nothing -> "Failing: "
|
||||||
|
Just{} -> "Succeeding: ") <>
|
||||||
|
"Parsing " <>
|
||||||
|
show input <>
|
||||||
|
" " <>
|
||||||
|
case expected of
|
||||||
|
Nothing -> "should fail."
|
||||||
|
Just x -> "should succeed with: " <> show x)
|
||||||
|
(actual == expected)
|
||||||
|
where actual = parser input
|
||||||
Reference in New Issue
Block a user