Compare commits

...

13 Commits

Author SHA1 Message Date
Max Howell
6e1eeb158a No fatal on linux Swift < 5 2020-01-25 14:00:59 -05:00
Max Howell
260196a27a CodeCoverage++ 2020-01-25 14:00:59 -05:00
Max Howell
0de9715b46 [ci skip] Update README.md 2020-01-25 12:05:58 -05:00
Max Howell
240d699986 Delete pushed version tag on failed deploy 2020-01-25 10:53:23 -05:00
Max Howell
b63b5746dc Delete pushed version tag on failed deploy 2020-01-24 12:14:03 -05:00
Max Howell
f062ed9ce3 Fix CI deploy 2020-01-24 12:01:34 -05:00
Max Howell
f1f7ee33b1 [ci skip] List all support Swift 2020-01-24 11:17:42 -05:00
Max Howell
694d04f18b Prepare 1.0.0 release 2020-01-24 11:03:07 -05:00
Max Howell
5636a7ac65 Update Swift Linux test versions 2020-01-18 12:10:32 -05:00
Max Howell
3e964833ff Fix README documentation for Finder 2020-01-18 12:10:32 -05:00
Max Howell
30122659a5 Update linux-tests; fail if warnings on travis
* Update linux-tests; fail if warnings on travis

* Fix warnings on Linux

* Typo

* Can’t test these on Linux
2019-08-18 16:52:24 -04:00
Max Howell
0ef50dff2e Finder is a iterable Sequence; .type -> .kind 2019-07-24 14:39:47 -04:00
Max Howell
dfad7367b7 Get out documentation %age up 2019-07-21 21:35:48 -04:00
14 changed files with 536 additions and 199 deletions

2
.github/jazzy.yml vendored
View File

@@ -3,7 +3,7 @@ custom_categories:
- name: Path - name: Path
children: children:
- Path - Path
- /(_:_:) - Pathish
xcodebuild_arguments: xcodebuild_arguments:
- UseModernBuildSystem=NO - UseModernBuildSystem=NO
output: output:

View File

@@ -1,10 +1,14 @@
# only run for: merge commits, releases and pull-requests # only run for: merge commits, releases and pull-requests
if: type != push OR branch = master OR branch =~ /^\d+\.\d+\.\d+(-.*)?$/ if: type != push OR branch = master OR branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/ OR branch =~ /^\d+\.\d+\.\d+(-.*)?$/
stages: stages:
- name: pretest - name: pretest
if: NOT branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/
- name: test - name: test
if: NOT branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/
- name: deploy - name: deploy
if: branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/
- name: publish
if: branch =~ /^\d+\.\d+\.\d+(-.*)?$/ if: branch =~ /^\d+\.\d+\.\d+(-.*)?$/
os: osx os: osx
@@ -16,18 +20,21 @@ xcode_scheme: Path.swift-Package
jobs: jobs:
include: include:
- name: macOS / Swift 4.0.3 - name: macOS / Swift 4.0.3
before_script: swift build -Xswiftc -warnings-as-errors
script: swift test --parallel -Xswiftc -swift-version -Xswiftc 4 script: swift test --parallel -Xswiftc -swift-version -Xswiftc 4
- name: macOS / Swift 4.2.1 - &std
name: macOS / Swift 4.2.1
before_script: swift build -Xswiftc -warnings-as-errors
script: swift test --parallel script: swift test --parallel
- name: macOS / Swift 5.0 - <<: *std
name: macOS / Swift 5.0
osx_image: xcode10.2 osx_image: xcode10.2
script: swift test --parallel
- name: macOS / Swift 5.1 - <<: *std
name: macOS / Swift 5.1
osx_image: xcode11 osx_image: xcode11
script: swift test --parallel
- &xcodebuild - &xcodebuild
before_install: swift package generate-xcodeproj --enable-code-coverage before_install: swift package generate-xcodeproj --enable-code-coverage
@@ -57,15 +64,20 @@ jobs:
language: generic language: generic
sudo: false sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
before_script: swift build -Xswiftc -warnings-as-errors
script: swift test --parallel script: swift test --parallel
- <<: *linux - <<: *linux
env: SWIFT_VERSION='5.0.2' env: SWIFT_VERSION=5.0.3
name: Linux / Swift 5.0.2 name: Linux / Swift 5.0.3
- <<: *linux - <<: *linux
env: SWIFT_VERSION=5.1-DEVELOPMENT-SNAPSHOT-2019-07-03-a env: SWIFT_VERSION=5.1.3
name: Linux / Swift 5.1 (2019-07-03) name: Linux / Swift 5.1.3
- <<: *linux
env: SWIFT_VERSION=5.2-DEVELOPMENT-SNAPSHOT-2020-01-22-a
name: Linux / Swift 5.2.0-dev+2020-01-22-a
- stage: pretest - stage: pretest
name: Check Linux tests are syncd name: Check Linux tests are syncd
@@ -74,6 +86,24 @@ jobs:
osx_image: xcode10.2 osx_image: xcode10.2
- stage: deploy - stage: deploy
name: Deploy
osx_image: xcode11
env: HOMEBREW_NO_INSTALL_CLEANUP=1
install: brew install mxcl/made/swift-sh
git.depth: false
script:
- set -e
- export VERSION=$(echo $TRAVIS_TAG | cut -c 8-)
- git tag "$VERSION" --force
- git remote set-url origin "https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG.git"
- git push origin "$VERSION"
- swift sh <(curl https://raw.githubusercontent.com/mxcl/ops/master/deploy) publish-release
- git push origin :$TRAVIS_TAG
after_failure: |
export VERSION=$(echo $TRAVIS_TAG | cut -c 8-)
git push origin :$VERSION
- stage: publish
name: Jazzy name: Jazzy
osx_image: xcode10.2 osx_image: xcode10.2
install: gem install jazzy install: gem install jazzy
@@ -92,10 +122,10 @@ jobs:
- name: CocoaPods - name: CocoaPods
osx_image: xcode10.2 osx_image: xcode10.2
install: | env: HOMEBREW_NO_INSTALL_CLEANUP=1
brew install mxcl/made/swift-sh install:
curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy - brew install mxcl/made/swift-sh
chmod u+x deploy - curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy
- chmod u+x deploy
before_script: ./deploy generate-podspec before_script: ./deploy generate-podspec
script: pod trunk push script: pod trunk push
after_success: ./deploy publish-release

137
README.md
View File

@@ -44,10 +44,8 @@ Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Support mxcl
Hi, Im Max Howell and I have written a lot of open source software, and Hi, Im Max Howell and I have written a lot of open source software—generally
probably you already use some of it (Homebrew anyone?). I work full-time on a good deal of my free time 👨🏻‍💻.
open source and its hard; currently I earn *less* than minimum wage. Please
help me continue my work, I appreciate it x
<a href="https://www.patreon.com/mxcl"> <a href="https://www.patreon.com/mxcl">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160"> <img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
@@ -75,30 +73,36 @@ try JSONEncoder().encode([Path.home, Path.home/"foo"])
] ]
``` ```
However, often you want to encode relative paths: Though we recommend encoding *relative* paths:
```swift ```swift
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home encoder.userInfo[.relativePath] = Path.home
encoder.encode([Path.home, Path.home/"foo"]) encoder.encode([Path.home, Path.home/"foo", Path.home/"../baz"])
``` ```
```json ```json
[ [
"", "",
"foo", "foo",
"../baz"
] ]
``` ```
**Note** make sure you decode with this key set *also*, otherwise we `fatal` **Note** if you encode with this key set you *must* decode with the key
(unless the paths are absolute obv.) set also:
```swift ```swift
let decoder = JSONDecoder() let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home decoder.userInfo[.relativePath] = Path.home
decoder.decode(from: data) try decoder.decode(from: data) // would throw if `.relativePath` not set
``` ```
> ‡ If you are saving files to a system provided location, eg. Documents then
> the directory could change at Apples choice, or if say the user changes their
> username. Using relative paths also provides you with the flexibility in
> future to change where you are storing your files without hassle.
## Dynamic members ## Dynamic members
We support `@dynamicMemberLookup`: We support `@dynamicMemberLookup`:
@@ -111,7 +115,7 @@ We only provide this for “starting” function, eg. `Path.home` or `Bundle.pat
This is because we found in practice it was easy to write incorrect code, since This is because we found in practice it was easy to write incorrect code, since
everything would compile if we allowed arbituary variables to take *any* named everything would compile if we allowed arbituary variables to take *any* named
property as valid syntax. What we have is what you want most of the time but property as valid syntax. What we have is what you want most of the time but
much less dangerous. much less (potentially) dangerous (at runtime).
## Initializing from user-input ## Initializing from user-input
@@ -129,6 +133,28 @@ expect to be relative.
Our initializer is nameless to be consistent with the equivalent operation for Our initializer is nameless to be consistent with the equivalent operation for
converting strings to `Int`, `Float` etc. in the standard library. converting strings to `Int`, `Float` etc. in the standard library.
## Initializing from known strings
Theres no need to use the optional initializer in general if you have known
strings that you need to be paths:
```swift
let absolutePath = "/known/path"
let path1 = Path.root/pathString
let pathWithoutInitialSlash = "known/path"
let path2 = Path.root/pathWithoutInitialSlash
assert(path1 == path2)
let path3 = Path(absolutePath)! // at your options
assert(path2 == path3)
// be cautious:
let path4 = Path(pathWithoutInitialSlash)! // CRASH!
```
## Extensions ## Extensions
We have some extensions to Apple APIs: We have some extensions to Apple APIs:
@@ -170,38 +196,46 @@ let files = Path.home.ls().files
// ^^ files that both *exist* and are *not* directories // ^^ files that both *exist* and are *not* directories
let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" } let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" }
let includingHiddenFiles = Path.home.ls(.a)
``` ```
**Note** `ls()` does not throw, instead outputing a warning to the console if it
fails to list the directory. The rationale for this is weak, please open a
ticket for discussion.
We provide `find()` for recursive listing: We provide `find()` for recursive listing:
```swift ```swift
Path.home.find().execute { path in for path in Path.home.find() {
// descends all directories, and includes hidden files
// so it behaves the same as the terminal command `find`
}
```
It is configurable:
```swift
for path in Path.home.find().depth(max: 1).extension("swift").type(.file) {
// //
} }
``` ```
Which is configurable: It can be controlled with a closure syntax:
```swift ```swift
Path.home.find().maxDepth(1).extension("swift").kind(.file) { path in Path.home.find().depth(2...3).execute { path in
guard path.basename() != "foo.lock" else { return .abort }
if path.basename() == ".build", path.isDirectory { return .skip }
// //
}
```
And can be controlled:
```swift
Path.home.find().execute { path in
guard foo else { return .skip }
guard bar else { return .abort }
return .continue return .continue
} }
``` ```
Or just get all paths at once: Or get everything at once as an array:
```swift ```swift
let paths = Path.home.find().execute() let paths = Path.home.find().map(\.self)
``` ```
# `Path.swift` is robust # `Path.swift` is robust
@@ -210,8 +244,8 @@ Some parts of `FileManager` are not exactly idiomatic. For example
`isExecutableFile` returns `true` even if there is no file there, it is instead `isExecutableFile` returns `true` even if there is no file there, it is instead
telling you that *if* you made a file there it *could* be executable. Thus we telling you that *if* you made a file there it *could* be executable. Thus we
check the POSIX permissions of the file first, before returning the result of check the POSIX permissions of the file first, before returning the result of
`isExecutableFile`. `Path.swift` has done the leg-work for you so you can get on `isExecutableFile`. `Path.swift` has done the leg-work for you so you can just
with your work without worries. get on with it and not have to worry.
There is also some magic going on in Foundations filesystem APIs, which we look There is also some magic going on in Foundations filesystem APIs, which we look
for and ensure our API is deterministic, eg. [this test]. for and ensure our API is deterministic, eg. [this test].
@@ -225,7 +259,8 @@ round them where necessary.
# Rules & Caveats # Rules & Caveats
Paths are just string representations, there *might not* be a real file there. Paths are just (normalized) string representations, there *might not* be a real
file there.
```swift ```swift
Path.home/"b" // => /Users/mxcl/b Path.home/"b" // => /Users/mxcl/b
@@ -233,7 +268,7 @@ Path.home/"b" // => /Users/mxcl/b
// joining multiple strings works as youd expect // joining multiple strings works as youd expect
Path.home/"b"/"c" // => /Users/mxcl/b/c Path.home/"b"/"c" // => /Users/mxcl/b/c
// joining multiple parts at a time is fine // joining multiple parts simultaneously is fine
Path.home/"b/c" // => /Users/mxcl/b/c Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash // joining with absolute paths omits prefixed slash
@@ -266,13 +301,13 @@ Path("/foo/bar/../baz") // => /foo/baz
// symlinks are not resolved // symlinks are not resolved
Path.root.bar.symlink(as: "foo") Path.root.bar.symlink(as: "foo")
Path("foo") // => /foo Path("/foo") // => /foo
Path.foo // => /foo Path.root.foo // => /foo
// unless you do it explicitly // unless you do it explicitly
try Path.foo.readlink() // => /bar try Path.root.foo.readlink() // => /bar
// `readlink` only resolves the *final* path component, // `readlink` only resolves the *final* path component,
// thus use `realpath` if there are multiple symlinks // thus use `realpath` if there are multiple symlinks
``` ```
*Path.swift* has the general policy that if the desired end result preexists, *Path.swift* has the general policy that if the desired end result preexists,
@@ -282,7 +317,7 @@ then its a noop:
* If you try to make a directory and it already exists, we do nothing. * If you try to make a directory and it already exists, we do nothing.
* If you call `readlink` on a non-symlink, we return `self` * If you call `readlink` on a non-symlink, we return `self`
However notably if you try to copy or move a file with specifying `overwrite` However notably if you try to copy or move a file without specifying `overwrite`
and the file already exists at the destination and is identical, we dont check and the file already exists at the destination and is identical, we dont check
for that as the check was deemed too expensive to be worthwhile. for that as the check was deemed too expensive to be worthwhile.
@@ -293,14 +328,14 @@ for that as the check was deemed too expensive to be worthwhile.
equality check is required. equality check is required.
* There are several symlink paths on Mac that are typically automatically * There are several symlink paths on Mac that are typically automatically
resolved by Foundation, eg. `/private`, we attempt to do the same for resolved by Foundation, eg. `/private`, we attempt to do the same for
functions that you would expect it (notably `realpath`), we *do* the same for functions that you would expect it (notably `realpath`), we *do* the same
`Path.init`, but *do not* if you are joining a path that ends up being one of for `Path.init`, but *do not* if you are joining a path that ends up being
these paths, (eg. `Path.root.join("var/private')`). one of these paths, (eg. `Path.root.join("var/private')`).
If a `Path` is a symlink but the destination of the link does not exist `exists` If a `Path` is a symlink but the destination of the link does not exist `exists`
returns `false`. This seems to be the correct thing to do since symlinks are returns `false`. This seems to be the correct thing to do since symlinks are
meant to be an abstraction for filesystems. To instead verify that there is meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `kind` is `nil`. no filesystem entry there at all check if `type` is `nil`.
## We do not provide change directory functionality ## We do not provide change directory functionality
@@ -318,8 +353,8 @@ Apple recommend this because they provide a magic translation for
file:///.file/id=6571367.15106761 file:///.file/id=6571367.15106761
Therefore, if you are not using this feature you are fine. If you have URLs the correct Therefore, if you are not using this feature you are fine. If you have URLs the
way to get a `Path` is: correct way to get a `Path` is:
```swift ```swift
if let path = Path(url: url) { if let path = Path(url: url) {
@@ -332,33 +367,31 @@ actual filesystem path, however we also check the URL has a `file` scheme first.
[file-refs]: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl [file-refs]: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
# In defense of our naming scheme
Chainable syntax demands short method names, thus we adopted the naming scheme
of the terminal, which is absolutely not very “Apple” when it comes to how they
design their APIs, however for users of the terminal (which *surely* is most
developers) it is snappy and familiar.
# Installation # Installation
SwiftPM: SwiftPM:
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.13.0")) package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "1.0.0"))
``` ```
CocoaPods: CocoaPods:
```ruby ```ruby
pod 'Path.swift', '~> 0.13' pod 'Path.swift', '~> 1.0.0'
``` ```
Carthage: Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945). > Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Pre1.0 status
We are pre 1.0, thus we can change the API as we like, and we will (to the
pursuit of getting it *right*)! We will tag 1.0 as soon as possible.
### Get push notifications for new releases
https://mxcl.dev/canopy/
# Alternatives # Alternatives
* [Pathos](https://github.com/dduan/Pathos) by Daniel Duan * [Pathos](https://github.com/dduan/Pathos) by Daniel Duan
@@ -368,7 +401,7 @@ https://mxcl.dev/canopy/
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg [badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg
[badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0-orange.svg [badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-orange.svg
[docs]: https://mxcl.dev/Path.swift/Structs/Path.html [docs]: https://mxcl.dev/Path.swift/Structs/Path.html
[badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true [badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true
[badge-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg [badge-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg

View File

@@ -30,6 +30,29 @@ public extension Pathish {
} }
} }
/// The type of the entry.
/// - SeeAlso: `Path.EntryType`
@available(*, deprecated, message: "- SeeAlso: Path.type")
var kind: Path.EntryType? {
return type
}
/// The type of the entry.
/// - SeeAlso: `Path.EntryType`
var type: Path.EntryType? {
var buf = stat()
guard lstat(string, &buf) == 0 else {
return nil
}
if buf.st_mode & S_IFMT == S_IFLNK {
return .symlink
} else if buf.st_mode & S_IFMT == S_IFDIR {
return .directory
} else {
return .file
}
}
/** /**
Sets the files attributes using UNIX octal notation. Sets the files attributes using UNIX octal notation.
@@ -40,6 +63,8 @@ public extension Pathish {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string) try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return Path(self) return Path(self)
} }
//MARK: Filesystem Locking
/** /**
Applies the macOS filesystem lock attribute. Applies the macOS filesystem lock attribute.
@@ -83,24 +108,17 @@ public extension Pathish {
#endif #endif
return Path(self) return Path(self)
} }
var kind: Path.Kind? {
var buf = stat()
guard lstat(string, &buf) == 0 else {
return nil
}
if buf.st_mode & S_IFMT == S_IFLNK {
return .symlink
} else if buf.st_mode & S_IFMT == S_IFDIR {
return .directory
} else {
return .file
}
}
} }
/// The `extension` that provides `Kind`.
public extension Path { public extension Path {
enum Kind { /// A filesystem entrys kind, file, directory, symlink etc.
case file, symlink, directory enum EntryType: CaseIterable {
/// The entry is a file.
case file
/// The entry is a symlink.
case symlink
/// The entry is a directory.
case directory
} }
} }

View File

@@ -1,8 +1,9 @@
import Foundation import Foundation
/// The `extension` that provides static properties that are common directories.
extension Path { extension Path {
//MARK: Common Directories //MARK: Common Directories
/// Returns a `Path` containing `FileManager.default.currentDirectoryPath`. /// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
public static var cwd: DynamicPath { public static var cwd: DynamicPath {
return .init(string: FileManager.default.currentDirectoryPath) return .init(string: FileManager.default.currentDirectoryPath)

View File

@@ -4,8 +4,9 @@ import Glibc
#endif #endif
public extension Pathish { public extension Pathish {
//MARK: File Management //MARK: File Management
/** /**
Copies a file. Copies a file.
@@ -25,11 +26,11 @@ public extension Pathish {
*/ */
@discardableResult @discardableResult
func copy<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path { func copy<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, let tokind = to.kind, tokind != .directory, kind != .directory { if overwrite, let tokind = to.type, tokind != .directory, type != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
#if os(Linux) && !swift(>=5.2) // check if fixed #if os(Linux) && !swift(>=5.3) // check if fixed
if !overwrite, to.kind != nil { if !overwrite, to.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -61,15 +62,15 @@ public extension Pathish {
*/ */
@discardableResult @discardableResult
func copy<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path { func copy<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
if into.kind == nil { if into.type == nil {
try into.mkdir(.p) try into.mkdir(.p)
} }
let rv = into/basename() let rv = into/basename()
if overwrite, let kind = rv.kind, kind != .directory { if overwrite, let kind = rv.type, kind != .directory {
try FileManager.default.removeItem(at: rv.url) try FileManager.default.removeItem(at: rv.url)
} }
#if os(Linux) && !swift(>=5.2) // check if fixed #if os(Linux) && !swift(>=5.3) // check if fixed
if !overwrite, rv.kind != nil { if !overwrite, rv.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -95,7 +96,7 @@ public extension Pathish {
*/ */
@discardableResult @discardableResult
func move<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path { func move<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, let kind = to.kind, kind != .directory { if overwrite, let kind = to.type, kind != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
try FileManager.default.moveItem(at: url, to: to.url) try FileManager.default.moveItem(at: url, to: to.url)
@@ -119,13 +120,13 @@ public extension Pathish {
*/ */
@discardableResult @discardableResult
func move<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path { func move<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
switch into.kind { switch into.type {
case nil: case nil:
try into.mkdir(.p) try into.mkdir(.p)
fallthrough fallthrough
case .directory?: case .directory?:
let rv = into/basename() let rv = into/basename()
if overwrite, let rvkind = rv.kind, rvkind != .directory { if overwrite, let rvkind = rv.type, rvkind != .directory {
try FileManager.default.removeItem(at: rv.url) try FileManager.default.removeItem(at: rv.url)
} }
try FileManager.default.moveItem(at: url, to: rv.url) try FileManager.default.moveItem(at: url, to: rv.url)
@@ -147,7 +148,7 @@ public extension Pathish {
*/ */
@inlinable @inlinable
func delete() throws { func delete() throws {
if kind != nil { if type != nil {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
} }
@@ -159,7 +160,7 @@ public extension Pathish {
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
if kind == nil { if type == nil {
guard FileManager.default.createFile(atPath: string, contents: nil) else { guard FileManager.default.createFile(atPath: string, contents: nil) else {
throw CocoaError.error(.fileWriteUnknown) throw CocoaError.error(.fileWriteUnknown)
} }
@@ -203,7 +204,7 @@ public extension Pathish {
} }
/** /**
Renames the file at path. Renames the file (basename only) at path.
Path.root.foo.bar.rename(to: "baz") // => /foo/baz Path.root.foo.bar.rename(to: "baz") // => /foo/baz
@@ -233,7 +234,7 @@ public extension Pathish {
*/ */
@discardableResult @discardableResult
func symlink<P: Pathish>(into dir: P) throws -> Path { func symlink<P: Pathish>(into dir: P) throws -> Path {
switch dir.kind { switch dir.type {
case nil, .symlink?: case nil, .symlink?:
try dir.mkdir(.p) try dir.mkdir(.p)
fallthrough fallthrough

View File

@@ -1,32 +1,110 @@
import Foundation import Foundation
public extension Path { public extension Path {
/// The builder for `Path.find()`
class Finder { class Finder {
fileprivate init(path: Path) { fileprivate init(path: Path) {
self.path = path self.path = path
self.enumerator = FileManager.default.enumerator(atPath: path.string)
} }
/// The `path` find operations operate on.
public let path: Path public let path: Path
fileprivate(set) public var maxDepth: Int? = nil
fileprivate(set) public var kinds: Set<Path.Kind>? private let enumerator: FileManager.DirectoryEnumerator!
fileprivate(set) public var extensions: Set<String>?
/// The range of directory depths for which the find operation will return entries.
private(set) public var depth: ClosedRange<Int> = 1...Int.max {
didSet {
if depth.lowerBound < 0 {
depth = 0...depth.upperBound
}
}
}
/// The kinds of filesystem entries find operations will return.
public var types: Set<EntryType> {
return _types ?? Set(EntryType.allCases)
}
private var _types: Set<EntryType>?
/// The file extensions find operations will return. Files *and* directories unless you filter for `kinds`.
private(set) public var extensions: Set<String>?
} }
} }
extension Path.Finder: Sequence, IteratorProtocol {
public func next() -> Path? {
guard let enumerator = enumerator else {
return nil
}
while let relativePath = enumerator.nextObject() as? String {
let path = self.path/relativePath
#if !os(Linux) || swift(>=5.0)
if enumerator.level > depth.upperBound {
enumerator.skipDescendants()
continue
}
if enumerator.level < depth.lowerBound {
continue
}
#endif
if let type = path.type, !types.contains(type) { continue }
if let exts = extensions, !exts.contains(path.extension) { continue }
return path
}
return nil
}
public typealias Element = Path
}
public extension Path.Finder { public extension Path.Finder {
/// Multiple calls will configure the Finder for the final depth call only. /// A max depth of `0` returns only the path we are searching, `1` is that directorys listing.
func maxDepth(_ maxDepth: Int) -> Path.Finder { func depth(max maxDepth: Int) -> Path.Finder {
#if os(Linux) && !swift(>=5.0) #if os(Linux) && !swift(>=5.0)
fputs("warning: maxDepth not implemented for Swift < 5\n", stderr) fputs("warning: depth not implemented for Swift < 5\n", stderr)
#endif #endif
self.maxDepth = maxDepth depth = Swift.min(maxDepth, depth.lowerBound)...maxDepth
return self
}
/// A min depth of `0` also returns the path we are searching, `1` is that directorys listing. Default is `1` thus not returning ourself.
func depth(min minDepth: Int) -> Path.Finder {
#if os(Linux) && !swift(>=5.0)
fputs("warning: depth not implemented for Swift < 5\n", stderr)
#endif
depth = minDepth...Swift.max(depth.upperBound, minDepth)
return self
}
/// A max depth of `0` returns only the path we are searching, `1` is that directorys listing.
/// A min depth of `0` also returns the path we are searching, `1` is that directorys listing. Default is `1` thus not returning ourself.
func depth(_ rng: Range<Int>) -> Path.Finder {
#if os(Linux) && !swift(>=5.0)
fputs("warning: depth not implemented for Swift < 5\n", stderr)
#endif
depth = rng.lowerBound...(rng.upperBound - 1)
return self
}
/// A max depth of `0` returns only the path we are searching, `1` is that directorys listing.
/// A min depth of `0` also returns the path we are searching, `1` is that directorys listing. Default is `1` thus not returning ourself.
func depth(_ rng: ClosedRange<Int>) -> Path.Finder {
#if os(Linux) && !swift(>=5.0)
fputs("warning: depth not implemented for Swift < 5\n", stderr)
#endif
depth = rng
return self return self
} }
/// Multiple calls will configure the Finder with multiple kinds. /// Multiple calls will configure the Finder with multiple kinds.
func kind(_ kind: Path.Kind) -> Path.Finder { func type(_ type: Path.EntryType) -> Path.Finder {
kinds = kinds ?? [] _types = _types ?? []
kinds!.insert(kind) _types!.insert(type)
return self return self
} }
@@ -37,13 +115,6 @@ public extension Path.Finder {
return self return self
} }
/// Enumerate and return all results, note that this may take a while since we are recursive.
func execute() -> [Path] {
var rv: [Path] = []
execute{ rv.append($0); return .continue }
return rv
}
/// The return type for `Path.Finder` /// The return type for `Path.Finder`
enum ControlFlow { enum ControlFlow {
/// Stop enumerating this directory, return to the parent. /// Stop enumerating this directory, return to the parent.
@@ -56,35 +127,26 @@ public extension Path.Finder {
/// Enumerate, one file at a time. /// Enumerate, one file at a time.
func execute(_ closure: (Path) throws -> ControlFlow) rethrows { func execute(_ closure: (Path) throws -> ControlFlow) rethrows {
guard let finder = FileManager.default.enumerator(atPath: path.string) else { while let path = next() {
fputs("warning: could not enumerate: \(path)\n", stderr)
return
}
while let relativePath = finder.nextObject() as? String {
#if !os(Linux) || swift(>=5.0)
if let maxDepth = maxDepth, finder.level > maxDepth {
finder.skipDescendants()
}
#endif
let path = self.path/relativePath
if path == self.path { continue }
if let kinds = kinds, let kind = path.kind, !kinds.contains(kind) { continue }
if let exts = extensions, !exts.contains(path.extension) { continue }
switch try closure(path) { switch try closure(path) {
case .skip: case .skip:
finder.skipDescendants() #if !os(Linux) || swift(>=5.0)
enumerator.skipDescendants()
#else
fputs("warning: skip is not implemented for Swift < 5.0\n", stderr)
#endif
case .abort: case .abort:
return return
case .continue: case .continue:
break continue
} }
} }
} }
} }
public extension Pathish { public extension Pathish {
//MARK: Directory Listings
//MARK: Directory Listing
/** /**
Same as the `ls` command output is shallow and unsorted. Same as the `ls` command output is shallow and unsorted.
@@ -105,12 +167,13 @@ public extension Pathish {
} }
} }
/// Recursively find files under this path. If the path is a file, no files will be found.
func find() -> Path.Finder { func find() -> Path.Finder {
return .init(path: Path(self)) return .init(path: Path(self))
} }
} }
/// Convenience functions for the arraies of `Path` /// Convenience functions for the arrays of `Path`
public extension Array where Element == Path { public extension Array where Element == Path {
/// Filters the list of entries to be a list of Paths that are directories. Symlinks to directories are not returned. /// Filters the list of entries to be a list of Paths that are directories. Symlinks to directories are not returned.
var directories: [Path] { var directories: [Path] {
@@ -123,12 +186,17 @@ public extension Array where Element == Path {
/// - Note: symlinks that point to files that do not exist are *not* returned. /// - Note: symlinks that point to files that do not exist are *not* returned.
var files: [Path] { var files: [Path] {
return filter { return filter {
$0.exists && !$0.isDirectory switch $0.type {
case .none, .directory?:
return false
case .file?, .symlink?:
return true
}
} }
} }
} }
/// Options for `Path.mkdir(_:)` /// Options for `Path.ls(_:)`
public enum ListDirectoryOptions { public enum ListDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`. /// Creates intermediary directories; works the same as `mkdir -p`.
case a case a

View File

@@ -6,11 +6,13 @@ import Darwin
#endif #endif
public extension Pathish { public extension Pathish {
//MARK: Filesystem Properties //MARK: Filesystem Properties
/** /**
- Returns: `true` if the path represents an actual filesystem entry. - Returns: `true` if the path represents an actual filesystem entry.
- Note: If `self` is a symlink the return value represents the destination. - Note: If `self` is a symlink the return value represents the destination, thus if the destination doesnt exist this returns `false` even if the symlink exists.
- Note: To determine if a symlink exists, use `.type`.
*/ */
var exists: Bool { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)

View File

@@ -36,6 +36,7 @@ let _realpath = Glibc.realpath
to the anti-pattern where Path.swift suddenly feels like Javascript otherwise. to the anti-pattern where Path.swift suddenly feels like Javascript otherwise.
- Note: A `Path` does not necessarily represent an actual filesystem entry. - Note: A `Path` does not necessarily represent an actual filesystem entry.
- SeeAlso: `Pathish` for most methods you will use on `Path` instances.
*/ */
public struct Path: Pathish { public struct Path: Pathish {
@@ -135,6 +136,8 @@ public struct Path: Pathish {
} }
public extension Pathish { public extension Pathish {
//MARK: Filesystem Representation
/// Returns a `URL` representing this file path. /// Returns a `URL` representing this file path.
var url: URL { var url: URL {
return URL(fileURLWithPath: string) return URL(fileURLWithPath: string)
@@ -173,7 +176,7 @@ public extension Pathish {
Returns the filename extension of this path. Returns the filename extension of this path.
- Remark: If there is no extension returns "". - Remark: If there is no extension returns "".
- Remark: If the filename ends with any number of ".", returns "". - Remark: If the filename ends with any number of ".", returns "".
- Note: We special case eg. `foo.tar.gz`. - Note: We special case eg. `foo.tar.gz`there are a limited number of these specializations, feel free to PR more.
*/ */
@inlinable @inlinable
var `extension`: String { var `extension`: String {
@@ -200,14 +203,14 @@ public extension Pathish {
/** /**
Splits the string representation on the directory separator. Splits the string representation on the directory separator.
- Important: The first element is always "/" to be consistent with `NSString.pathComponents`. - Important: `NSString.pathComponents` will always return an initial `/` in its array for absolute paths to indicate that the path was absolute, we dont do this because we are *always* absolute paths.
*/ */
@inlinable @inlinable
var components: [String] { var components: [String] {
return ["/"] + string.split(separator: "/").map(String.init) return string.split(separator: "/").map(String.init)
} }
//MARK: Pathing //MARK:- Pathing
/** /**
Joins a path and a string to produce a new path. Joins a path and a string to produce a new path.
@@ -404,7 +407,7 @@ private func join_<S>(prefix: String, pathComponents: S) -> String where S: Sequ
return rv return rv
} }
/// A path that supports arbituary dot notation, eg. Path.root.usr.bin /// A path that supports arbituary dot notation, eg. `Path.root.usr.bin`
@dynamicMemberLookup @dynamicMemberLookup
public struct DynamicPath: Pathish { public struct DynamicPath: Pathish {
/// The normalized string representation of the underlying filesystem path /// The normalized string representation of the underlying filesystem path
@@ -416,7 +419,7 @@ public struct DynamicPath: Pathish {
} }
/// Converts a `Path` to a `DynamicPath` /// Converts a `Path` to a `DynamicPath`
public init(_ path: Path) { public init<P: Pathish>(_ path: P) {
string = path.string string = path.string
} }

View File

@@ -1,13 +1,6 @@
/// A type that represents a filesystem path, if you conform your type /// A type that represents a filesystem path, if you conform your type
/// to `Pathish` it is your responsibility to ensure the string is correctly normalized /// to `Pathish` it is your responsibility to ensure the string is correctly normalized
public protocol Pathish: Hashable, Comparable { public protocol Pathish: Hashable, Comparable {
/// The normalized string representation of the underlying filesystem path /// The normalized string representation of the underlying filesystem path
var string: String { get } var string: String { get }
} }
public extension Pathish {
static func ==<P: Pathish> (lhs: Self, rhs: P) -> Bool {
return lhs.string == rhs.string
}
}

View File

@@ -2,39 +2,138 @@ import XCTest
import Path import Path
extension PathTests { extension PathTests {
func testFindMaxDepth0() throws { func testFindMaxDepth1() throws {
#if !os(Linux) || swift(>=5)
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
try tmpdir.a.touch() try tmpdir.a.touch()
try tmpdir.b.touch() try tmpdir.b.touch()
try tmpdir.c.mkdir().join("e").touch() try tmpdir.c.mkdir().join("e").touch()
XCTAssertEqual( do {
Set(tmpdir.find().maxDepth(0).execute()), let finder = tmpdir.find().depth(max: 1)
Set([tmpdir.a, tmpdir.b, tmpdir.c].map(Path.init))) XCTAssertEqual(finder.depth, 1...1)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(Set(finder), Set([tmpdir.a, tmpdir.b, tmpdir.c].map(Path.init)))
#endif
}
do {
let finder = tmpdir.find().depth(max: 0)
XCTAssertEqual(finder.depth, 0...0)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(Set(finder), Set())
#endif
}
} }
#endif
} }
func testFindMaxDepth1() throws { func testFindMaxDepth2() throws {
#if !os(Linux) || swift(>=5)
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
try tmpdir.a.touch() try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch() try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch() try tmpdir.b.d.mkdir().join("e").touch()
#if !os(Linux) do {
XCTAssertEqual( let finder = tmpdir.find().depth(max: 2)
Set(tmpdir.find().maxDepth(1).execute()), XCTAssertEqual(finder.depth, 1...2)
Set([tmpdir.a, tmpdir.b, tmpdir.b.c].map(Path.init))) #if !os(Linux) || swift(>=5)
#else XCTAssertEqual(
// Linux behavior is different :-/ Set(finder),
XCTAssertEqual( Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c].map(Path.init)))
Set(tmpdir.find().maxDepth(1).execute()), #endif
Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c].map(Path.init))) }
#endif do {
let finder = tmpdir.find().depth(max: 3)
XCTAssertEqual(finder.depth, 1...3)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e].map(Path.init)))
#endif
}
}
}
func testFindMinDepth() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let finder = tmpdir.find().depth(min: 2)
XCTAssertEqual(finder.depth, 2...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindDepth0() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let finder = tmpdir.find().depth(min: 0)
XCTAssertEqual(finder.depth, 0...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
do {
// this should work, even though its weird
let finder = tmpdir.find().depth(min: -1)
XCTAssertEqual(finder.depth, 0...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindDepthRange() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let range = 2...3
let finder = tmpdir.find().depth(range)
XCTAssertEqual(finder.depth, range)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e, tmpdir.b.d.f].map(Path.init)),
relativeTo: tmpdir)
#endif
}
do {
let range = 2..<4
let finder = tmpdir.find().depth(range)
XCTAssertEqual(finder.depth, 2...3)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e, tmpdir.b.d.f].map(Path.init)),
relativeTo: tmpdir)
#endif
}
} }
#endif
} }
func testFindExtension() throws { func testFindExtension() throws {
@@ -43,28 +142,88 @@ extension PathTests {
try tmpdir.join("bar.txt").touch() try tmpdir.join("bar.txt").touch()
XCTAssertEqual( XCTAssertEqual(
Set(tmpdir.find().extension("json").execute()), Set(tmpdir.find().extension("json")),
[tmpdir.join("foo.json")]) [tmpdir.join("foo.json")])
XCTAssertEqual( XCTAssertEqual(
Set(tmpdir.find().extension("txt").extension("json").execute()), Set(tmpdir.find().extension("txt").extension("json")),
[tmpdir.join("foo.json"), tmpdir.join("bar.txt")]) [tmpdir.join("foo.json"), tmpdir.join("bar.txt")])
} }
} }
func testFindKinds() throws { //NOTE this is how iterators work, so we have a test to validate that behavior
func testFindCallingExecuteTwice() throws {
try Path.mktemp { tmpdir in
try tmpdir.join("foo.json").touch()
try tmpdir.join("bar.txt").touch()
let finder = tmpdir.find()
XCTAssertEqual(finder.map{ $0 }.count, 2)
XCTAssertEqual(finder.map{ $0 }.count, 0)
}
}
func testFindExecute() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
#if !os(Linux) || swift(>=5)
do {
var rv = Set<Path>()
tmpdir.find().execute {
switch $0 {
case Path(tmpdir.b.d): return .skip
default:
rv.insert($0)
return .continue
}
}
XCTAssertEqual(rv, Set([tmpdir.a, tmpdir.b, tmpdir.b.c].map(Path.init)))
}
#endif
do {
var x = 0
tmpdir.find().execute { _ in
x += 1
return .abort
}
XCTAssertEqual(x, 1)
}
}
}
func testFindTypes() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
try tmpdir.foo.mkdir() try tmpdir.foo.mkdir()
try tmpdir.bar.touch() try tmpdir.bar.touch()
XCTAssertEqual( XCTAssertEqual(
Set(tmpdir.find().kind(.file).execute()), Set(tmpdir.find().type(.file)),
[tmpdir.join("bar")]) [tmpdir.join("bar")])
XCTAssertEqual( XCTAssertEqual(
Set(tmpdir.find().kind(.directory).execute()), Set(tmpdir.find().type(.directory)),
[tmpdir.join("foo")]) [tmpdir.join("foo")])
XCTAssertEqual( XCTAssertEqual(
Set(tmpdir.find().kind(.file).kind(.directory).execute()), Set(tmpdir.find().type(.file).type(.directory)),
Set(["foo", "bar"].map(tmpdir.join))) Set(["foo", "bar"].map(tmpdir.join)))
} }
} }
func testLsOnNonexistentDirectoryReturnsEmptyArray() throws {
try Path.mktemp { tmpdir in
XCTAssertEqual(tmpdir.a.ls(), [])
}
}
func testFindOnNonexistentDirectoryHasNoContent() throws {
try Path.mktemp { tmpdir in
XCTAssertNil(tmpdir.a.find().next())
}
}
} }

View File

@@ -76,10 +76,10 @@ class PathTests: XCTestCase {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.exists) XCTAssertTrue(tmpdir.exists)
XCTAssertFalse(try tmpdir.bar.symlink(as: tmpdir.foo).exists) XCTAssertFalse(try tmpdir.bar.symlink(as: tmpdir.foo).exists)
XCTAssertTrue(tmpdir.foo.kind == .symlink) XCTAssertTrue(tmpdir.foo.type == .symlink)
XCTAssertTrue(try tmpdir.bar.touch().symlink(as: tmpdir.baz).exists) XCTAssertTrue(try tmpdir.bar.touch().symlink(as: tmpdir.baz).exists)
XCTAssertTrue(tmpdir.bar.kind == .file) XCTAssertTrue(tmpdir.bar.type == .file)
XCTAssertTrue(tmpdir.kind == .directory) XCTAssertTrue(tmpdir.type == .directory)
} }
} }
@@ -146,15 +146,21 @@ class PathTests: XCTestCase {
].map(Path.init) ].map(Path.init)
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root
let data = try encoder.encode(input)
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"]) func test<P: Pathish>(relativePath: P, line: UInt = #line) throws {
encoder.userInfo[.relativePath] = relativePath
let data = try encoder.encode(input)
let decoder = JSONDecoder() XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"], line: line)
XCTAssertThrowsError(try decoder.decode([Path].self, from: data))
decoder.userInfo[.relativePath] = root let decoder = JSONDecoder()
XCTAssertEqual(try decoder.decode([Path].self, from: data), input) XCTAssertThrowsError(try decoder.decode([Path].self, from: data), line: line)
decoder.userInfo[.relativePath] = relativePath
XCTAssertEqual(try decoder.decode([Path].self, from: data), input, line: line)
}
try test(relativePath: root) // DynamicPath
try test(relativePath: Path(root)) // Path
} }
func testJoin() { func testJoin() {
@@ -313,6 +319,8 @@ class PathTests: XCTestCase {
func testStringConvertibles() { func testStringConvertibles() {
XCTAssertEqual(Path.root.description, "/") XCTAssertEqual(Path.root.description, "/")
XCTAssertEqual(Path.root.debugDescription, "Path(/)") XCTAssertEqual(Path.root.debugDescription, "Path(/)")
XCTAssertEqual(Path(Path.root).description, "/")
XCTAssertEqual(Path(Path.root).debugDescription, "Path(/)")
} }
func testFilesystemAttributes() throws { func testFilesystemAttributes() throws {
@@ -393,17 +401,20 @@ class PathTests: XCTestCase {
// regression test: can delete a symlink that points to a non-existent file // regression test: can delete a symlink that points to a non-existent file
let bar5 = try tmpdir.bar4.symlink(as: tmpdir.bar5) let bar5 = try tmpdir.bar4.symlink(as: tmpdir.bar5)
XCTAssertEqual(bar5.kind, .symlink) XCTAssertEqual(bar5.type, .symlink)
XCTAssertFalse(bar5.exists) XCTAssertFalse(bar5.exists)
XCTAssertNoThrow(try bar5.delete()) XCTAssertNoThrow(try bar5.delete())
XCTAssertEqual(bar5.kind, nil) XCTAssertEqual(bar5.type, nil)
// test that deleting a symlink *only* deletes the symlink // test that deleting a symlink *only* deletes the symlink
let bar7 = try tmpdir.bar6.touch().symlink(as: tmpdir.bar7) let bar7 = try tmpdir.bar6.touch().symlink(as: tmpdir.bar7)
XCTAssertEqual(bar7.kind, .symlink) XCTAssertEqual(bar7.type, .symlink)
XCTAssertTrue(bar7.exists) XCTAssertTrue(bar7.exists)
XCTAssertNoThrow(try bar7.delete()) XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.kind, nil) XCTAssertEqual(bar7.type, nil)
XCTAssertEqual(tmpdir.bar6.type, .file)
// for code-coverage
XCTAssertEqual(tmpdir.bar6.kind, .file) XCTAssertEqual(tmpdir.bar6.kind, .file)
} }
} }
@@ -619,8 +630,8 @@ class PathTests: XCTestCase {
} }
func testPathComponents() throws { func testPathComponents() throws {
XCTAssertEqual(Path.root.foo.bar.components, ["/", "foo", "bar"]) XCTAssertEqual(Path.root.foo.bar.components, ["foo", "bar"])
XCTAssertEqual(Path.root.components, ["/"]) XCTAssertEqual(Path.root.components, [])
} }
func testFlatMap() throws { func testFlatMap() throws {
@@ -637,9 +648,9 @@ class PathTests: XCTestCase {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch() let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar) let bar = try foo.symlink(as: tmpdir.bar)
XCTAssertEqual(tmpdir.kind, .directory) XCTAssertEqual(tmpdir.type, .directory)
XCTAssertEqual(foo.kind, .file) XCTAssertEqual(foo.type, .file)
XCTAssertEqual(bar.kind, .symlink) XCTAssertEqual(bar.type, .symlink)
} }
} }
} }

View File

@@ -24,16 +24,23 @@ extension PathTests {
("testFileHandleExtensions", testFileHandleExtensions), ("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference), ("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes), ("testFilesystemAttributes", testFilesystemAttributes),
("testFindCallingExecuteTwice", testFindCallingExecuteTwice),
("testFindDepth0", testFindDepth0),
("testFindDepthRange", testFindDepthRange),
("testFindExecute", testFindExecute),
("testFindExtension", testFindExtension), ("testFindExtension", testFindExtension),
("testFindKinds", testFindKinds),
("testFindMaxDepth0", testFindMaxDepth0),
("testFindMaxDepth1", testFindMaxDepth1), ("testFindMaxDepth1", testFindMaxDepth1),
("testFindMaxDepth2", testFindMaxDepth2),
("testFindMinDepth", testFindMinDepth),
("testFindOnNonexistentDirectoryHasNoContent", testFindOnNonexistentDirectoryHasNoContent),
("testFindTypes", testFindTypes),
("testFlatMap", testFlatMap), ("testFlatMap", testFlatMap),
("testInitializerForRelativePath", testInitializerForRelativePath), ("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testKind", testKind), ("testKind", testKind),
("testLock", testLock), ("testLock", testLock),
("testLsOnNonexistentDirectoryReturnsEmptyArray", testLsOnNonexistentDirectoryReturnsEmptyArray),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto), ("testMoveInto", testMoveInto),

11
Tests/PathTests/etc.swift Normal file
View File

@@ -0,0 +1,11 @@
import XCTest
import Path
func XCTAssertEqual<P: Pathish>(_ set1: Set<Path>, _ set2: Set<Path>, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line, relativeTo: P) {
if set1 != set2 {
let cvt: (Path) -> String = { $0.relative(to: relativeTo) }
let out1 = set1.map(cvt).sorted()
let out2 = set1.map(cvt).sorted()
XCTFail("Set(\(out1)) is not equal to Set(\(out2))", file: file, line: line)
}
}