Compare commits

...

27 Commits

Author SHA1 Message Date
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
Max Howell
af091cc1f0 Split this test so I can figure out CI failure 2019-07-21 18:58:50 -04:00
Max Howell
e5188bf93b Still not fixed 2019-07-21 18:58:49 -04:00
Max Howell
462a62920f Update Swifts in CI; Test Xcode 11 2019-07-21 18:58:49 -04:00
Max Howell
45b0b59a94 Better rx for tagged version detection on Travis 2019-07-21 17:37:10 -04:00
Max Howell
62073d584b Remove Entry since it is barely worthwhile sugar 2019-07-21 17:37:10 -04:00
Max Howell
621d1b0160 Remove @dynamicMember generally 2019-07-21 17:37:10 -04:00
Max Howell
d2bb2a1fdc Path.find() 2019-07-21 17:22:38 -04:00
Max Howell
38e98ee7fd Jazzy requires Xcode 10.2 now (per their docs) 2019-07-21 09:07:03 -04:00
Dash2507
dac007e907 Change "pkg" to "package" (#50) 2019-06-10 17:20:05 -04:00
Max Howell
b6b4a74a26 Move deploy script to @mxcl/ops 2019-04-14 12:40:07 -04:00
Max Howell
b76db41ca4 422 means the release already exists, so… succeed 2019-04-14 10:49:17 -04:00
Max Howell
8d5d67b81b Merge pull request #48 from mxcl/travis-swift-5.0-GM
[travis] Swift 5 GM
2019-03-25 22:02:33 -04:00
Max Howell
21ddc7dc3a [travis] Swift 5 GM 2019-03-25 21:40:17 -04:00
Max Howell
f324b4a562 Merge pull request #47 from mxcl/fix-symlink-delete
Adds `kind` fixes deleting broken symlinks
2019-03-18 19:56:39 -04:00
Max Howell
0e061f9cc8 Adds kind fixes deleting broken symlinks
`delete()` and other functions would check `exists` to do certain behaviors, but `exists` will validate a symlink if the entry is a symlink, thus instead we check if the path is an actual entry now instead.
2019-03-18 19:47:49 -04:00
Max Howell
7e774b6cf5 Use 10.2 image for pretest
Also some tidy
2019-03-06 15:55:51 -05:00
Max Howell
8b371fa5d2 Allow initialization from Substring etc. 2019-03-06 15:55:51 -05:00
Max Howell
02fd579f19 mxcl.github.io -> mxcl.dev
[ci skip]
2019-02-28 18:40:19 -05:00
repo-ranger[bot]
e915bc0cfb Merge pull request #43 from mxcl/Bundle.executable
Add Bundle.executable
2019-02-17 15:14:17 +00:00
Max Howell
f4c2c75aa1 Add Bundle.executable 2019-02-17 10:05:30 -05:00
repo-ranger[bot]
dc7affa28c Merge pull request #42 from mxcl/path-components
Add Path.components
2019-02-15 18:49:13 +00:00
Max Howell
476cdc1461 Add Path.components 2019-02-15 13:38:36 -05:00
Max Howell
a644208c62 Test Swift 4.0.3 also 2019-02-13 21:34:35 -05:00
Max Howell
d7a9819350 Fix publishing the release in the deploy stage
[ci skip]
2019-02-13 20:52:39 -05:00
20 changed files with 721 additions and 431 deletions

174
.github/deploy vendored
View File

@@ -1,174 +0,0 @@
#!/usr/bin/swift sh
import func Darwin.fputs
import var Darwin.stderr
import PMKFoundation // PromiseKit/Foundation ~> 3.3
import LegibleError // @mxcl ~> 1.0
import Foundation
import PromiseKit // @mxcl ~> 6.8
import Path // mxcl/Path.swift == master
let env = ProcessInfo.processInfo.environment
let token = env["GITHUB_TOKEN"] ?? env["GITHUB_ACCESS_TOKEN"]!
let slug = env["TRAVIS_REPO_SLUG"]!
let tag = env["TRAVIS_TAG"]!
func fatal(message: String) -> Never {
fputs("error: \(message)\n", stderr)
exit(1)
}
func fatal(error: Error) -> Never {
fatal(message: error.legibleLocalizedDescription)
}
guard let licenseFile = try Path.cwd.ls().files.first(where: {
$0.basename().hasPrefix("LICENSE")
})?.basename() else {
fatal(message: "no LICENSE file found")
}
struct Repo: Decodable {
let description: String
let license: License
struct License: Decodable {
let spdx_id: String
}
}
struct Package: Decodable {
let swiftLanguageVersions: [String]
let targets: [Target]
struct Target: Decodable {
let path: String?
let type: Kind
enum Kind: String, Decodable {
case regular
case test
}
}
}
extension URLRequest {
init(github path: String) {
let url = URL(string: "https://api.github.com\(path)")!
self.init(url: url)
setValue("token \(token)", forHTTPHeaderField: "Authorization")
setValue("application/json", forHTTPHeaderField: "Content-Type")
setValue("application/json", forHTTPHeaderField: "Accept")
}
}
func description() -> Promise<Repo> {
let rq = URLRequest(github: "/repos/\(slug)")
return firstly {
URLSession.shared.dataTask(.promise, with: rq).validate()
}.map { data, _ in
try JSONDecoder().decode(Repo.self, from: data)
}
}
struct User: Decodable {
let name: String
let email: String
}
func email() -> Promise<User> {
let rq = URLRequest(github: "/user")
return firstly {
URLSession.shared.dataTask(.promise, with: rq).validate()
}.map { data, _ in
try JSONDecoder().decode(User.self, from: data)
}
}
func dumpPackage() -> Promise<Package> {
let task = Process()
task.launchPath = "/usr/bin/swift"
task.arguments = ["package", "dump-package"]
return firstly {
task.launch(.promise)
}.map { out, _ in
out.fileHandleForReading.readDataToEndOfFile()
}.map { data in
try JSONDecoder().decode(Package.self, from: data)
}
}
var defaultSwiftVersion: String {
let task = Process()
task.launchPath = "/usr/bin/swift"
task.arguments = ["--version"]
func extract(input: String) -> String {
let range = input.range(of: #"Apple Swift version \d+\.\d+"#, options: .regularExpression)!
return String(input[range].split(separator: " ").last!)
}
return try! firstly {
task.launch(.promise)
}.compactMap { out, _ in
String(data: out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
}.map { out in
extract(input: out)
}.wait()
}
func podspec(repo: Repo, user: User, pkg: Package) -> (Substring, String) {
let (owner, name) = { ($0[0], $0[1]) }(slug.split(separator: "/"))
let swiftVersion = pkg.swiftLanguageVersions.min() ?? defaultSwiftVersion
let targets = pkg.targets.filter{ $0.type == .regular }
guard targets.count == 1 else { fatal(message: "Too many targets for this script!") }
guard let sources = targets[0].path else { fatal(message: "Target has no path!") }
return (name, """
Pod::Spec.new do |s|
s.name = '\(name)'
s.version = '\(tag)'
s.summary = '\(repo.description)'
s.homepage = "https://github.com/\(slug)"
s.license = { type: '\(repo.license.spdx_id)', file: '\(licenseFile)' }
s.author = { '\(user.name)': '\(user.email)' }
s.source = { git: "https://github.com/\(slug).git", tag: '\(tag)' }
s.social_media_url = 'https://twitter.com/\(owner)'
s.osx.deployment_target = '10.10'
s.ios.deployment_target = '8.0'
s.tvos.deployment_target = '9.0'
s.watchos.deployment_target = '2.0'
s.source_files = '\(sources)/*.swift'
s.swift_version = '\(swiftVersion)'
end
""")
}
func publishRelease() throws -> Promise<Void> {
struct Input: Encodable {
var tag_name: String { return tag }
var name: String { return tag }
let body = ""
}
var rq = URLRequest(github: "/repos/\(slug)/releases")
rq.httpMethod = "POST"
rq.httpBody = try JSONEncoder().encode(Input())
return URLSession.shared.dataTask(.promise, with: rq).validate().asVoid()
}
switch CommandLine.arguments[1] {
case "generate-podspec":
firstly {
when(fulfilled: description(), email(), dumpPackage())
}.map(podspec).done { name, podspec in
try podspec.write(toFile: "\(name).podspec", atomically: false, encoding: .utf8)
exit(0)
}.catch {
fatal(error: $0)
}
case "publish-release":
try publishRelease().done {
exit(0)
}.catch {
fatal(error: $0)
}
default:
fatal(message: "invalid usage")
}
RunLoop.main.run()

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:

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
/*.xcodeproj /*.xcodeproj
/build /build
/docs /docs
/.swiftpm

View File

@@ -1,11 +1,11 @@
# 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+)?(-\S*)?$/ if: type != push OR branch = master OR branch =~ /^\d+\.\d+\.\d+(-.*)?$/
stages: stages:
- name: pretest - name: pretest
- name: test - name: test
- name: deploy - name: deploy
if: branch =~ ^\d+\.\d+\.\d+$ if: branch =~ /^\d+\.\d+\.\d+(-.*)?$/
os: osx os: osx
language: swift language: swift
@@ -15,20 +15,33 @@ xcode_scheme: Path.swift-Package
jobs: jobs:
include: include:
- name: macOS / Swift 4.2.1 - name: macOS / Swift 4.0.3
before_script: swift build -Xswiftc -warnings-as-errors
script: swift test --parallel -Xswiftc -swift-version -Xswiftc 4
- &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
- <<: *std
name: macOS / Swift 5.1
osx_image: xcode11
- &xcodebuild - &xcodebuild
before_install: swift package generate-xcodeproj --enable-code-coverage before_install: swift package generate-xcodeproj --enable-code-coverage
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS / Swift 4.2.1 name: iOS / Swift 4.2.1
after_success: bash <(curl -s https://codecov.io/bash) after_success: bash <(curl -s https://codecov.io/bash)
- <<: *xcodebuild - <<: *xcodebuild
xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV
name: tvOS / Swift 4.2.1 name: tvOS / Swift 4.2.1
- <<: *xcodebuild - <<: *xcodebuild
name: watchOS / Swift 4.2.1 name: watchOS / Swift 4.2.1
script: | script: |
@@ -41,26 +54,32 @@ jobs:
after_success: false after_success: false
- &linux - &linux
env: SWIFT_VERSION=4.2.1 env: SWIFT_VERSION=4.2.4
os: linux os: linux
name: Linux / Swift 4.2.1 name: Linux / Swift 4.2.4
language: generic language: generic
dist: trusty
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-DEVELOPMENT-SNAPSHOT-2019-01-22-a' env: SWIFT_VERSION='5.0.2'
name: Linux / Swift 5.0.0-dev+2019-01-22 name: Linux / Swift 5.0.2
- <<: *linux
env: SWIFT_VERSION=5.1-DEVELOPMENT-SNAPSHOT-2019-07-03-a
name: Linux / Swift 5.1 (2019-07-03)
- stage: pretest - stage: pretest
name: Check Linux tests are syncd name: Check Linux tests are syncd
install: swift test --generate-linuxmain install: swift test --generate-linuxmain
script: git diff --exit-code script: git diff --exit-code
osx_image: xcode10.2
- stage: deploy - stage: deploy
name: Jazzy name: Jazzy
osx_image: xcode10.2
install: gem install jazzy install: gem install jazzy
before_script: swift package generate-xcodeproj before_script: swift package generate-xcodeproj
script: | script: |
@@ -77,7 +96,10 @@ jobs:
- name: CocoaPods - name: CocoaPods
osx_image: xcode10.2 osx_image: xcode10.2
install: brew install mxcl/made/swift-sh install: |
before_script: .github/deploy generate-podspec brew install mxcl/made/swift-sh
curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy
chmod u+x deploy
before_script: ./deploy generate-podspec
script: pod trunk push script: pod trunk push
after_success: .github/deploy publish-release after_success: ./deploy publish-release

View File

@@ -1,7 +1,7 @@
// swift-tools-version:4.2 // swift-tools-version:4.2
import PackageDescription import PackageDescription
let pkg = Package( let package = Package(
name: "Path.swift", name: "Path.swift",
products: [ products: [
.library(name: "Path", targets: ["Path"]), .library(name: "Path", targets: ["Path"]),

View File

@@ -32,7 +32,7 @@ let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
print(foo) // => /bar/foo print(foo) // => /bar/foo
print(foo.isFile) // => true print(foo.isFile) // => true
// we support dynamic members (_use_sparingly_): // we support dynamic-member-syntax when joining named static members, eg:
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
// a practical example: installing a helper executable // a practical example: installing a helper executable
@@ -53,7 +53,7 @@ help me continue my work, I appreciate it x
<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">
</a> </a>
[Other donation/tipping options](http://mxcl.github.io/donate/) [Other donation/tipping options](http://mxcl.dev/#donate)
# Handbook # Handbook
@@ -107,10 +107,11 @@ We support `@dynamicMemberLookup`:
let ls = Path.root.usr.bin.ls // => /usr/bin/ls let ls = Path.root.usr.bin.ls // => /usr/bin/ls
``` ```
This is less commonly useful than you would think, hence our documentation We only provide this for “starting” function, eg. `Path.home` or `Bundle.path`.
does not use it. Usually you are joining variables or other `String` arguments This is because we found in practice it was easy to write incorrect code, since
or trying to describe files (and files usually have extensions). However when everything would compile if we allowed arbituary variables to take *any* named
you need it, its *lovely*. property as valid syntax. What we have is what you want most of the time but
much less dangerous.
## Initializing from user-input ## Initializing from user-input
@@ -146,27 +147,61 @@ try Bundle.main.resources.join("foo").copy(to: .home)
## Directory listings ## Directory listings
We provide `ls()`, called because it behaves like the Terminal `ls` function, We provide `ls()`, called because it behaves like the Terminal `ls` function,
the name thus implies its behavior, ie. that it is not recursive. the name thus implies its behavior, ie. that it is not recursive and doesnt
list hidden files.
```swift ```swift
for entry in Path.home.ls() { for path in Path.home.ls() {
print(entry.path)
print(entry.kind) // .directory or .file
}
for entry in Path.home.ls() where entry.kind == .file {
// //
} }
for entry in Path.home.ls() where entry.path.mtime > yesterday { for path in Path.home.ls() where path.isFile {
//
}
for path in Path.home.ls() where path.mtime > yesterday {
// //
} }
let dirs = Path.home.ls().directories let dirs = Path.home.ls().directories
// ^^ directories that *exist*
let files = Path.home.ls().files let files = Path.home.ls().files
// ^^ files that both *exist* and are *not* directories
let swiftFiles = Path.home.ls().files(withExtension: "swift") let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" }
```
We provide `find()` for recursive listing:
```swift
Path.home.find().execute { path in
//
}
```
Which is configurable:
```swift
Path.home.find().maxDepth(1).extension("swift").kind(.file) { path in
//
}
```
And can be controlled:
```swift
Path.home.find().execute { path in
guard foo else { return .skip }
guard bar else { return .abort }
return .continue
}
```
Or just get all paths at once:
```swift
let paths = Path.home.find().execute()
``` ```
# `Path.swift` is robust # `Path.swift` is robust
@@ -258,9 +293,15 @@ 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`), but we do *not* for functions that you would expect it (notably `realpath`), we *do* the same for
`Path.init`, *nor* if you are joining a path that ends up being one of these `Path.init`, but *do not* if you are joining a path that ends up being one of
paths, (eg. `Path.root.join("var/private')`). these paths, (eg. `Path.root.join("var/private')`).
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
meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `kind` is `nil`.
## We do not provide change directory functionality ## We do not provide change directory functionality
@@ -316,7 +357,7 @@ pursuit of getting it *right*)! We will tag 1.0 as soon as possible.
### Get push notifications for new releases ### Get push notifications for new releases
https://codebasesaga.com/canopy/ https://mxcl.dev/canopy/
# Alternatives # Alternatives
@@ -328,7 +369,7 @@ https://codebasesaga.com/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-orange.svg
[docs]: https://mxcl.github.io/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
[badge-ci]: https://travis-ci.com/mxcl/Path.swift.svg [badge-ci]: https://travis-ci.com/mxcl/Path.swift.svg

View File

@@ -13,26 +13,31 @@ public extension Bundle {
Returns the path for the shared-frameworks directory in this bundle. Returns the path for the shared-frameworks directory in this bundle.
- Note: This is typically `ShareFrameworks` - Note: This is typically `ShareFrameworks`
*/ */
var sharedFrameworks: Path { var sharedFrameworks: DynamicPath {
return sharedFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath return sharedFrameworksPath.flatMap(DynamicPath.init) ?? defaultSharedFrameworksPath
} }
/** /**
Returns the path for the private-frameworks directory in this bundle. Returns the path for the private-frameworks directory in this bundle.
- Note: This is typically `Frameworks` - Note: This is typically `Frameworks`
*/ */
var privateFrameworks: Path { var privateFrameworks: DynamicPath {
return privateFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath return privateFrameworksPath.flatMap(DynamicPath.init) ?? defaultSharedFrameworksPath
} }
/// Returns the path for the resources directory in this bundle. /// Returns the path for the resources directory in this bundle.
var resources: Path { var resources: DynamicPath {
return resourcePath.flatMap(Path.init) ?? defaultResourcesPath return resourcePath.flatMap(DynamicPath.init) ?? defaultResourcesPath
} }
/// Returns the path for this bundle. /// Returns the path for this bundle.
var path: Path { var path: DynamicPath {
return Path(string: bundlePath) return DynamicPath(string: bundlePath)
}
/// Returns the executable for this bundle, if there is one, not all bundles have one hence `Optional`.
var executable: DynamicPath? {
return executablePath.flatMap(DynamicPath.init)
} }
} }
@@ -40,14 +45,14 @@ public extension Bundle {
public extension String { public extension String {
/// Initializes this `String` with the contents of the provided path. /// Initializes this `String` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init<P: Pathish>(contentsOf path: P) throws {
try self.init(contentsOfFile: path.string) try self.init(contentsOfFile: path.string)
} }
/// - Returns: `to` to allow chaining /// - Returns: `to` to allow chaining
@inlinable @inlinable
@discardableResult @discardableResult
func write(to: Path, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path { func write<P: Pathish>(to: P, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> P {
try write(toFile: to.string, atomically: atomically, encoding: encoding) try write(toFile: to.string, atomically: atomically, encoding: encoding)
return to return to
} }
@@ -57,14 +62,14 @@ public extension String {
public extension Data { public extension Data {
/// Initializes this `Data` with the contents of the provided path. /// Initializes this `Data` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init<P: Pathish>(contentsOf path: P) throws {
try self.init(contentsOf: path.url) try self.init(contentsOf: path.url)
} }
/// - Returns: `to` to allow chaining /// - Returns: `to` to allow chaining
@inlinable @inlinable
@discardableResult @discardableResult
func write(to: Path, atomically: Bool = false) throws -> Path { func write<P: Pathish>(to: P, atomically: Bool = false) throws -> P {
let opts: NSData.WritingOptions let opts: NSData.WritingOptions
if atomically { if atomically {
#if !os(Linux) #if !os(Linux)
@@ -84,39 +89,39 @@ public extension Data {
public extension FileHandle { public extension FileHandle {
/// Initializes this `FileHandle` for reading at the location of the provided path. /// Initializes this `FileHandle` for reading at the location of the provided path.
@inlinable @inlinable
convenience init(forReadingAt path: Path) throws { convenience init<P: Pathish>(forReadingAt path: P) throws {
try self.init(forReadingFrom: path.url) try self.init(forReadingFrom: path.url)
} }
/// Initializes this `FileHandle` for writing at the location of the provided path. /// Initializes this `FileHandle` for writing at the location of the provided path.
@inlinable @inlinable
convenience init(forWritingAt path: Path) throws { convenience init<P: Pathish>(forWritingAt path: P) throws {
try self.init(forWritingTo: path.url) try self.init(forWritingTo: path.url)
} }
/// Initializes this `FileHandle` for reading and writing at the location of the provided path. /// Initializes this `FileHandle` for reading and writing at the location of the provided path.
@inlinable @inlinable
convenience init(forUpdatingAt path: Path) throws { convenience init<P: Pathish>(forUpdatingAt path: P) throws {
try self.init(forUpdating: path.url) try self.init(forUpdating: path.url)
} }
} }
internal extension Bundle { internal extension Bundle {
var defaultSharedFrameworksPath: Path { var defaultSharedFrameworksPath: DynamicPath {
#if os(macOS) #if os(macOS)
return path.join("Contents/Frameworks") return path.Contents.Frameworks
#elseif os(Linux) #elseif os(Linux)
return path.join("lib") return path.lib
#else #else
return path.join("Frameworks") return path.Frameworks
#endif #endif
} }
var defaultResourcesPath: Path { var defaultResourcesPath: DynamicPath {
#if os(macOS) #if os(macOS)
return path.join("Contents/Resources") return path.Contents.Resources
#elseif os(Linux) #elseif os(Linux)
return path.join("share") return path.share
#else #else
return path return path
#endif #endif

View File

@@ -1,6 +1,6 @@
import Foundation import Foundation
public extension Path { public extension Pathish {
//MARK: Filesystem Attributes //MARK: Filesystem Attributes
/** /**
@@ -30,6 +30,29 @@ public extension Path {
} }
} }
/// 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.
@@ -38,8 +61,10 @@ public extension Path {
@discardableResult @discardableResult
func chmod(_ octal: Int) throws -> Path { func chmod(_ octal: Int) throws -> Path {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string) try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return self return Path(self)
} }
//MARK: Filesystem Locking
/** /**
Applies the macOS filesystem lock attribute. Applies the macOS filesystem lock attribute.
@@ -57,7 +82,7 @@ public extension Path {
try FileManager.default.setAttributes(attrs, ofItemAtPath: string) try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
} }
#endif #endif
return self return Path(self)
} }
/** /**
@@ -73,7 +98,7 @@ public extension Path {
do { do {
attrs = try FileManager.default.attributesOfItem(atPath: string) attrs = try FileManager.default.attributesOfItem(atPath: string)
} catch CocoaError.fileReadNoSuchFile { } catch CocoaError.fileReadNoSuchFile {
return self return Path(self)
} }
let b = attrs[.immutable] as? Bool ?? false let b = attrs[.immutable] as? Bool ?? false
if b { if b {
@@ -81,6 +106,19 @@ public extension Path {
try FileManager.default.setAttributes(attrs, ofItemAtPath: string) try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
} }
#endif #endif
return self return Path(self)
}
}
/// The `extension` that provides `Kind`.
public extension Path {
/// A filesystem entrys kind, file, directory, symlink etc.
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

@@ -23,18 +23,19 @@ public extension CodingUserInfoKey {
Provided for relative-path coding. See the instructions in our Provided for relative-path coding. See the instructions in our
[README](https://github.com/mxcl/Path.swift/#codable). [README](https://github.com/mxcl/Path.swift/#codable).
*/ */
extension Path: Codable { extension Path: Codable {
/// - SeeAlso: `CodingUserInfoKey.relativePath` /// - SeeAlso: `CodingUserInfoKey.relativePath`
/// :nodoc: /// :nodoc:
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self) let value = try decoder.singleValueContainer().decode(String.self)
if value.hasPrefix("/") { if value.hasPrefix("/") {
string = value string = value
} else { } else if let root = decoder.userInfo[.relativePath] as? Path {
guard let root = decoder.userInfo[.relativePath] as? Path else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
}
string = (root/value).string string = (root/value).string
} else if let root = decoder.userInfo[.relativePath] as? DynamicPath {
string = (root/value).string
} else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
} }
} }
@@ -44,6 +45,8 @@ extension Path: Codable {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
if let root = encoder.userInfo[.relativePath] as? Path { if let root = encoder.userInfo[.relativePath] as? Path {
try container.encode(relative(to: root)) try container.encode(relative(to: root))
} else if let root = encoder.userInfo[.relativePath] as? DynamicPath {
try container.encode(relative(to: root))
} else { } else {
try container.encode(string) try container.encode(string)
} }

View File

@@ -1,20 +1,21 @@
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: Path { public static var cwd: DynamicPath {
return Path(string: FileManager.default.currentDirectoryPath) return .init(string: FileManager.default.currentDirectoryPath)
} }
/// Returns a `Path` representing the root path. /// Returns a `Path` representing the root path.
public static var root: Path { public static var root: DynamicPath {
return Path(string: "/") return .init(string: "/")
} }
/// Returns a `Path` representing the users home directory /// Returns a `Path` representing the users home directory
public static var home: Path { public static var home: DynamicPath {
let string: String let string: String
#if os(macOS) #if os(macOS)
if #available(OSX 10.12, *) { if #available(OSX 10.12, *) {
@@ -25,30 +26,30 @@ extension Path {
#else #else
string = NSHomeDirectory() string = NSHomeDirectory()
#endif #endif
return Path(string: string) return .init(string: string)
} }
/// Helper to allow search path and domain mask to be passed in. /// Helper to allow search path and domain mask to be passed in.
private static func path(for searchPath: FileManager.SearchPathDirectory) -> Path { private static func path(for searchPath: FileManager.SearchPathDirectory) -> DynamicPath {
#if os(Linux) #if os(Linux)
// the urls(for:in:) function is not implemented on Linux // the urls(for:in:) function is not implemented on Linux
//TODO strictly we should first try to use the provided binary tool //TODO strictly we should first try to use the provided binary tool
let foo = { ProcessInfo.processInfo.environment[$0].flatMap(Path.init) ?? $1 } let foo = { ProcessInfo.processInfo.environment[$0].flatMap(Path.init).map(DynamicPath.init) ?? $1 }
switch searchPath { switch searchPath {
case .documentDirectory: case .documentDirectory:
return Path.home/"Documents" return Path.home.Documents
case .applicationSupportDirectory: case .applicationSupportDirectory:
return foo("XDG_DATA_HOME", Path.home/".local/share") return foo("XDG_DATA_HOME", Path.home[dynamicMember: ".local/share"])
case .cachesDirectory: case .cachesDirectory:
return foo("XDG_CACHE_HOME", Path.home/".cache") return foo("XDG_CACHE_HOME", Path.home[dynamicMember: ".cache"])
default: default:
fatalError() fatalError()
} }
#else #else
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { return defaultUrl(for: searchPath) } guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { return defaultUrl(for: searchPath) }
return Path(string: pathString) return DynamicPath(string: pathString)
#endif #endif
} }
@@ -57,7 +58,7 @@ extension Path {
- Note: There is no standard location for documents on Linux, thus we return `~/Documents`. - Note: There is no standard location for documents on Linux, thus we return `~/Documents`.
- Note: You should create a subdirectory before creating any files. - Note: You should create a subdirectory before creating any files.
*/ */
public static var documents: Path { public static var documents: DynamicPath {
return path(for: .documentDirectory) return path(for: .documentDirectory)
} }
@@ -66,7 +67,7 @@ extension Path {
- Note: On Linux this is `XDG_CACHE_HOME`. - Note: On Linux this is `XDG_CACHE_HOME`.
- Note: You should create a subdirectory before creating any files. - Note: You should create a subdirectory before creating any files.
*/ */
public static var caches: Path { public static var caches: DynamicPath {
return path(for: .cachesDirectory) return path(for: .cachesDirectory)
} }
@@ -75,20 +76,20 @@ extension Path {
- Note: On Linux is `XDG_DATA_HOME`. - Note: On Linux is `XDG_DATA_HOME`.
- Note: You should create a subdirectory before creating any files. - Note: You should create a subdirectory before creating any files.
*/ */
public static var applicationSupport: Path { public static var applicationSupport: DynamicPath {
return path(for: .applicationSupportDirectory) return path(for: .applicationSupportDirectory)
} }
} }
#if !os(Linux) #if !os(Linux)
func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> Path { func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> DynamicPath {
switch searchPath { switch searchPath {
case .documentDirectory: case .documentDirectory:
return Path.home/"Documents" return Path.home.Documents
case .applicationSupportDirectory: case .applicationSupportDirectory:
return Path.home/"Library/Application Support" return Path.home.Library[dynamicMember: "Application Support"]
case .cachesDirectory: case .cachesDirectory:
return Path.home/"Library/Caches" return Path.home.Library.Caches
default: default:
fatalError() fatalError()
} }

View File

@@ -3,9 +3,10 @@ import Foundation
import Glibc import Glibc
#endif #endif
public extension Path { public extension Pathish {
//MARK: File Management //MARK: File Management
/** /**
Copies a file. Copies a file.
@@ -24,17 +25,17 @@ public extension Path {
- SeeAlso: `copy(into:overwrite:)` - SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
func copy(to: Path, overwrite: Bool = false) throws -> Path { func copy<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile, isFile { 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.1) // check if fixed #if os(Linux) && !swift(>=5.2) // check if fixed
if !overwrite, to.isFile { if !overwrite, to.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
try FileManager.default.copyItem(atPath: string, toPath: to.string) try FileManager.default.copyItem(atPath: string, toPath: to.string)
return to return Path(to)
} }
/** /**
@@ -60,16 +61,16 @@ public extension Path {
- SeeAlso: `copy(to:overwrite:)` - SeeAlso: `copy(to:overwrite:)`
*/ */
@discardableResult @discardableResult
func copy(into: Path, overwrite: Bool = false) throws -> Path { func copy<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
if !into.exists { if into.type == nil {
try into.mkdir(.p) try into.mkdir(.p)
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile { if overwrite, let kind = rv.type, kind != .directory {
try rv.delete() try FileManager.default.removeItem(at: rv.url)
} }
#if os(Linux) && !swift(>=5.1) // check if fixed #if os(Linux) && !swift(>=5.2) // check if fixed
if !overwrite, rv.isFile { if !overwrite, rv.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -94,12 +95,12 @@ public extension Path {
- SeeAlso: `move(into:overwrite:)` - SeeAlso: `move(into:overwrite:)`
*/ */
@discardableResult @discardableResult
func move(to: Path, overwrite: Bool = false) throws -> Path { func move<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile { 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)
return to return Path(to)
} }
/** /**
@@ -118,18 +119,22 @@ public extension Path {
- SeeAlso: `move(to:overwrite:)` - SeeAlso: `move(to:overwrite:)`
*/ */
@discardableResult @discardableResult
func move(into: Path, overwrite: Bool = false) throws -> Path { func move<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
if !into.exists { switch into.type {
case nil:
try into.mkdir(.p) try into.mkdir(.p)
} else if !into.isDirectory { fallthrough
case .directory?:
let rv = into/basename()
if overwrite, let rvkind = rv.type, rvkind != .directory {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
case .file?, .symlink?:
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
let rv = into/basename()
if overwrite, rv.isFile {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
} }
/** /**
@@ -138,23 +143,24 @@ public extension Path {
*Path.swift* doesnt error if desired end result preexists. *Path.swift* doesnt error if desired end result preexists.
- Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission. - Note: On UNIX will this function will succeed if the parent directory is writable and the current user has permission.
- Note: This function will fail if the file or directory is locked - Note: This function will fail if the file or directory is locked
- Note: If entry is a symlink, deletes the symlink.
- SeeAlso: `lock()` - SeeAlso: `lock()`
*/ */
@inlinable @inlinable
func delete() throws { func delete() throws {
if exists { if type != nil {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
} }
/** /**
Creates an empty file at this path or if the file exists, updates its modification time. Creates an empty file at this path or if the file exists, updates its modification time.
- Returns: `self` to allow chaining. - Returns: A copy of `self` to allow chaining.
*/ */
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
if !exists { 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)
} }
@@ -167,7 +173,7 @@ public extension Path {
try FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: string) try FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: string)
#endif #endif
} }
return self return Path(self)
} }
/** /**
@@ -175,7 +181,7 @@ public extension Path {
- Parameter options: Specify `mkdir(.p)` to create intermediary directories. - Parameter options: Specify `mkdir(.p)` to create intermediary directories.
- Note: We do not error if the directory already exists (even without `.p`) - Note: We do not error if the directory already exists (even without `.p`)
because *Path.swift* noops if the desired end result preexists. because *Path.swift* noops if the desired end result preexists.
- Returns: `self` to allow chaining. - Returns: A copy of `self` to allow chaining.
*/ */
@discardableResult @discardableResult
func mkdir(_ options: MakeDirectoryOptions? = nil) throws -> Path { func mkdir(_ options: MakeDirectoryOptions? = nil) throws -> Path {
@@ -194,7 +200,7 @@ public extension Path {
throw error throw error
#endif #endif
} }
return self return Path(self)
} }
/** /**
@@ -217,9 +223,9 @@ public extension Path {
- Note: If `self` does not exist, is **not** an error. - Note: If `self` does not exist, is **not** an error.
*/ */
@discardableResult @discardableResult
func symlink(as: Path) throws -> Path { func symlink<P: Pathish>(as: P) throws -> Path {
try FileManager.default.createSymbolicLink(atPath: `as`.string, withDestinationPath: string) try FileManager.default.createSymbolicLink(atPath: `as`.string, withDestinationPath: string)
return `as` return Path(`as`)
} }
/** /**
@@ -227,15 +233,18 @@ public extension Path {
- Note: If into does not exist, creates the directory with intermediate directories if necessary. - Note: If into does not exist, creates the directory with intermediate directories if necessary.
*/ */
@discardableResult @discardableResult
func symlink(into dir: Path) throws -> Path { func symlink<P: Pathish>(into dir: P) throws -> Path {
if !dir.exists { switch dir.type {
case nil, .symlink?:
try dir.mkdir(.p) try dir.mkdir(.p)
} else if !dir.isDirectory { fallthrough
case .directory?:
let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst
case .file?:
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst
} }
} }

View File

@@ -11,3 +11,17 @@ extension Path: CustomDebugStringConvertible {
return "Path(\(string))" return "Path(\(string))"
} }
} }
extension DynamicPath: CustomStringConvertible {
/// Returns `Path.string`
public var description: String {
return string
}
}
extension DynamicPath: CustomDebugStringConvertible {
/// Returns eg. `Path(string: "/foo")`
public var debugDescription: String {
return "Path(\(string))"
}
}

View File

@@ -1,67 +1,197 @@
import Foundation import Foundation
/** public extension Path {
A file entry from a directory listing. /// The builder for `Path.find()`
- SeeAlso: `ls()` class Finder {
*/ fileprivate init(path: Path) {
public struct Entry { self.path = path
/// The kind of this directory entry. self.enumerator = FileManager.default.enumerator(atPath: path.string)
public enum Kind { }
/// The path is a file.
case file /// The `path` find operations operate on.
/// The path is a directory. public let path: Path
case directory
private let enumerator: FileManager.DirectoryEnumerator!
/// The range of directory depths for which the find operation will return entries.
private(set) public var depth: ClosedRange<Int> = 1...Int.max
/// 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>?
} }
/// The kind of this entry.
public let kind: Kind
/// The path of this entry.
public let path: Path
} }
public extension Path { extension Path.Finder: Sequence, IteratorProtocol {
//MARK: Directory Listings 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 {
if path == self.path, depth.lowerBound == 0 {
return path
} else {
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 {
/// A max depth of `0` returns only the path we are searching, `1` is that directorys listing.
func depth(max maxDepth: Int) -> Path.Finder {
#if os(Linux) && !swift(>=5.0)
fputs("warning: depth not implemented for Swift < 5\n", stderr)
#endif
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
}
/// Multiple calls will configure the Finder with multiple kinds.
func type(_ type: Path.EntryType) -> Path.Finder {
_types = _types ?? []
_types!.insert(type)
return self
}
/// Multiple calls will configure the Finder with for multiple extensions
func `extension`(_ ext: String) -> Path.Finder {
extensions = extensions ?? []
extensions!.insert(ext)
return self
}
/// The return type for `Path.Finder`
enum ControlFlow {
/// Stop enumerating this directory, return to the parent.
case skip
/// Stop enumerating all together.
case abort
/// Keep going.
case `continue`
}
/// Enumerate, one file at a time.
func execute(_ closure: (Path) throws -> ControlFlow) rethrows {
while let path = next() {
switch try closure(path) {
case .skip:
enumerator.skipDescendants()
case .abort:
return
case .continue:
continue
}
}
}
}
public extension Pathish {
//MARK: Directory Listing
/** /**
Same as the `ls -a` command output is shallow and unsorted. Same as the `ls` command output is shallow and unsorted.
- Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`. - Note: as per `ls`, by default we do *not* return hidden files. Specify `.a` for hidden files.
- Important: `includeHiddenFiles` does not work on Linux - Parameter options: Configure the listing.
- Important: On Linux the listing is always `ls -a`
*/ */
func ls(includeHiddenFiles: Bool = true) throws -> [Entry] { func ls(_ options: ListDirectoryOptions? = nil) -> [Path] {
var opts = FileManager.DirectoryEnumerationOptions() guard let urls = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else {
#if !os(Linux) fputs("warning: could not list: \(self)\n", stderr)
if !includeHiddenFiles { return []
opts.insert(.skipsHiddenFiles)
} }
#endif return urls.compactMap { url in
let paths = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: opts)
func convert(url: URL) -> Entry? {
guard let path = Path(url.path) else { return nil } guard let path = Path(url.path) else { return nil }
return Entry(kind: path.isDirectory ? .directory : .file, path: path) if options != .a, path.basename().hasPrefix(".") { return nil }
// ^^ we dont use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls`
return path
} }
return paths.compactMap(convert) }
/// Recursively find files under this path. If the path is a file, no files will be found.
func find() -> Path.Finder {
return .init(path: Path(self))
} }
} }
/// Convenience functions for the array return value of `Path.ls()` /// Convenience functions for the arrays of `Path`
public extension Array where Element == Entry { public extension Array where Element == Path {
/// Filters the list of entries to be a list of Paths that are directories. /// 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] {
return compactMap { return filter {
$0.kind == .directory ? $0.path : nil $0.isDirectory
} }
} }
/// Filters the list of entries to be a list of Paths that are files. /// Filters the list of entries to be a list of Paths that exist and are *not* directories. Thus expect symlinks, etc.
/// - Note: symlinks that point to files that do not exist are *not* returned.
var files: [Path] { var files: [Path] {
return compactMap { return filter {
$0.kind == .file ? $0.path : nil switch $0.type {
} case .none, .directory?:
} return false
case .file?, .symlink?:
/// Filters the list of entries to be a list of Paths that are files with the specified extension. return true
func files(withExtension ext: String) -> [Path] { }
return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil
} }
} }
} }
/// Options for `Path.ls(_:)`
public enum ListDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`.
case a
}

View File

@@ -5,10 +5,14 @@ import func Glibc.access
import Darwin import Darwin
#endif #endif
public extension Path { 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.
*/
var exists: Bool { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)
} }

View File

@@ -32,11 +32,16 @@ let _realpath = Glibc.realpath
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
- Note: A `Path` does not necessarily represent an actual filesystem entry. However we only provide this support off of the static members like `root` due
*/ to the anti-pattern where Path.swift suddenly feels like Javascript otherwise.
@dynamicMemberLookup - Note: A `Path` does not necessarily represent an actual filesystem entry.
public struct Path: Equatable, Hashable, Comparable { - SeeAlso: `Pathish` for most methods you will use on `Path` instances.
*/
public struct Path: Pathish {
/// The normalized string representation of the underlying filesystem path
public let string: String
init(string: String) { init(string: String) {
assert(string.first == "/") assert(string.first == "/")
@@ -53,14 +58,28 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: On macOS, removes an initial component of /private/var/automount, /var/automount, or /private from the path, if the result still indicates an existing file or directory (checked by consulting the file system). - Note: On macOS, removes an initial component of /private/var/automount, /var/automount, or /private from the path, if the result still indicates an existing file or directory (checked by consulting the file system).
- Returns: The path or `nil` if fed a relative path or a `~foo` string where there is no user `foo`. - Returns: The path or `nil` if fed a relative path or a `~foo` string where there is no user `foo`.
*/ */
public init?(_ description: String) { public init?<S: StringProtocol>(_ description: S) {
var pathComponents = description.split(separator: "/") var pathComponents = description.split(separator: "/")
switch description.first { switch description.first {
case "/": case "/":
break #if os(macOS)
func ifExists(withPrefix prefix: String, removeFirst n: Int) {
assert(prefix.split(separator: "/").count == n)
if description.hasPrefix(prefix), FileManager.default.fileExists(atPath: String(description)) {
pathComponents.removeFirst(n)
}
}
ifExists(withPrefix: "/private/var/automount", removeFirst: 3)
ifExists(withPrefix: "/var/automount", removeFirst: 2)
ifExists(withPrefix: "/private", removeFirst: 1)
#endif
string = join_(prefix: "/", pathComponents: pathComponents)
case "~": case "~":
if description == "~" { if description == "~" {
self = Path.home string = Path.home.string
return return
} }
let tilded: String let tilded: String
@@ -82,26 +101,11 @@ public struct Path: Equatable, Hashable, Comparable {
#endif #endif
} }
pathComponents.remove(at: 0) pathComponents.remove(at: 0)
pathComponents.insert(contentsOf: tilded.split(separator: "/"), at: 0) string = join_(prefix: tilded, pathComponents: pathComponents)
default: default:
return nil return nil
} }
#if os(macOS)
func ifExists(withPrefix prefix: String, removeFirst n: Int) {
assert(prefix.split(separator: "/").count == n)
if description.hasPrefix(prefix), FileManager.default.fileExists(atPath: description) {
pathComponents.removeFirst(n)
}
}
ifExists(withPrefix: "/private/var/automount", removeFirst: 3)
ifExists(withPrefix: "/var/automount", removeFirst: 2)
ifExists(withPrefix: "/private", removeFirst: 1)
#endif
self.string = join_(prefix: "/", pathComponents: pathComponents)
} }
/** /**
@@ -125,20 +129,17 @@ public struct Path: Equatable, Hashable, Comparable {
// ^^ works even if the url is a file-reference url // ^^ works even if the url is a file-reference url
} }
/// :nodoc: /// Converts anything that is `Pathish` to a `Path`
public subscript(dynamicMember addendum: String) -> Path { public init<P: Pathish>(_ path: P) {
//NOTE its possible for the string to be anything if we are invoked via string = path.string
// explicit subscript thus we use our fully sanitized `join` function
return Path(string: join_(prefix: string, appending: addendum))
} }
}
//MARK: Properties public extension Pathish {
//MARK: Filesystem Representation
/// The underlying filesystem path
public let string: String
/// Returns a `URL` representing this file path. /// Returns a `URL` representing this file path.
public var url: URL { var url: URL {
return URL(fileURLWithPath: string) return URL(fileURLWithPath: string)
} }
@@ -148,7 +149,7 @@ public struct Path: Equatable, Hashable, Comparable {
- SeeAlso: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl - SeeAlso: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
- Important: On Linux returns an file scheme NSURL for this path string. - Important: On Linux returns an file scheme NSURL for this path string.
*/ */
public var fileReferenceURL: NSURL? { var fileReferenceURL: NSURL? {
#if !os(Linux) #if !os(Linux)
// https://bugs.swift.org/browse/SR-2728 // https://bugs.swift.org/browse/SR-2728
return (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL return (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL
@@ -165,7 +166,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`. - Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
*/ */
public var parent: Path { var parent: Path {
let index = string.lastIndex(of: "/")! let index = string.lastIndex(of: "/")!
let substr = string[string.indices.startIndex..<index] let substr = string[string.indices.startIndex..<index]
return Path(string: String(substr)) return Path(string: String(substr))
@@ -178,7 +179,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: We special case eg. `foo.tar.gz`. - Note: We special case eg. `foo.tar.gz`.
*/ */
@inlinable @inlinable
public var `extension`: String { var `extension`: String {
//FIXME efficiency //FIXME efficiency
switch true { switch true {
case string.hasSuffix(".tar.gz"): case string.hasSuffix(".tar.gz"):
@@ -200,7 +201,16 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
//MARK: Pathing /**
Splits the string representation on the directory separator.
- 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
var components: [String] {
return string.split(separator: "/").map(String.init)
}
//MARK:- Pathing
/** /**
Joins a path and a string to produce a new path. Joins a path and a string to produce a new path.
@@ -217,7 +227,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Returns: A new joined path. - Returns: A new joined path.
- SeeAlso: `Path./(_:_:)` - SeeAlso: `Path./(_:_:)`
*/ */
public func join<S>(_ addendum: S) -> Path where S: StringProtocol { func join<S>(_ addendum: S) -> Path where S: StringProtocol {
return Path(string: join_(prefix: string, appending: addendum)) return Path(string: join_(prefix: string, appending: addendum))
} }
@@ -238,7 +248,7 @@ public struct Path: Equatable, Hashable, Comparable {
- SeeAlso: `join(_:)` - SeeAlso: `join(_:)`
*/ */
@inlinable @inlinable
public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol { static func /<S>(lhs: Self, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs) return lhs.join(rhs)
} }
@@ -249,7 +259,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter base: The base to which we calculate the relative path. - Parameter base: The base to which we calculate the relative path.
- ToDo: Another variant that returns `nil` if result would start with `..` - ToDo: Another variant that returns `nil` if result would start with `..`
*/ */
public func relative(to base: Path) -> String { func relative<P: Pathish>(to base: P) -> String {
// Split the two paths into their components. // Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying. // FIXME: The is needs to be optimized to avoid unncessary copying.
let pathComps = (string as NSString).pathComponents let pathComps = (string as NSString).pathComponents
@@ -288,7 +298,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Returns: A string that is the filenames basename. - Returns: A string that is the filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension. - Parameter dropExtension: If `true` returns the basename without its file extension.
*/ */
public func basename(dropExtension: Bool = false) -> String { func basename(dropExtension: Bool = false) -> String {
var lastPathComponent: Substring { var lastPathComponent: Substring {
let slash = string.lastIndex(of: "/")! let slash = string.lastIndex(of: "/")!
let index = string.index(after: slash) let index = string.index(after: slash)
@@ -313,27 +323,25 @@ public struct Path: Equatable, Hashable, Comparable {
If the path represents an actual entry that is a symlink, returns the symlinks If the path represents an actual entry that is a symlink, returns the symlinks
absolute destination. absolute destination.
- Important: This is not exhaustive, the resulting path may still contain - Important: This is not exhaustive, the resulting path may still contain a symlink.
symlink. - Important: The path will only be different if the last path component is a symlink, any symlinks in prior components are not resolved.
- Important: The path will only be different if the last path component is a
symlink, any symlinks in prior components are not resolved.
- Note: If file exists but isnt a symlink, returns `self`. - Note: If file exists but isnt a symlink, returns `self`.
- Note: If symlink destination does not exist, is **not** an error. - Note: If symlink destination does not exist, is **not** an error.
*/ */
public func readlink() throws -> Path { func readlink() throws -> Path {
do { do {
let rv = try FileManager.default.destinationOfSymbolicLink(atPath: string) let rv = try FileManager.default.destinationOfSymbolicLink(atPath: string)
return Path(rv) ?? parent/rv return Path(rv) ?? parent/rv
} catch CocoaError.fileReadUnknown { } catch CocoaError.fileReadUnknown {
// file is not symlink, return `self` // file is not symlink, return `self`
assert(exists) assert(exists)
return self return Path(string: string)
} catch { } catch {
#if os(Linux) #if os(Linux)
// ugh: Swift on Linux // ugh: Swift on Linux
let nsError = error as NSError let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain, nsError.code == CocoaError.fileReadUnknown.rawValue, exists { if nsError.domain == NSCocoaErrorDomain, nsError.code == CocoaError.fileReadUnknown.rawValue, exists {
return self return Path(self)
} }
#endif #endif
throw error throw error
@@ -341,7 +349,7 @@ public struct Path: Equatable, Hashable, Comparable {
} }
/// Recursively resolves symlinks in this path. /// Recursively resolves symlinks in this path.
public func realpath() throws -> Path { func realpath() throws -> Path {
guard let rv = _realpath(string, nil) else { throw CocoaError.error(.fileNoSuchFile) } guard let rv = _realpath(string, nil) else { throw CocoaError.error(.fileNoSuchFile) }
defer { free(rv) } defer { free(rv) }
guard let rvv = String(validatingUTF8: rv) else { throw CocoaError.error(.fileReadUnknownStringEncoding) } guard let rvv = String(validatingUTF8: rv) else { throw CocoaError.error(.fileReadUnknownStringEncoding) }
@@ -359,7 +367,7 @@ public struct Path: Equatable, Hashable, Comparable {
/// Returns the locale-aware sort order for the two paths. /// Returns the locale-aware sort order for the two paths.
/// :nodoc: /// :nodoc:
@inlinable @inlinable
public static func <(lhs: Path, rhs: Path) -> Bool { static func <(lhs: Self, rhs: Self) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
} }
} }
@@ -398,3 +406,27 @@ 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`
@dynamicMemberLookup
public struct DynamicPath: Pathish {
/// The normalized string representation of the underlying filesystem path
public let string: String
init(string: String) {
assert(string.hasPrefix("/"))
self.string = string
}
/// Converts a `Path` to a `DynamicPath`
public init<P: Pathish>(_ path: P) {
string = path.string
}
/// :nodoc:
public subscript(dynamicMember addendum: String) -> DynamicPath {
//NOTE its possible for the string to be anything if we are invoked via
// explicit subscript thus we use our fully sanitized `join` function
return DynamicPath(string: join_(prefix: string, appending: addendum))
}
}

6
Sources/Pathish.swift Normal file
View File

@@ -0,0 +1,6 @@
/// 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
public protocol Pathish: Hashable, Comparable {
/// The normalized string representation of the underlying filesystem path
var string: String { get }
}

View File

@@ -0,0 +1,85 @@
import XCTest
import Path
extension PathTests {
func testFindMaxDepth1() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.touch()
try tmpdir.c.mkdir().join("e").touch()
do {
let finder = tmpdir.find().depth(max: 1)
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
}
}
}
func testFindMaxDepth2() 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()
do {
let finder = tmpdir.find().depth(max: 2)
XCTAssertEqual(finder.depth, 1...2)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
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 testFindExtension() throws {
try Path.mktemp { tmpdir in
try tmpdir.join("foo.json").touch()
try tmpdir.join("bar.txt").touch()
XCTAssertEqual(
Set(tmpdir.find().extension("json")),
[tmpdir.join("foo.json")])
XCTAssertEqual(
Set(tmpdir.find().extension("txt").extension("json")),
[tmpdir.join("foo.json"), tmpdir.join("bar.txt")])
}
}
func testFindKinds() throws {
try Path.mktemp { tmpdir in
try tmpdir.foo.mkdir()
try tmpdir.bar.touch()
XCTAssertEqual(
Set(tmpdir.find().type(.file)),
[tmpdir.join("bar")])
XCTAssertEqual(
Set(tmpdir.find().type(.directory)),
[tmpdir.join("foo")])
XCTAssertEqual(
Set(tmpdir.find().type(.file).type(.directory)),
Set(["foo", "bar"].map(tmpdir.join)))
}
}
}

View File

@@ -1,4 +1,6 @@
@testable import Path @testable import Path
import func XCTest.XCTAssertEqual
import Foundation
import XCTest import XCTest
class PathTests: XCTestCase { class PathTests: XCTestCase {
@@ -18,32 +20,31 @@ class PathTests: XCTestCase {
func testEnumeration() throws { func testEnumeration() throws {
let tmpdir_ = try TemporaryDirectory() let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path let tmpdir = tmpdir_.path
try tmpdir.a.mkdir().c.touch() try tmpdir.join("a").mkdir().join("c").touch()
try tmpdir.join("b.swift").touch() try tmpdir.join("b.swift").touch()
try tmpdir.c.touch() try tmpdir.join("c").touch()
try tmpdir.join(".d").mkdir().e.touch() try tmpdir.join(".d").mkdir().join("e").touch()
var paths = Set<String>() var paths = Set<String>()
let lsrv = try tmpdir.ls() let lsrv = tmpdir.ls(.a)
var dirs = 0 var dirs = 0
for entry in lsrv { for path in lsrv {
if entry.kind == .directory { if path.isDirectory {
dirs += 1 dirs += 1
} }
paths.insert(entry.path.basename()) paths.insert(path.basename())
} }
XCTAssertEqual(dirs, 2) XCTAssertEqual(dirs, 2)
XCTAssertEqual(dirs, lsrv.directories.count) XCTAssertEqual(dirs, lsrv.directories.count)
XCTAssertEqual(["a", ".d"], Set(lsrv.directories.map{ $0.relative(to: tmpdir) })) XCTAssertEqual(["a", ".d"], Set(lsrv.directories.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["b.swift", "c"], Set(lsrv.files.map{ $0.relative(to: tmpdir) })) XCTAssertEqual(["b.swift", "c"], Set(lsrv.files.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["b.swift"], Set(lsrv.files(withExtension: "swift").map{ $0.relative(to: tmpdir) })) XCTAssertEqual(["b.swift"], Set(lsrv.files.filter{ $0.extension == "swift" }.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["c"], Set(lsrv.files(withExtension: "").map{ $0.relative(to: tmpdir) })) XCTAssertEqual(["c"], Set(lsrv.files.filter{ $0.extension == "" }.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"]) XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"])
} }
func testEnumerationSkippingHiddenFiles() throws { func testEnumerationSkippingHiddenFiles() throws {
#if !os(Linux)
let tmpdir_ = try TemporaryDirectory() let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch() try tmpdir.join("a").mkdir().join("c").touch()
@@ -53,25 +54,33 @@ class PathTests: XCTestCase {
var paths = Set<String>() var paths = Set<String>()
var dirs = 0 var dirs = 0
for entry in try tmpdir.ls(includeHiddenFiles: false) { for path in tmpdir.ls() {
if entry.kind == .directory { if path.isDirectory {
dirs += 1 dirs += 1
} }
paths.insert(entry.path.basename()) paths.insert(path.basename())
} }
XCTAssertEqual(dirs, 1) XCTAssertEqual(dirs, 1)
XCTAssertEqual(paths, ["a", "b", "c"]) XCTAssertEqual(paths, ["a", "b", "c"])
#endif
} }
func testRelativeTo() { func testRelativeTo() {
XCTAssertEqual((Path.root/"tmp/foo").relative(to: .root/"tmp"), "foo") XCTAssertEqual((Path.root.tmp.foo).relative(to: Path.root/"tmp"), "foo")
XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar") XCTAssertEqual((Path.root.tmp.foo.bar).relative(to: Path.root/"tmp/baz"), "../foo/bar")
} }
func testExists() { func testExists() throws {
XCTAssert(Path.root.exists) XCTAssert(Path.root.exists)
XCTAssert((Path.root/"bin").exists) XCTAssert((Path.root/"bin").exists)
try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.exists)
XCTAssertFalse(try tmpdir.bar.symlink(as: tmpdir.foo).exists)
XCTAssertTrue(tmpdir.foo.type == .symlink)
XCTAssertTrue(try tmpdir.bar.touch().symlink(as: tmpdir.baz).exists)
XCTAssertTrue(tmpdir.bar.type == .file)
XCTAssertTrue(tmpdir.type == .directory)
}
} }
func testIsDirectory() { func testIsDirectory() {
@@ -96,7 +105,7 @@ class PathTests: XCTestCase {
} }
func testMktemp() throws { func testMktemp() throws {
var path: Path! var path: DynamicPath!
try Path.mktemp { try Path.mktemp {
path = $0 path = $0
XCTAssert(path.isDirectory) XCTAssert(path.isDirectory)
@@ -124,7 +133,7 @@ class PathTests: XCTestCase {
} }
func testCodable() throws { func testCodable() throws {
let input = [Path.root.foo, Path.root.foo.bar, Path.root] let input = [Path.root.foo, Path.root.foo.bar, Path.root].map(Path.init)
XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input) XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input)
} }
@@ -134,7 +143,7 @@ class PathTests: XCTestCase {
Path.root, Path.root,
root, root,
root.bar root.bar
] ].map(Path.init)
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root encoder.userInfo[.relativePath] = root
@@ -195,7 +204,9 @@ class PathTests: XCTestCase {
XCTAssertThrowsError(try root.foo.copy(to: root.bar)) XCTAssertThrowsError(try root.foo.copy(to: root.bar))
try root.foo.copy(to: root.bar, overwrite: true) try root.foo.copy(to: root.bar, overwrite: true)
} }
}
func testCopyToExistingDirectoryFails() throws {
// test copy errors if directory exists at destination, even with overwrite // test copy errors if directory exists at destination, even with overwrite
try Path.mktemp { root in try Path.mktemp { root in
try root.foo.touch() try root.foo.touch()
@@ -379,11 +390,26 @@ class PathTests: XCTestCase {
#if !os(Linux) #if !os(Linux)
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete()) XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
#endif #endif
// regression test: can delete a symlink that points to a non-existent file
let bar5 = try tmpdir.bar4.symlink(as: tmpdir.bar5)
XCTAssertEqual(bar5.type, .symlink)
XCTAssertFalse(bar5.exists)
XCTAssertNoThrow(try bar5.delete())
XCTAssertEqual(bar5.type, nil)
// test that deleting a symlink *only* deletes the symlink
let bar7 = try tmpdir.bar6.touch().symlink(as: tmpdir.bar7)
XCTAssertEqual(bar7.type, .symlink)
XCTAssertTrue(bar7.exists)
XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.type, nil)
XCTAssertEqual(tmpdir.bar6.type, .file)
} }
} }
func testRelativeCodable() throws { func testRelativeCodable() throws {
let path = Path.home.foo let path = Path(Path.home.foo)
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home encoder.userInfo[.relativePath] = Path.home
let data = try encoder.encode([path]) let data = try encoder.encode([path])
@@ -391,18 +417,21 @@ class PathTests: XCTestCase {
decoder.userInfo[.relativePath] = Path.home decoder.userInfo[.relativePath] = Path.home
XCTAssertEqual(try decoder.decode([Path].self, from: data), [path]) XCTAssertEqual(try decoder.decode([Path].self, from: data), [path])
decoder.userInfo[.relativePath] = Path.documents decoder.userInfo[.relativePath] = Path.documents
XCTAssertEqual(try decoder.decode([Path].self, from: data), [Path.documents.foo]) XCTAssertEqual(try decoder.decode([Path].self, from: data), [Path(Path.documents.foo)])
XCTAssertThrowsError(try JSONDecoder().decode([Path].self, from: data)) XCTAssertThrowsError(try JSONDecoder().decode([Path].self, from: data))
} }
func testBundleExtensions() throws { func testBundleExtensions() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir -> Void in
let bndl = Bundle(path: tmpdir.string)! guard let bndl = Bundle(path: tmpdir.string) else {
return XCTFail("Couldnt make Bundle for \(tmpdir)")
}
XCTAssertEqual(bndl.path, tmpdir) XCTAssertEqual(bndl.path, tmpdir)
XCTAssertEqual(bndl.sharedFrameworks, tmpdir.SharedFrameworks) XCTAssertEqual(bndl.sharedFrameworks, tmpdir.SharedFrameworks)
XCTAssertEqual(bndl.privateFrameworks, tmpdir.Frameworks) XCTAssertEqual(bndl.privateFrameworks, tmpdir.Frameworks)
XCTAssertEqual(bndl.resources, tmpdir) XCTAssertEqual(bndl.resources, tmpdir)
XCTAssertNil(bndl.path(forResource: "foo", ofType: "bar")) XCTAssertNil(bndl.path(forResource: "foo", ofType: "bar"))
XCTAssertNil(bndl.executable)
#if os(macOS) #if os(macOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks) XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks)
@@ -488,7 +517,7 @@ class PathTests: XCTestCase {
let foo = try tmpdir.foo.touch() let foo = try tmpdir.foo.touch()
let bar = try tmpdir.bar.mkdir() let bar = try tmpdir.bar.mkdir()
XCTAssertThrowsError(try foo.symlink(as: bar)) XCTAssertThrowsError(try foo.symlink(as: bar))
XCTAssert(try foo.symlink(as: bar.foo).isSymlink) XCTAssert(try foo.symlink(as: bar/"foo").isSymlink)
} }
} }
@@ -543,8 +572,8 @@ class PathTests: XCTestCase {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.mkdir() let foo = try tmpdir.foo.mkdir()
try foo.bar.mkdir().fuz.touch() try foo.join("bar").mkdir().join("fuz").touch()
let baz = try foo.symlink(as: tmpdir.baz) let baz = DynamicPath(try foo.symlink(as: tmpdir.baz))
XCTAssert(baz.isSymlink) XCTAssert(baz.isSymlink)
XCTAssert(baz.bar.isDirectory) XCTAssert(baz.bar.isDirectory)
XCTAssertEqual(baz.bar.join("..").string, "\(tmpdir)/baz") XCTAssertEqual(baz.bar.join("..").string, "\(tmpdir)/baz")
@@ -557,7 +586,7 @@ class PathTests: XCTestCase {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
let b = try tmpdir.a.b.mkdir(.p) let b = try tmpdir.a.b.mkdir(.p)
let c = try tmpdir.a.c.touch() let c = try tmpdir.a.c.touch()
let e = try c.symlink(as: b.e) let e = try c.symlink(as: b/"e")
let f = try e.symlink(as: tmpdir.f) let f = try e.symlink(as: tmpdir.f)
XCTAssertEqual(try f.readlink(), e) XCTAssertEqual(try f.readlink(), e)
XCTAssertEqual(try f.realpath(), c) XCTAssertEqual(try f.realpath(), c)
@@ -588,4 +617,37 @@ class PathTests: XCTestCase {
XCTAssertNil(Path("../foo")) XCTAssertNil(Path("../foo"))
XCTAssertNil(Path("./foo")) XCTAssertNil(Path("./foo"))
} }
func testPathComponents() throws {
XCTAssertEqual(Path.root.foo.bar.components, ["foo", "bar"])
XCTAssertEqual(Path.root.components, [])
}
func testFlatMap() throws {
// testing compile works
let foo: String? = "/a"
_ = foo.flatMap(Path.init)
let bar: Substring? = "/a"
_ = bar.flatMap(Path.init)
let baz: String.SubSequence? = "/a/b:1".split(separator: ":").first
_ = baz.flatMap(Path.init)
}
func testKind() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssertEqual(tmpdir.type, .directory)
XCTAssertEqual(foo.type, .file)
XCTAssertEqual(bar.type, .symlink)
}
}
}
private func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P, _ q: Q, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(p.string, q.string, file: file, line: line)
}
private func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P?, _ q: Q?, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(p?.string, q?.string, file: file, line: line)
} }

View File

@@ -3,7 +3,7 @@ import Foundation
class TemporaryDirectory { class TemporaryDirectory {
let url: URL let url: URL
var path: Path { return Path(string: url.path) } var path: DynamicPath { return DynamicPath(Path(string: url.path)) }
/** /**
Creates a new temporary directory. Creates a new temporary directory.
@@ -51,7 +51,7 @@ class TemporaryDirectory {
} }
extension Path { extension Path {
static func mktemp<T>(body: (Path) throws -> T) throws -> T { static func mktemp<T>(body: (DynamicPath) throws -> T) throws -> T {
let tmp = try TemporaryDirectory() let tmp = try TemporaryDirectory()
return try body(tmp.path) return try body(tmp.path)
} }

View File

@@ -1,7 +1,11 @@
#if !canImport(ObjectiveC)
import XCTest import XCTest
extension PathTests { extension PathTests {
static let __allTests = [ // DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__PathTests = [
("testBasename", testBasename), ("testBasename", testBasename),
("testBundleExtensions", testBundleExtensions), ("testBundleExtensions", testBundleExtensions),
("testCodable", testCodable), ("testCodable", testCodable),
@@ -9,6 +13,7 @@ extension PathTests {
("testConcatenation", testConcatenation), ("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto), ("testCopyInto", testCopyInto),
("testCopyTo", testCopyTo), ("testCopyTo", testCopyTo),
("testCopyToExistingDirectoryFails", testCopyToExistingDirectoryFails),
("testDataExtensions", testDataExtensions), ("testDataExtensions", testDataExtensions),
("testDelete", testDelete), ("testDelete", testDelete),
("testDynamicMember", testDynamicMember), ("testDynamicMember", testDynamicMember),
@@ -19,15 +24,22 @@ extension PathTests {
("testFileHandleExtensions", testFileHandleExtensions), ("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference), ("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes), ("testFilesystemAttributes", testFilesystemAttributes),
("testFindExtension", testFindExtension),
("testFindKinds", testFindKinds),
("testFindMaxDepth1", testFindMaxDepth1),
("testFindMaxDepth2", testFindMaxDepth2),
("testFlatMap", testFlatMap),
("testInitializerForRelativePath", testInitializerForRelativePath), ("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testKind", testKind),
("testLock", testLock), ("testLock", testLock),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto), ("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo), ("testMoveTo", testMoveTo),
("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution), ("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution),
("testPathComponents", testPathComponents),
("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf), ("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf),
("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows), ("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows),
("testReadlinkOnRelativeSymlink", testReadlinkOnRelativeSymlink), ("testReadlinkOnRelativeSymlink", testReadlinkOnRelativeSymlink),
@@ -47,10 +59,9 @@ extension PathTests {
] ]
} }
#if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] { public func __allTests() -> [XCTestCaseEntry] {
return [ return [
testCase(PathTests.__allTests), testCase(PathTests.__allTests__PathTests),
] ]
} }
#endif #endif