Compare commits

...

93 Commits

Author SHA1 Message Date
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
repo-ranger[bot]
24a54c2ee0 Merge pull request #39 from mxcl/less-manifests-test
You can specify future Swifts in a 4.2 manifest!
2019-02-14 01:00:02 +00:00
Max Howell
3735ed4476 You can specify future Swifts in a 4.2 manifest! 2019-02-13 19:40:07 -05:00
Max Howell
2880aa556b This hack did not work in fact 2019-02-13 16:28:16 -05:00
repo-ranger[bot]
a125a871f5 Merge pull request #38 from mxcl/tweaks
Tweaks
2019-02-13 21:13:29 +00:00
Max Howell
d79844cf2b Use a symlink to prevent Package.swift divergence 2019-02-13 15:53:41 -05:00
Max Howell
d0648411ea Get minimum supported Swift version for CocoaPods 2019-02-13 15:47:40 -05:00
Max Howell
e74cc63271 [ci skip] Fix publish release command 2019-02-13 15:27:06 -05:00
Max Howell
28f84d3961 Deploy needs Swift 5 2019-02-13 10:54:10 -05:00
repo-ranger[bot]
db184a13a3 Merge pull request #37 from mxcl/deploy-script
“Scriptify” deployment
2019-02-13 15:28:21 +00:00
Max Howell
b65d167937 “Scriptify” deployment 2019-02-13 10:12:32 -05:00
repo-ranger[bot]
9a770ca576 Merge pull request #36 from mxcl/remove-deployment-targets
These deployment versions are the defaults
2019-02-13 14:49:00 +00:00
Max Howell
b7c189e6af These deployment versions are the defaults
Well, almost, but adjusted we still work so in fact the ”bump” was spurious.
2019-02-13 09:32:19 -05:00
repo-ranger[bot]
2758f0f698 Merge pull request #35 from mxcl/ci-xcode10.2
[ci] Xcode 10.2
2019-02-12 20:41:53 +00:00
Max Howell
e68ad25cc0 [ci] Xcode 10.2 2019-02-12 15:23:27 -05:00
Max Howell
c9d300a7b6 Path(_ url:) -> Path(url:) 2019-02-11 20:40:27 -05:00
Max Howell
ed4b773870 Fill in this TODO in README
[skip ci]
2019-02-11 15:13:14 -05:00
Max Howell
097e020735 There are no usernames on iOS etc. 2019-02-11 15:11:22 -05:00
Max Howell
be49fb9e49 Merge pull request #34 from mxcl/symlinks
Symlink funcs & support `NSURL` file-refs
2019-02-11 15:06:56 -05:00
Max Howell
164cd2b413 Fix iOS, etc. 2019-02-11 14:19:27 -05:00
Max Howell
709c3fb99d Symlink funcs & support NSURL file-refs
* Also removes most `NSString` usage
* Also does more thorough testing in some places
* Also adds
* Fixes `Path?(_:)` resolving symlinks in some cases
2019-02-11 14:04:06 -05:00
repo-ranger[bot]
6c84754ad8 Merge pull request #33 from mxcl/bundle-private-frameworks
Bundle.privateFrameworks
2019-02-09 18:32:02 +00:00
Max Howell
8469565b06 Bundle.privateFrameworks 2019-02-09 13:24:57 -05:00
Max Howell
ed45d10179 Docs updates; CocoaPods 16.0 release; [ci skip] 2019-02-08 09:14:23 -05:00
Max Howell
8033ae49b4 Add [pathos]
[pathos]: https://github.com/dduan/Pathos

[skip ci]
2019-02-04 12:05:14 -05:00
Max Howell
b290173486 Update travis scripts 2019-02-04 12:04:48 -05:00
Max Howell
8248354a80 Some documentation improvements 2019-02-01 15:18:15 -05:00
Max Howell
14963e48f5 Merge pull request #30 from mxcl/codecov
More coverage
2019-02-01 10:26:59 -05:00
Max Howell
7f5340bc19 More coverage
Though I can hardly test these functions, at least we can verify they run
without crashing etc.
2019-02-01 10:17:37 -05:00
Max Howell
74074c634f Merge pull request #29 from mxcl/codecov
More coverage
2019-01-31 21:33:50 -05:00
Max Howell
74656bbfcd More coverage 2019-01-31 21:17:37 -05:00
Max Howell
cced2af2cd Merge pull request #28 from mxcl/codecov
More coverage
2019-01-31 19:17:43 -05:00
Max Howell
b9abd07318 More coverage 2019-01-31 19:09:36 -05:00
Max Howell
6b52932e7b Merge pull request #27 from mxcl/codecov
More coverage
2019-01-31 16:29:08 -05:00
Max Howell
c456081e65 More coverage 2019-01-31 14:26:09 -05:00
Max Howell
ec6c0113f9 Remaining code coverage
Can’t do coverage for Linux unfortunately.
2019-01-31 13:11:50 -05:00
Max Howell
7970c4d8a7 Document robustness & XP
[ci skip]
2019-01-31 12:38:41 -05:00
Max Howell
e342da8644 Remove auto-tagging since I can do this manually 2019-01-31 12:28:16 -05:00
Max Howell
9aa91c649e Merge pull request #26 from mxcl/codecov
Increase code coverage
2019-01-31 12:25:41 -05:00
Max Howell
62ea0d47b3 Fixes & increased code coverage 2019-01-31 12:15:31 -05:00
repo-ranger[bot]
ab9a70e947 Merge pull request #25 from mxcl/rename
Refactor rename -> rename(to:)
2019-01-31 15:32:59 +00:00
Max Howell
49ef073e34 Refactor rename -> rename(to:) 2019-01-31 10:15:39 -05:00
Max Howell
889d825b3a Merge pull request #24 from mxcl/rename
Rename
2019-01-31 10:08:02 -05:00
Max Howell
f1cd06fdff Add CI post success hook yamls 2019-01-31 09:58:46 -05:00
Max Howell
c6e840b9b6 Add rename 2019-01-31 08:39:54 -05:00
Max Howell
eb34ac4af8 Add overwrite parameter to move(into:) 2019-01-31 08:37:32 -05:00
Max Howell
66ae86c986 Enable codecov.io 2019-01-31 08:37:14 -05:00
Max Howell
c432f710eb Merge pull request #22 from mxcl/files()
Entry.files defaults to all files
2019-01-28 12:17:08 -05:00
Max Howell
19c0c19bb6 Entry.files defaults to all files 2019-01-28 12:04:47 -05:00
Max Howell
ee1f46954c Fixes #20
[skip ci]
2019-01-28 11:05:51 -05:00
Max Howell
2394cc1c85 Merge pull request #19 from JaapWijnen/filehandle-extensions
added extension to initialize filehandle from path
2019-01-27 16:59:44 -05:00
Jaap Wijnen
50bb319619 added extension to initialize filehandle from path 2019-01-27 22:57:19 +01:00
Max Howell
9f40068833 Badge for Swift 5 support 2019-01-26 16:15:45 -05:00
Max Howell
67f4e5f41a Merge pull request #16 from mxcl/bundle-non-optional
Bundle extensions don’t return optional Paths
2019-01-26 15:46:03 -05:00
Max Howell
83c83dcaba Bundle extensions don’t return optional Paths
Rationale: Paths are not guaranteed to exist, the Bundle functions return optional if the path doesn't exist. So we'll provide a sensible default instead and you need to check the result exists at some point instead.

This makes more elegant chains, the chain will fail when you operate on it, but you don’t have to do a check for optional first. Or risk a bang.
2019-01-26 15:20:32 -05:00
Max Howell
93e2701950 Docs tweaks
[ci skip]
2019-01-26 15:10:52 -05:00
Max Howell
bbf1f24ef6 Fix Cocoapods deploy 2019-01-26 14:42:00 -05:00
Max Howell
c08ccdfb30 Merge pull request #15 from mxcl/dynamic-members
Dynamic members
2019-01-26 13:34:17 -05:00
Max Howell
859164e59f Dynamic Members 2019-01-26 13:23:25 -05:00
Max Howell
44be1c45a9 Add Path.ctime 2019-01-26 13:17:39 -05:00
Max Howell
99b948f9c1 Minor documentation fixes
[ci skip]
2019-01-26 13:17:39 -05:00
Max Howell
3beba13677 Merge pull request #14 from mxcl/delete-noop
Delete noop
2019-01-26 11:12:17 -05:00
Max Howell
bafb05ff54 Document noop behavior 2019-01-26 11:05:49 -05:00
Max Howell
356a1b3ac2 Delete is a noop if file doesn’t exist
Closes #11.
2019-01-26 11:05:31 -05:00
Max Howell
6d8712f4d6 Remove mkpath, add mkdir(.p) 2019-01-26 11:05:10 -05:00
Max Howell
8744b68709 Make nodoc work for Codable 2019-01-26 10:59:20 -05:00
Max Howell
9ea32048f7 Merge pull request #13 from mxcl/more-docs
Improved documentation; Fixes #12
2019-01-25 21:20:24 -05:00
Max Howell
4b16dac3bf Improved documentation; Fixes #12 2019-01-25 20:46:37 -05:00
Max Howell
b613449232 Making this static will fix documentation location
[ci skip]
2019-01-25 12:23:54 -05:00
Max Howell
db135e32c8 Tag 0.5.0 2019-01-25 11:08:57 -05:00
Max Howell
bfcc48db20 Merge pull request #10 from mxcl/fixes
Swift 5 / Xcode 10.2 / Fixes
2019-01-25 11:04:35 -05:00
Max Howell
b0bf0d0074 This is not yet fixed in Linux Swift 5.0 2019-01-25 10:55:02 -05:00
Max Howell
5f364fe21b Test against Swift 5 snapshot 2019-01-25 10:40:10 -05:00
Max Howell
fdff3bcc05 Swift 5 --warnings 2019-01-24 15:02:06 -05:00
Max Howell
2388c384a1 Swift 5 Manifest (untested until Travis catches up) 2019-01-24 15:01:53 -05:00
Max Howell
80960f5876 Don’t overwrite a file with a directory 2019-01-24 14:44:01 -05:00
Max Howell
9eca479f7b Fix mtime return so it is as per doc contract 2019-01-24 14:43:45 -05:00
Max Howell
ca9f1e0a74 Parallelize tests 2019-01-23 11:25:51 -05:00
Max Howell
de4fb3ae47 Remove replaceContents, user can use String.write 2019-01-22 17:57:28 -05:00
Max Howell
58d026c8a9 Fix copy(into:) overwrite mode
Seems like Linux Foundation has a bug

I checked, seems fixed in Swift 5. But added a swift version check se we can verify and if not report the bug.
2019-01-22 15:26:04 -05:00
Max Howell
43d3e0a745 Improve docs 2019-01-21 14:30:12 -05:00
Max Howell
21fb03b9d9 Test .. paths work with join 2019-01-21 14:26:56 -05:00
Max Howell
3333c731d3 Fix Travis 2019-01-21 12:57:13 -05:00
Max Howell
e15173cfbc Merge pull request #9 from LucianoPAlmeida/is-path-conveniece-add
Adding convenience extensions on Path->Bool
2019-01-20 19:28:05 -05:00
Luciano Almeida
7be264a38e Adding convenience extensions on Path->Bool 2019-01-20 22:13:13 -02:00
20 changed files with 1644 additions and 344 deletions

2
.github/codecov.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
ignore:
- Tests

168
.github/deploy vendored Executable file
View File

@@ -0,0 +1,168 @@
#!/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 ~> 0.15
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)\n\n\(error.legibleDescription)")
}
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.author = { '\(user.name)': '\(user.email)' }
s.source = { git: "https://github.com/\(slug).git", tag: '\(tag)' }
s.version = '\(tag)'
s.summary = '\(repo.description)'
s.license = '\(repo.license.spdx_id)'
s.homepage = "https://github.com/\(slug)"
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 {
let tag_name = tag
let name = 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()

13
.github/jazzy.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
module: Path
custom_categories:
- name: Path
children:
- Path
- /(_:_:)
xcodebuild_arguments:
- UseModernBuildSystem=NO
output:
../output
# output directory is relative to config file… ugh
exclude:
- Sources/Path+StringConvertibles.swift

2
.github/ranger.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
merges:
- action: delete_branch

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.DS_Store .DS_Store
/.build /.build
/*.xcodeproj /*.xcodeproj
/build
/docs

View File

@@ -12,21 +12,31 @@ language: swift
osx_image: xcode10.1 osx_image: xcode10.1
xcode_project: Path.swift.xcodeproj xcode_project: Path.swift.xcodeproj
xcode_scheme: Path.swift-Package xcode_scheme: Path.swift-Package
jobs: jobs:
include: include:
- script: swift test - name: macOS / Swift 4.0.3
name: macOS script: swift test --parallel -Xswiftc -swift-version -Xswiftc 4
- name: macOS / Swift 4.2.1
script: swift test --parallel
- name: macOS / Swift 5.0
osx_image: xcode10.2
script: swift test --parallel
- &xcodebuild - &xcodebuild
before_install: swift package generate-xcodeproj 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 name: iOS / Swift 4.2.1
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 name: tvOS / Swift 4.2.1
- <<: *xcodebuild - <<: *xcodebuild
name: watchOS name: watchOS / Swift 4.2.1
script: | script: |
set -o pipefail set -o pipefail
xcodebuild \ xcodebuild \
@@ -34,43 +44,36 @@ jobs:
-scheme Path.swift-Package \ -scheme Path.swift-Package \
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \ -destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty build | xcpretty
after_success: false
- env: SWIFT_VERSION=4.2.1 - &linux
env: SWIFT_VERSION=4.2.1
os: linux os: linux
name: Linux name: Linux / Swift 4.2.1
language: generic language: generic
dist: trusty 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)"
script: swift test script: swift test --parallel
- <<: *linux
env: SWIFT_VERSION='5.0-DEVELOPMENT-SNAPSHOT-2019-01-22-a'
name: Linux / Swift 5.0.0-dev+2019.01.22
- stage: pretest - stage: pretest
name: Check if Linux tests are up-to-date 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
before_install: | install: gem install jazzy
cat << EOF > .jazzy.yaml before_script: swift package generate-xcodeproj
module: Path
module_version: $TRAVIS_TAG
custom_categories:
- name: Path
children:
- Path
- /(_:_:)
xcodebuild_arguments:
- UseModernBuildSystem=NO
output: output
github_url: https://github.com/mxcl/Path.swift
EOF
install: |
gem install jazzy
swift package generate-xcodeproj
script: | script: |
jazzy jazzy --config .github/jazzy.yml \
rm -rf output/docsets --module-version $TRAVIS_TAG \
--github_url "https://github.com/$TRAVIS_REPO_SLUG"
deploy: deploy:
provider: pages provider: pages
skip-cleanup: true skip-cleanup: true
@@ -80,24 +83,8 @@ jobs:
tags: true tags: true
- name: CocoaPods - name: CocoaPods
before_install: | osx_image: xcode10.2
cat << EOF > Path.swift.podspec install: brew install mxcl/made/swift-sh
Pod::Spec.new do |s| before_script: .github/deploy generate-podspec
s.name = 'Path.swift' script: pod trunk push
s.version = '$TRAVIS_TAG' after_success: .github/deploy publish-release
s.summary = 'Delightful, robust file-pathing functions'
s.homepage = 'https://github.com/mxcl/Path.swift'
s.license = { :type => 'Unlicense', :file => 'LICENSE.md' }
s.author = { 'mxcl' => 'mxcl@me.com' }
s.source = { :git => 'https://github.com/mxcl/Path.swift.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/mxcl'
s.osx.deployment_target = '10.10'
s.ios.deployment_target = '8.0'
s.tvos.deployment_target = '10.0'
s.watchos.deployment_target = '3.0'
s.source_files = 'Sources/*'
s.swift_version = "4.2"
end
EOF
install: gem install cocoapods --pre
script: pod trunk push --allow-warnings

View File

@@ -1,7 +1,7 @@
// swift-tools-version:4.2 // swift-tools-version:4.2
import PackageDescription import PackageDescription
let package = Package( let pkg = Package(
name: "Path.swift", name: "Path.swift",
products: [ products: [
.library(name: "Path", targets: ["Path"]), .library(name: "Path", targets: ["Path"]),
@@ -9,5 +9,6 @@ let package = Package(
targets: [ targets: [
.target(name: "Path", path: "Sources"), .target(name: "Path", path: "Sources"),
.testTarget(name: "PathTests", dependencies: ["Path"]), .testTarget(name: "PathTests", dependencies: ["Path"]),
] ],
swiftLanguageVersions: [.v4, .v4_2, .version("5")]
) )

195
README.md
View File

@@ -1,7 +1,7 @@
# Path.swift ![badge-platforms] ![badge-languages] [![Build Status](https://travis-ci.com/mxcl/Path.swift.svg)](https://travis-ci.com/mxcl/Path.swift) # Path.swift ![badge-platforms][] ![badge-languages][] [![badge-ci][]][travis] [![badge-jazzy][]][docs] [![badge-codecov][]][codecov] [![badge-version][]][cocoapods]
A file-system pathing library focused on developer experience and robust A file-system pathing library focused on developer experience and robust end
endresults. results.
```swift ```swift
import Path import Path
@@ -15,37 +15,50 @@ let docs = Path.home/"Documents"
// paths are *always* absolute thus avoiding common bugs // paths are *always* absolute thus avoiding common bugs
let path = Path(userInput) ?? Path.cwd/userInput let path = Path(userInput) ?? Path.cwd/userInput
// chainable syntax so you have less boilerplate // elegant, chainable syntax
try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555) try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555)
// easy file-management // sensible considerations
try Path.root.join("foo").copy(to: Path.root/"bar") try Path.home.join("bar").mkdir()
try Path.home.join("bar").mkdir() // doesnt throw we already have the desired result
// careful API to avoid common bugs // easy file-management
try Path.root.join("foo").copy(into: Path.root.mkdir("bar")) let bar = try Path.root.join("foo").copy(to: Path.root/"bar")
// ^^ other libraries would make the above `to:` form handle both these cases print(bar) // => /bar
// but that can easily lead to bugs where you accidentally write files that print(bar.isFile) // => true
// were meant to be directory destinations
// careful API considerations so as to avoid common bugs
let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
print(foo) // => /bar/foo
print(foo.isFile) // => true
// we support dynamic members (_use_sparingly_):
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
// a practical example: installing a helper executable
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)
``` ```
We emphasize safety and correctness, just like Swift, and also just We emphasize safety and correctness, just like Swift, and also (again like
like Swift, we provide a thoughtful and comprehensive (yet concise) API. 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, and
probably you already use some of it (Homebrew anyone?). Please help me so I probably you already use some of it (Homebrew anyone?). I work full-time on
can continue to make tools and software you need and love. I appreciate it x. 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">
</a> </a>
[Other donation/tipping options](http://mxcl.github.io/donate/) [Other donation/tipping options](http://mxcl.dev/#donate)
# Handbook # Handbook
Our [online API documentation] is automatically updated for new releases. Our [online API documentation][docs] covers 100% of our public API and is
automatically updated for new releases.
## Codable ## Codable
@@ -86,6 +99,19 @@ decoder.userInfo[.relativePath] = Path.home
decoder.decode(from: data) decoder.decode(from: data)
``` ```
## Dynamic members
We support `@dynamicMemberLookup`:
```swift
let ls = Path.root.usr.bin.ls // => /usr/bin/ls
```
This is less commonly useful than you would think, hence our documentation
does not use it. Usually you are joining variables or other `String` arguments
or trying to describe files (and files usually have extensions). However when
you need it, its *lovely*.
## Initializing from user-input ## Initializing from user-input
The `Path` initializer returns `nil` unless fed an absolute path; thus to The `Path` initializer returns `nil` unless fed an absolute path; thus to
@@ -99,10 +125,8 @@ This is explicit, not hiding anything that code-review may miss and preventing
common bugs like accidentally creating `Path` objects from strings you did not common bugs like accidentally creating `Path` objects from strings you did not
expect to be relative. expect to be relative.
Our initializer is nameless because we conform to `LosslessStringConvertible`, Our initializer is nameless to be consistent with the equivalent operation for
the same conformance as that `Int`, `Float` etc. conform. The protocol enforces converting strings to `Int`, `Float` etc. in the standard library.
a nameless initialization and since it is appropriate for us to conform to it,
we do.
## Extensions ## Extensions
@@ -116,8 +140,7 @@ bashProfile += "\n\nfoo"
try bashProfile.write(to: Path.home/".bash_profile") try bashProfile.write(to: Path.home/".bash_profile")
try Bundle.main.resources!.join("foo").copy(to: .home) try Bundle.main.resources.join("foo").copy(to: .home)
// ^^ `-> Path?` because the underlying `Bundle` function is `-> String?`
``` ```
## Directory listings ## Directory listings
@@ -139,13 +162,32 @@ for entry in Path.home.ls() where entry.path.mtime > yesterday {
// //
} }
let dirs = Path.home.ls().directories().filter { let dirs = Path.home.ls().directories
//
} let files = Path.home.ls().files
let swiftFiles = Path.home.ls().files(withExtension: "swift") let swiftFiles = Path.home.ls().files(withExtension: "swift")
``` ```
# `Path.swift` is robust
Some parts of `FileManager` are not exactly idiomatic. For example
`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
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
with your work without worries.
There is also some magic going on in Foundations filesystem APIs, which we look
for and ensure our API is deterministic, eg. [this test].
[this test]: https://github.com/mxcl/Path.swift/blob/master/Tests/PathTests/PathTests.swift#L539-L554
# `Path.swift` is properly cross-platform
`FileManager` on Linux is full of holes. We have found the holes and worked
round them where necessary.
# Rules & Caveats # Rules & Caveats
Paths are just string representations, there *might not* be a real file there. Paths are just string representations, there *might not* be a real file there.
@@ -162,6 +204,10 @@ Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash // joining with absolute paths omits prefixed slash
Path.home/"/b" // => /Users/mxcl/b Path.home/"/b" // => /Users/mxcl/b
// joining with .. or . works as expected
Path.home.foo.bar.join("..") // => /Users/mxcl/foo
Path.home.foo.bar.join(".") // => /Users/mxcl/foo/bar
// of course, feel free to join variables: // of course, feel free to join variables:
let b = "b" let b = "b"
let c = "c" let c = "c"
@@ -174,38 +220,119 @@ Path.root/"~/b" // => /~/b
// but is here // but is here
Path("~/foo")! // => /Users/mxcl/foo Path("~/foo")! // => /Users/mxcl/foo
// this does not work though // this works provided the user `Guest` exists
Path("~Guest") // => /Users/Guest
// but if the user does not exist
Path("~foo") // => nil Path("~foo") // => nil
// paths with .. or . are resolved
Path("/foo/bar/../baz") // => /foo/baz
// symlinks are not resolved
Path.root.bar.symlink(as: "foo")
Path("foo") // => /foo
Path.foo // => /foo
// unless you do it explicitly
try Path.foo.readlink() // => /bar
// `readlink` only resolves the *final* path component,
// thus use `realpath` if there are multiple symlinks
``` ```
*Path.swift* has the general policy that if the desired end result preexists,
then its a noop:
* If you try to delete a file, but the file doesn't exist, 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`
However notably if you try to copy or move a file with specifying `overwrite`
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.
## Symbolic links
* Two paths may represent the same *resolved* path yet not be equal due to
symlinks in such cases you should use `realpath` on both first if an
equality check is required.
* There are several symlink paths on Mac that are typically automatically
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
`Path.init`, *nor* if you are joining a path that ends up being one of these
paths, (eg. `Path.root.join("var/private')`).
## We do not provide change directory functionality
Changing directory is dangerous, you should *always* try to avoid it and thus
we dont even provide the method. If you are executing a sub-process then
use `Process.currentDirectoryURL`.
If you must then use `FileManager.changeCurrentDirectory`.
# I thought I should only use `URL`s?
Apple recommend this because they provide a magic translation for
[file-references embodied by URLs][file-refs], which gives you URLs like so:
file:///.file/id=6571367.15106761
Therefore, if you are not using this feature you are fine. If you have URLs the correct
way to get a `Path` is:
```swift
if let path = Path(url: url) {
/**/
}
```
Our initializer calls `path` on the URL which resolves any reference to an
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
# Installation # Installation
SwiftPM: SwiftPM:
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.4.1")) package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.13.0"))
``` ```
CocoaPods: CocoaPods:
```ruby ```ruby
pod 'Path.swift' ~> '0.4.1' pod 'Path.swift', '~> 0.13'
``` ```
Please note! We are pre 1.0, thus we can change the API as we like! We will tag Carthage:
1.0 as soon as possible.
> 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 ### Get push notifications for new releases
https://codebasesaga.com/canopy/ https://mxcl.dev/canopy/
# Alternatives # Alternatives
* [Pathos](https://github.com/dduan/Pathos) by Daniel Duan
* [PathKit](https://github.com/kylef/PathKit) by Kyle Fuller * [PathKit](https://github.com/kylef/PathKit) by Kyle Fuller
* [Files](https://github.com/JohnSundell/Files) by John Sundell * [Files](https://github.com/JohnSundell/Files) by John Sundell
* [Utility](https://github.com/apple/swift-package-manager) by Apple * [Utility](https://github.com/apple/swift-package-manager) by Apple
[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-orange.svg [badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0-orange.svg
[online API documentation]: 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-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg
[badge-ci]: https://travis-ci.com/mxcl/Path.swift.svg
[travis]: https://travis-ci.com/mxcl/Path.swift
[codecov]: https://codecov.io/gh/mxcl/Path.swift
[badge-version]: https://img.shields.io/cocoapods/v/Path.swift.svg?label=version
[cocoapods]: https://cocoapods.org/pods/Path.swift

View File

@@ -1,5 +1,6 @@
import Foundation import Foundation
/// Extensions on Foundations `Bundle` so you get `Path` rather than `String` or `URL`.
public extension Bundle { public extension Bundle {
/// Returns the path for requested resource in this bundle. /// Returns the path for requested resource in this bundle.
func path(forResource: String, ofType: String?) -> Path? { func path(forResource: String, ofType: String?) -> Path? {
@@ -8,22 +9,39 @@ public extension Bundle {
return str.flatMap(Path.init) return str.flatMap(Path.init)
} }
/// Returns the path for the shared-frameworks directory in this bundle. /**
public var sharedFrameworks: Path? { Returns the path for the shared-frameworks directory in this bundle.
return sharedFrameworksPath.flatMap(Path.init) - Note: This is typically `ShareFrameworks`
*/
var sharedFrameworks: Path {
return sharedFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath
}
/**
Returns the path for the private-frameworks directory in this bundle.
- Note: This is typically `Frameworks`
*/
var privateFrameworks: Path {
return privateFrameworksPath.flatMap(Path.init) ?? defaultSharedFrameworksPath
} }
/// Returns the path for the resources directory in this bundle. /// Returns the path for the resources directory in this bundle.
public var resources: Path? { var resources: Path {
return resourcePath.flatMap(Path.init) return resourcePath.flatMap(Path.init) ?? defaultResourcesPath
} }
/// Returns the path for this bundle. /// Returns the path for this bundle.
public var path: Path { var path: Path {
return Path(string: bundlePath) return Path(string: bundlePath)
} }
/// Returns the executable for this bundle, if there is one, not all bundles have one hence `Optional`.
var executable: Path? {
return executablePath.flatMap(Path.init)
}
} }
/// Extensions on `String` that work with `Path` rather than `String` or `URL`
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
@@ -40,6 +58,7 @@ public extension String {
} }
} }
/// Extensions on `Data` that work with `Path` rather than `String` or `URL`
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
@@ -65,3 +84,46 @@ public extension Data {
return to return to
} }
} }
/// Extensions on `FileHandle` that work with `Path` rather than `String` or `URL`
public extension FileHandle {
/// Initializes this `FileHandle` for reading at the location of the provided path.
@inlinable
convenience init(forReadingAt path: Path) throws {
try self.init(forReadingFrom: path.url)
}
/// Initializes this `FileHandle` for writing at the location of the provided path.
@inlinable
convenience init(forWritingAt path: Path) throws {
try self.init(forWritingTo: path.url)
}
/// Initializes this `FileHandle` for reading and writing at the location of the provided path.
@inlinable
convenience init(forUpdatingAt path: Path) throws {
try self.init(forUpdating: path.url)
}
}
internal extension Bundle {
var defaultSharedFrameworksPath: Path {
#if os(macOS)
return path.join("Contents/Frameworks")
#elseif os(Linux)
return path.join("lib")
#else
return path.join("Frameworks")
#endif
}
var defaultResourcesPath: Path {
#if os(macOS)
return path.join("Contents/Resources")
#elseif os(Linux)
return path.join("share")
#else
return path
#endif
}
}

View File

@@ -1,23 +1,74 @@
import Foundation import Foundation
public extension Path { public extension Path {
/// - Note: If file is already locked, does nothing //MARK: Filesystem Attributes
/// - Note: If file doesnt exist, throws
/**
Returns the creation-time of the file.
- Note: Returns `nil` if there is no creation-time, this should only happen if the file doesnt exist.
- Important: On Linux this is filesystem dependendent and may not exist.
*/
var ctime: Date? {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.creationDate] as? Date
} catch {
return nil
}
}
/**
Returns the modification-time of the file.
- Note: If this returns `nil` and the file exists, something is very wrong.
*/
var mtime: Date? {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date
} catch {
return nil
}
}
/**
Sets the files attributes using UNIX octal notation.
Path.home.join("foo").chmod(0o555)
*/
@discardableResult @discardableResult
public func lock() throws -> Path { func chmod(_ octal: Int) throws -> Path {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return self
}
/**
Applies the macOS filesystem lock attribute.
- Note: If file is already locked, does nothing.
- Note: If file doesnt exist, throws.
- Important: On Linux does nothing.
*/
@discardableResult
func lock() throws -> Path {
#if !os(Linux)
var attrs = try FileManager.default.attributesOfItem(atPath: string) var attrs = try FileManager.default.attributesOfItem(atPath: string)
let b = attrs[.immutable] as? Bool ?? false let b = attrs[.immutable] as? Bool ?? false
if !b { if !b {
attrs[.immutable] = true attrs[.immutable] = true
try FileManager.default.setAttributes(attrs, ofItemAtPath: string) try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
} }
#endif
return self return self
} }
/// - Note: If file isnt locked, does nothing /**
/// - Note: If file doesnt exist, does nothing - Note: If file isnt locked, does nothing.
- Note: If file doesnt exist, does nothing.
- Important: On Linux does nothing.
- SeeAlso: `lock()`
*/
@discardableResult @discardableResult
public func unlock() throws -> Path { func unlock() throws -> Path {
#if !os(Linux)
var attrs: [FileAttributeKey: Any] var attrs: [FileAttributeKey: Any]
do { do {
attrs = try FileManager.default.attributesOfItem(atPath: string) attrs = try FileManager.default.attributesOfItem(atPath: string)
@@ -29,32 +80,7 @@ public extension Path {
attrs[.immutable] = false attrs[.immutable] = false
try FileManager.default.setAttributes(attrs, ofItemAtPath: string) try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
} }
#endif
return self return self
} }
/**
Sets the files attributes using UNIX octal notation.
Path.home.join("foo").chmod(0o555)
*/
@discardableResult
public func chmod(_ octal: Int) throws -> Path {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return self
}
/**
Returns the modification-time.
- Note: Returns the creation time if there is no modification time.
- Note: Returns UNIX-time-zero if neither are available, though this *should* be impossible.
*/
public var mtime: Date {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date()
} catch {
//TODO log error
return Date(timeIntervalSince1970: 0)
}
}
} }

View File

@@ -1,11 +1,31 @@
import Foundation import Foundation
/**
Provided for relative-path coding. See the instructions in our
[README](https://github.com/mxcl/Path.swift/#codable).
*/
public extension CodingUserInfoKey { public extension CodingUserInfoKey {
/// If set paths are encoded as relative to this path. /**
If set on an `Encoder`s `userInfo` all paths are encoded relative to this path.
For example:
let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home
encoder.encode([Path.home, Path.home/"foo"])
- Remark: See the [README](https://github.com/mxcl/Path.swift/#codable) for more information.
*/
static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")! static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")!
} }
extension Path: Codable { /**
Provided for relative-path coding. See the instructions in our
[README](https://github.com/mxcl/Path.swift/#codable).
*/
extension Path: Codable {
/// - SeeAlso: `CodingUserInfoKey.relativePath`
/// :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("/") {
@@ -18,6 +38,8 @@ extension Path: Codable {
} }
} }
/// - SeeAlso: `CodingUserInfoKey.relativePath`
/// :nodoc:
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
if let root = encoder.userInfo[.relativePath] as? Path { if let root = encoder.userInfo[.relativePath] as? Path {

View File

@@ -1,7 +1,9 @@
import Foundation import Foundation
extension Path { extension Path {
/// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`. //MARK: Common Directories
/// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
public static var cwd: Path { public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath) return Path(string: FileManager.default.currentDirectoryPath)
} }
@@ -45,18 +47,7 @@ extension Path {
fatalError() fatalError()
} }
#else #else
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { return defaultUrl(for: searchPath) }
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
case .applicationSupportDirectory:
return Path.home/"Library/Application Support"
case .cachesDirectory:
return Path.home/"Library/Caches"
default:
fatalError()
}
}
return Path(string: pathString) return Path(string: pathString)
#endif #endif
} }
@@ -72,7 +63,7 @@ extension Path {
/** /**
The root for cache files. The root for cache files.
- 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: Path {
@@ -88,3 +79,19 @@ extension Path {
return path(for: .applicationSupportDirectory) return path(for: .applicationSupportDirectory)
} }
} }
#if !os(Linux)
func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> Path {
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
case .applicationSupportDirectory:
return Path.home/"Library/Application Support"
case .cachesDirectory:
return Path.home/"Library/Caches"
default:
fatalError()
}
}
#endif

View File

@@ -1,19 +1,38 @@
import Foundation import Foundation
#if os(Linux)
import Glibc
#endif
public extension Path { public extension Path {
//MARK: File Management
/** /**
Copies a file. Copies a file.
try Path.root.join("bar").copy(to: Path.home/"foo")
// => "/Users/mxcl/foo"
- Note: `throws` if `to` is a directory. - Note: `throws` if `to` is a directory.
- Parameter to: Destination filename. - Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`. - Parameter overwrite: If `true` and both `self` and `to` are files, overwrites `to`.
- Note: If either `self` or `to are directories, `overwrite` is ignored.
- Note: Throws if `overwrite` is `false` yet `to` is *already* identical to
`self` because even though *Path.swifts* policy is to noop if the desired
end result preexists, checking for this condition is too expensive a
trade-off.
- Returns: `to` to allow chaining - Returns: `to` to allow chaining
- SeeAlso: copy(into:overwrite:) - SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
public func copy(to: Path, overwrite: Bool = false) throws -> Path { func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.isFile, isFile {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
#if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, to.isFile {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
try FileManager.default.copyItem(atPath: string, toPath: to.string) try FileManager.default.copyItem(atPath: string, toPath: to.string)
return to return to
} }
@@ -21,37 +40,62 @@ public extension Path {
/** /**
Copies a file into a directory Copies a file into a directory
If the destination does not exist, this function creates the directory first. try Path.root.join("bar").copy(into: .home)
// => "/Users/mxcl/bar"
// Create ~/.local/bin, copy `ls` there and make the new copy executable
try Path.root.join("bin/ls").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
If the destination does not exist, this function creates the directory
(including intermediary directories if necessary) first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory - Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`. - Parameter overwrite: If true overwrites any file that already exists at `into`.
- Returns: The `Path` of the newly copied file. - Returns: The `Path` of the newly copied file.
- SeeAlso: copy(into:overwrite:) - Note: `throws` if `into` is a file.
- Note: Throws if `overwrite` is `false` yet `to` is *already* identical to
`self` because even though *Path.swifts* policy is to noop if the desired
end result preexists, checking for this condition is too expensive a
trade-off.
- SeeAlso: `copy(to:overwrite:)`
*/ */
@discardableResult @discardableResult
public func copy(into: Path, overwrite: Bool = false) throws -> Path { func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) try into.mkdir(.p)
} else if overwrite, !into.isDirectory {
try into.delete()
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
}
#if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, rv.isFile {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
try FileManager.default.copyItem(at: url, to: rv.url) try FileManager.default.copyItem(at: url, to: rv.url)
return rv return rv
} }
/** /**
Moves a file. Moves a file.
- Note: `throws` if `to` is a directory.
try Path.root.join("bar").move(to: Path.home/"foo")
// => "/Users/mxcl/foo"
- Parameter to: Destination filename. - Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`. - Parameter overwrite: If true overwrites any file that already exists at `to`.
- Returns: `to` to allow chaining - Returns: `to` to allow chaining
- SeeAlso: move(into:overwrite:) - Note: `throws` if `to` is a directory.
- Note: Throws if `overwrite` is `false` yet `to` is *already* identical to
`self` because even though *Path.swifts* policy is to noop if the desired
end result preexists, checking for this condition is too expensive a
trade-off.
- SeeAlso: `move(into:overwrite:)`
*/ */
@discardableResult @discardableResult
public func move(to: Path, overwrite: Bool = false) throws -> Path { func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.isFile {
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)
@@ -61,114 +105,142 @@ public extension Path {
/** /**
Moves a file into a directory Moves a file into a directory
If the destination does not exist, this function creates the directory first. try Path.root.join("bar").move(into: .home)
// => "/Users/mxcl/bar"
If the destination does not exist, this function creates the directory
(including intermediary directories if necessary) first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory - Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`. - Parameter overwrite: If true *overwrites* any file that already exists at `into`.
- Note: `throws` if `into` is a file.
- Returns: The `Path` of destination filename. - Returns: The `Path` of destination filename.
- SeeAlso: move(into:overwrite:) - SeeAlso: `move(to:overwrite:)`
*/ */
@discardableResult @discardableResult
public func move(into: Path) throws -> Path { func move(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if !into.exists {
try into.mkpath() try into.mkdir(.p)
} else if !into.isDirectory { } else if !into.isDirectory {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile {
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)
return rv return rv
} }
/// Deletes the path, recursively if a directory. /**
Deletes the path, recursively if a directory.
- Note: noop: if the path doesnt exist
*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: This function will fail if the file or directory is locked
- SeeAlso: `lock()`
*/
@inlinable @inlinable
public func delete() throws { func delete() throws {
try FileManager.default.removeItem(at: url) if exists {
try FileManager.default.removeItem(at: url)
}
} }
/** /**
Creates an empty file at this path. Creates an empty file at this path or if the file exists, updates its modification time.
- Returns: `self` to allow chaining. - Returns: `self` to allow chaining.
*/ */
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
return try "".write(to: self) if !exists {
guard FileManager.default.createFile(atPath: string, contents: nil) else {
throw CocoaError.error(.fileWriteUnknown)
}
} else {
#if os(Linux)
let fd = open(string, O_WRONLY)
defer { close(fd) }
futimens(fd, nil)
#else
try FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: string)
#endif
}
return self
} }
/// Helper due to Linux Swift being incomplete. /**
private func _foo(go: () throws -> Void) throws { Creates the directory at this path.
#if !os(Linux) - Parameter options: Specify `mkdir(.p)` to create intermediary directories.
- Note: We do not error if the directory already exists (even without `.p`)
because *Path.swift* noops if the desired end result preexists.
- Returns: `self` to allow chaining.
*/
@discardableResult
func mkdir(_ options: MakeDirectoryOptions? = nil) throws -> Path {
do { do {
try go() let wid = options == .p
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: wid, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists { } catch CocoaError.Code.fileWriteFileExists {
// noop //noop (fails to trigger on Linux)
}
#else
do {
try go()
} catch { } catch {
#if os(Linux)
let error = error as NSError let error = error as NSError
guard error.domain == NSCocoaErrorDomain, error.code == CocoaError.Code.fileWriteFileExists.rawValue else { guard error.domain == NSCocoaErrorDomain, error.code == CocoaError.Code.fileWriteFileExists.rawValue else {
throw error throw error
} }
} #else
#endif throw error
} #endif
/**
Creates the directory at this path.
- Note: Does not create any intermediary directories.
- Returns: `self` to allow chaining.
*/
@discardableResult
public func mkdir() throws -> Path {
try _foo {
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: false, attributes: nil)
} }
return self return self
} }
/** /**
Creates the directory at this path. Renames the file at path.
- Note: Creates any intermediary directories, if required.
- Returns: `self` to allow chaining. Path.root.foo.bar.rename(to: "baz") // => /foo/baz
- Parameter to: the new basename for the file
- Returns: The renamed path.
*/ */
@discardableResult @discardableResult
public func mkpath() throws -> Path { func rename(to newname: String) throws -> Path {
try _foo { let newpath = parent/newname
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
} return newpath
return self
} }
/** /**
Replaces the contents of the file at this path with the provided string. Creates a symlink of this file at `as`.
- Note: If file doesnt exist, creates file - Note: If `self` does not exist, is **not** an error.
- Note: If file is not writable, makes writable first, resetting permissions after the write
- Parameter contents: The string that will become the contents of this file.
- Parameter atomically: If `true` the operation will be performed atomically.
- Parameter encoding: The string encoding to use.
- Returns: `self` to allow chaining.
*/ */
@discardableResult @discardableResult
public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path { func symlink(as: Path) throws -> Path {
let resetPerms: Int? try FileManager.default.createSymbolicLink(atPath: `as`.string, withDestinationPath: string)
if exists, !isWritable { return `as`
resetPerms = try FileManager.default.attributesOfItem(atPath: string)[.posixPermissions] as? Int }
let perms = resetPerms ?? 0o777
try chmod(perms | 0o200) /**
} else { Creates a symlink of this file with the same filename in the `into` directory.
resetPerms = nil - Note: If into does not exist, creates the directory with intermediate directories if necessary.
*/
@discardableResult
func symlink(into dir: Path) throws -> Path {
if !dir.exists {
try dir.mkdir(.p)
} else if !dir.isDirectory {
throw CocoaError.error(.fileWriteFileExists)
} }
let dst = dir/basename()
defer { try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
_ = try? resetPerms.map(self.chmod) return dst
}
try contents.write(to: self)
return self
} }
} }
/// Options for `Path.mkdir(_:)`
public enum MakeDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`.
case p
}

View File

@@ -1,14 +1,4 @@
import class Foundation.NSString extension Path: CustomStringConvertible {
extension Path: LosslessStringConvertible {
/// Returns `nil` unless fed an absolute path
public init?(_ description: String) {
guard description.starts(with: "/") || description.starts(with: "~/") else { return nil }
self.init(string: (description as NSString).standardizingPath)
}
}
extension Path: CustomStringConvertible {
/// Returns `Path.string` /// Returns `Path.string`
public var description: String { public var description: String {
return string return string
@@ -18,6 +8,6 @@ extension Path: CustomStringConvertible {
extension Path: CustomDebugStringConvertible { extension Path: CustomDebugStringConvertible {
/// Returns eg. `Path(string: "/foo")` /// Returns eg. `Path(string: "/foo")`
public var debugDescription: String { public var debugDescription: String {
return "Path(string: \(string))" return "Path(\(string))"
} }
} }

View File

@@ -1,8 +1,28 @@
import Foundation import Foundation
public extension Path { /**
A file entry from a directory listing.
- SeeAlso: `ls()`
*/
public struct Entry {
/// The kind of this directory entry.
public enum Kind {
/// The path is a file.
case file
/// The path is a directory.
case directory
}
/// The kind of this entry.
public let kind: Kind
/// The path of this entry.
public let path: Path
}
public extension Path {
//MARK: Directory Listings
/** /**
Same as the `ls -a` command is shallow Same as the `ls -a` command output is shallow and unsorted.
- Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`. - Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`.
- Important: `includeHiddenFiles` does not work on Linux - Important: `includeHiddenFiles` does not work on Linux
*/ */
@@ -22,7 +42,8 @@ public extension Path {
} }
} }
public extension Array where Element == Path.Entry { /// Convenience functions for the array return value of `Path.ls()`
public extension Array where Element == Entry {
/// 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.
var directories: [Path] { var directories: [Path] {
return compactMap { return compactMap {
@@ -30,7 +51,14 @@ public extension Array where Element == Path.Entry {
} }
} }
/// Filters the list of entries to be a list of Paths that are files with the specified extension /// Filters the list of entries to be a list of Paths that are files.
var files: [Path] {
return compactMap {
$0.kind == .file ? $0.path : nil
}
}
/// Filters the list of entries to be a list of Paths that are files with the specified extension.
func files(withExtension ext: String) -> [Path] { func files(withExtension ext: String) -> [Path] {
return compactMap { return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil $0.kind == .file && $0.path.extension == ext ? $0.path : nil

View File

@@ -1,15 +1,16 @@
import Foundation import Foundation
#if os(Linux)
import func Glibc.access
#else
import Darwin
#endif
public extension Path { public extension Path {
/// Returns true if the path represents an actual file that is also writable by the current user. //MARK: Filesystem Properties
var isWritable: Bool {
return FileManager.default.isWritableFile(atPath: string)
}
/// Returns true if the path represents an actual directory. /// Returns true if the path represents an actual filesystem entry.
var isDirectory: Bool { var exists: Bool {
var isDir: ObjCBool = false return FileManager.default.fileExists(atPath: string)
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
} }
/// Returns true if the path represents an actual filesystem entry that is *not* a directory. /// Returns true if the path represents an actual filesystem entry that is *not* a directory.
@@ -18,13 +19,47 @@ public extension Path {
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue
} }
/// Returns true if the path represents an actual file that is also executable by the current user. /// Returns true if the path represents an actual directory.
var isExecutable: Bool { var isDirectory: Bool {
return FileManager.default.isExecutableFile(atPath: string) var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
} }
/// Returns true if the path represents an actual filesystem entry. /// Returns true if the path represents an actual file that is also readable by the current user.
var exists: Bool { var isReadable: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.isReadableFile(atPath: string)
}
/// Returns true if the path represents an actual file that is also writable by the current user.
var isWritable: Bool {
return FileManager.default.isWritableFile(atPath: string)
}
/// Returns true if the path represents an actual file that is also deletable by the current user.
var isDeletable: Bool {
#if os(Linux) && !swift(>=5.1)
return exists && access(parent.string, W_OK) == 0
#else
// FileManager.isDeletableFile returns true if there is *not* a file there
return exists && FileManager.default.isDeletableFile(atPath: string)
#endif
}
/// Returns true if the path represents an actual file that is also executable by the current user.
var isExecutable: Bool {
if access(string, X_OK) == 0 {
// FileManager.isExxecutableFile returns true even if there is *not*
// a file there *but* if there was it could be *made* executable
return FileManager.default.isExecutableFile(atPath: string)
} else {
return false
}
}
/// Returns `true` if the file is a symbolic-link (symlink).
var isSymlink: Bool {
var sbuf = stat()
lstat(string, &sbuf)
return (sbuf.st_mode & S_IFMT) == S_IFLNK
} }
} }

View File

@@ -1,28 +1,159 @@
import Foundation import Foundation
#if !os(Linux)
import func Darwin.realpath
let _realpath = Darwin.realpath
#else
import func Glibc.realpath
let _realpath = Glibc.realpath
#endif
/** /**
Represents a platform filesystem absolute path. A `Path` represents an absolute path on a filesystem.
The recommended conversions from string are: All functions on `Path` are chainable and short to facilitate doing sequences
of file operations in a concise manner.
`Path` supports `Codable`, and can be configured to
[encode paths *relatively*](https://github.com/mxcl/Path.swift/#codable).
Sorting a `Sequence` of paths will return the locale-aware sort order, which
will give you the same order as Finder.
Converting from a `String` is a common first step, here are the recommended
ways to do that:
let p1 = Path.root/pathString let p1 = Path.root/pathString
let p2 = Path.root/url.path let p2 = Path.root/url.path
let p3 = Path.cwd/relativePathString let p3 = Path.cwd/relativePathString
let p4 = Path(userInput) ?? Path.cwd/userInput let p4 = Path(userInput) ?? Path.cwd/userInput
- Note: There may not be an actual filename at the path. If you are constructing paths from static-strings we provide support for
dynamic members:
let p1 = Path.root.usr.bin.ls // => /usr/bin/ls
- Note: A `Path` does not necessarily represent an actual filesystem entry.
*/ */
@dynamicMemberLookup
public struct Path: Equatable, Hashable, Comparable { public struct Path: Equatable, Hashable, Comparable {
init(string: String) {
assert(string.first == "/")
assert(string.last != "/" || string == "/")
assert(string.split(separator: "/").contains("..") == false)
self.string = string
}
/**
Creates a new absolute, standardized path.
- Note: Resolves any .. or . components.
- Note: Removes multiple subsequent and trailing occurences of `/`.
- Note: Does *not* resolve any symlinks.
- 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`.
*/
public init?<S: StringProtocol>(_ description: S) {
var pathComponents = description.split(separator: "/")
switch description.first {
case "/":
#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
self.string = join_(prefix: "/", pathComponents: pathComponents)
case "~":
if description == "~" {
self = Path.home
return
}
let tilded: String
if description.hasPrefix("~/") {
tilded = Path.home.string
} else {
let username = String(pathComponents[0].dropFirst())
#if os(macOS) || os(Linux)
if #available(OSX 10.12, *) {
guard let url = FileManager.default.homeDirectory(forUser: username) else { return nil }
assert(url.scheme == "file")
tilded = url.path
} else {
guard let dir = NSHomeDirectoryForUser(username) else { return nil }
tilded = dir
}
#else
return nil // there are no usernames on iOS, etc.
#endif
}
pathComponents.remove(at: 0)
self.string = join_(prefix: tilded, pathComponents: pathComponents)
default:
return nil
}
}
/**
Creates a new absolute, standardized path from the provided file-scheme URL.
- Note: If the URL is not a file URL, returns `nil`.
*/
public init?(url: URL) {
guard url.scheme == "file" else { return nil }
self.init(url.path)
//NOTE: URL cannot be a file-reference url, unlike NSURL, so this always works
}
/**
Creates a new absolute, standardized path from the provided file-scheme URL.
- Note: If the URL is not a file URL, returns `nil`.
- Note: If the URL is a file reference URL, converts it to a POSIX path first.
*/
public init?(url: NSURL) {
guard url.scheme == "file", let path = url.path else { return nil }
self.init(string: path)
// ^^ works even if the url is a file-reference url
}
/// :nodoc:
public subscript(dynamicMember addendum: String) -> Path {
//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 Path(string: join_(prefix: string, appending: addendum))
}
//MARK: Properties
/// The underlying filesystem path /// The underlying filesystem path
public let string: String public let string: String
/// Returns a `URL` representing this file path.
public var url: URL {
return URL(fileURLWithPath: string)
}
/** /**
Returns the filename extension of this path. Returns a file-reference URL.
- Remark: Implemented via `NSString.pathExtension`. - Note: Only NSURL can be a file-reference-URL, hence we return NSURL.
- SeeAlso: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
- Important: On Linux returns an file scheme NSURL for this path string.
*/ */
@inlinable public var fileReferenceURL: NSURL? {
public var `extension`: String { #if !os(Linux)
return (string as NSString).pathExtension // https://bugs.swift.org/browse/SR-2728
return (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL
#else
return NSURL(fileURLWithPath: string)
#endif
} }
/** /**
@@ -34,38 +165,91 @@ 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 { public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent) let index = string.lastIndex(of: "/")!
} let substr = string[string.indices.startIndex..<index]
return Path(string: String(substr))
/// Returns a `URL` representing this file path.
@inlinable
public var url: URL {
return URL(fileURLWithPath: string)
} }
/** /**
The basename for the provided file, optionally dropping the file extension. Returns the filename extension of this path.
- Remark: If there is no extension returns "".
Path.root.join("foo.swift").basename() // => "foo.swift" - Remark: If the filename ends with any number of ".", returns "".
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo" - Note: We special case eg. `foo.tar.gz`.
- Returns: A string that is the filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension.
*/ */
public func basename(dropExtension: Bool = false) -> String { @inlinable
let str = string as NSString public var `extension`: String {
if !dropExtension { //FIXME efficiency
return str.lastPathComponent switch true {
} else { case string.hasSuffix(".tar.gz"):
let ext = str.pathExtension return "tar.gz"
if !ext.isEmpty { case string.hasSuffix(".tar.bz"):
return String(str.lastPathComponent.dropLast(ext.count + 1)) return "tar.bz"
case string.hasSuffix(".tar.bz2"):
return "tar.bz2"
case string.hasSuffix(".tar.xz"):
return "tar.xz"
default:
let slash = string.lastIndex(of: "/")!
if let dot = string.lastIndex(of: "."), slash < dot {
let foo = string.index(after: dot)
return String(string[foo...])
} else { } else {
return str.lastPathComponent return ""
} }
} }
} }
/**
Splits the string representation on the directory separator.
- Important: The first element is always "/" to be consistent with `NSString.pathComponents`.
*/
@inlinable
public var components: [String] {
return ["/"] + string.split(separator: "/").map(String.init)
}
//MARK: Pathing
/**
Joins a path and a string to produce a new path.
Path.root.join("a") // => /a
Path.root.join("a/b") // => /a/b
Path.root.join("a").join("b") // => /a/b
Path.root.join("a").join("/b") // => /a/b
- Note: `..` and `.` components are interpreted.
- Note: pathComponent *may* be multiple components.
- Note: symlinks are *not* resolved.
- Parameter pathComponent: The string to join with this path.
- Returns: A new joined path.
- SeeAlso: `Path./(_:_:)`
*/
public func join<S>(_ addendum: S) -> Path where S: StringProtocol {
return Path(string: join_(prefix: string, appending: addendum))
}
/**
Joins a path and a string to produce a new path.
Path.root/"a" // => /a
Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Note: `..` and `.` components are interpreted.
- Note: pathComponent *may* be multiple components.
- Note: symlinks are *not* resolved.
- Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path.
- SeeAlso: `join(_:)`
*/
@inlinable
public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
}
/** /**
Returns a string representing the relative path to `base`. Returns a string representing the relative path to `base`.
@@ -104,59 +288,121 @@ public struct Path: Equatable, Hashable, Comparable {
} }
/** /**
Joins a path and a string to produce a new path. The basename for the provided file, optionally dropping the file extension.
Path.root.join("a") // => /a Path.root.join("foo.swift").basename() // => "foo.swift"
Path.root.join("a/b") // => /a/b Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
Path.root.join("a").join("b") // => /a/b
Path.root.join("a").join("/b") // => /a/b
- Parameter pathComponent: The string to join with this path. - Returns: A string that is the filenames basename.
- Returns: A new joined path. - Parameter dropExtension: If `true` returns the basename without its file extension.
- SeeAlso: /(:Path,:String)
*/ */
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol { public func basename(dropExtension: Bool = false) -> String {
//TODO standardizingPath does more than we want really (eg tilde expansion) var lastPathComponent: Substring {
let str = (string as NSString).appendingPathComponent(String(pathComponent)) let slash = string.lastIndex(of: "/")!
return Path(string: (str as NSString).standardizingPath) let index = string.index(after: slash)
return string[index...]
}
var go: Substring {
if !dropExtension {
return lastPathComponent
} else {
let ext = self.extension
if !ext.isEmpty {
return lastPathComponent.dropLast(ext.count + 1)
} else {
return lastPathComponent
}
}
}
return String(go)
}
/**
If the path represents an actual entry that is a symlink, returns the symlinks
absolute destination.
- Important: This is not exhaustive, the resulting path may still contain
symlink.
- 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 symlink destination does not exist, is **not** an error.
*/
public func readlink() throws -> Path {
do {
let rv = try FileManager.default.destinationOfSymbolicLink(atPath: string)
return Path(rv) ?? parent/rv
} catch CocoaError.fileReadUnknown {
// file is not symlink, return `self`
assert(exists)
return self
} catch {
#if os(Linux)
// ugh: Swift on Linux
let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain, nsError.code == CocoaError.fileReadUnknown.rawValue, exists {
return self
}
#endif
throw error
}
}
/// Recursively resolves symlinks in this path.
public func realpath() throws -> Path {
guard let rv = _realpath(string, nil) else { throw CocoaError.error(.fileNoSuchFile) }
defer { free(rv) }
guard let rvv = String(validatingUTF8: rv) else { throw CocoaError.error(.fileReadUnknownStringEncoding) }
// Removing 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).
// ^^ we do this to not conflict with the results that other Apple APIs give
// which is necessary if we are to have equality checks work reliably
let rvvv = (rvv as NSString).standardizingPath
return Path(string: rvvv)
} }
/// Returns the locale-aware sort order for the two paths. /// Returns the locale-aware sort order for the two paths.
/// :nodoc:
@inlinable @inlinable
public static func <(lhs: Path, rhs: Path) -> Bool { public static func <(lhs: Path, rhs: Path) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
} }
}
/// A file entry from a directory listing. @inline(__always)
public struct Entry { private func join_<S>(prefix: String, appending: S) -> String where S: StringProtocol {
/// The kind of this directory entry. return join_(prefix: prefix, pathComponents: appending.split(separator: "/"))
public enum Kind { }
/// The path is a file.
case file private func join_<S>(prefix: String, pathComponents: S) -> String where S: Sequence, S.Element: StringProtocol {
/// The path is a directory. assert(prefix.first == "/")
case directory
var rv = prefix
for component in pathComponents {
assert(!component.contains("/"))
switch component {
case "..":
let start = rv.indices.startIndex
let index = rv.lastIndex(of: "/")!
if start == index {
rv = "/"
} else {
rv = String(rv[start..<index])
}
case ".":
break
default:
if rv == "/" {
rv = "/\(component)"
} else {
rv = "\(rv)/\(component)"
}
} }
/// The kind of this entry.
public let kind: Kind
/// The path of this entry.
public let path: Path
} }
} return rv
/**
Joins a path and a string to produce a new path.
Path.root/"a" // => /a
Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path.
- SeeAlso: Path.join(_:)
*/
@inlinable
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
} }

View File

@@ -1,5 +1,5 @@
@testable import Path
import XCTest import XCTest
import Path
class PathTests: XCTestCase { class PathTests: XCTestCase {
func testConcatenation() { func testConcatenation() {
@@ -9,26 +9,36 @@ class PathTests: XCTestCase {
XCTAssertEqual((Path.root/"///bar").string, "/bar") XCTAssertEqual((Path.root/"///bar").string, "/bar")
XCTAssertEqual((Path.root/"foo///bar////").string, "/foo/bar") XCTAssertEqual((Path.root/"foo///bar////").string, "/foo/bar")
XCTAssertEqual((Path.root/"foo"/"/bar").string, "/foo/bar") XCTAssertEqual((Path.root/"foo"/"/bar").string, "/foo/bar")
XCTAssertEqual(Path.root.foo.bar.join(".."), Path.root.foo)
XCTAssertEqual(Path.root.foo.bar.join("."), Path.root.foo.bar)
XCTAssertEqual(Path.root.foo.bar.join("../baz"), Path.root.foo.baz)
} }
func testEnumeration() throws { func testEnumeration() throws {
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.a.mkdir().c.touch()
try tmpdir.join("b").touch() try tmpdir.join("b.swift").touch()
try tmpdir.join("c").touch() try tmpdir.c.touch()
try tmpdir.join(".d").mkdir().join("e").touch() try tmpdir.join(".d").mkdir().e.touch()
var paths = Set<String>() var paths = Set<String>()
let lsrv = try tmpdir.ls()
var dirs = 0 var dirs = 0
for entry in try tmpdir.ls() { for entry in lsrv {
if entry.kind == .directory { if entry.kind == .directory {
dirs += 1 dirs += 1
} }
paths.insert(entry.path.basename()) paths.insert(entry.path.basename())
} }
XCTAssertEqual(dirs, 2) XCTAssertEqual(dirs, 2)
XCTAssertEqual(paths, ["a", "b", "c", ".d"]) XCTAssertEqual(dirs, lsrv.directories.count)
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"], Set(lsrv.files(withExtension: "swift").map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["c"], Set(lsrv.files(withExtension: "").map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"])
} }
@@ -69,6 +79,22 @@ class PathTests: XCTestCase {
XCTAssert((Path.root/"bin").isDirectory) XCTAssert((Path.root/"bin").isDirectory)
} }
func testExtension() {
for prefix in [Path.root, Path.root.foo, Path.root.foo.bar] {
XCTAssertEqual(prefix.join("a.swift").extension, "swift")
XCTAssertEqual(prefix.join("a").extension, "")
XCTAssertEqual(prefix.join("a.").extension, "")
XCTAssertEqual(prefix.join("a..").extension, "")
XCTAssertEqual(prefix.join("a..swift").extension, "swift")
XCTAssertEqual(prefix.join("a..swift.").extension, "")
XCTAssertEqual(prefix.join("a.tar.gz").extension, "tar.gz")
XCTAssertEqual(prefix.join("a.tar.bz2").extension, "tar.bz2")
XCTAssertEqual(prefix.join("a.tar.xz").extension, "tar.xz")
XCTAssertEqual(prefix.join("a..tar.bz").extension, "tar.bz")
XCTAssertEqual(prefix.join("a..tar..xz").extension, "xz")
}
}
func testMktemp() throws { func testMktemp() throws {
var path: Path! var path: Path!
try Path.mktemp { try Path.mktemp {
@@ -83,34 +109,38 @@ class PathTests: XCTestCase {
try Path.mktemp { try Path.mktemp {
for _ in 0...1 { for _ in 0...1 {
try $0.join("a").mkdir() try $0.join("a").mkdir()
try $0.join("b/c").mkpath() try $0.join("b/c").mkdir(.p)
} }
} }
} }
func testBasename() { func testBasename() {
XCTAssertEqual(Path.root.join("foo.bar").basename(dropExtension: true), "foo") for prefix in [Path.root, Path.root.foo, Path.root.foo.bar] {
XCTAssertEqual(Path.root.join("foo").basename(dropExtension: true), "foo") XCTAssertEqual(prefix.join("foo.bar").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo.").basename(dropExtension: true), "foo.") XCTAssertEqual(prefix.join("foo").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo.bar.baz").basename(dropExtension: true), "foo.bar") XCTAssertEqual(prefix.join("foo.").basename(dropExtension: true), "foo.")
XCTAssertEqual(prefix.join("foo.bar.baz").basename(dropExtension: true), "foo.bar")
}
} }
func testCodable() throws { func testCodable() throws {
let input = [Path.root/"bar"] let input = [Path.root.foo, Path.root.foo.bar, Path.root]
XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input) XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input)
} }
func testRelativePathCodable() throws { func testRelativePathCodable() throws {
let root = Path.root/"bar" let root = Path.root.foo
let input = [ let input = [
root/"foo" Path.root,
root,
root.bar
] ]
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root encoder.userInfo[.relativePath] = root
let data = try encoder.encode(input) let data = try encoder.encode(input)
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["foo"]) XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"])
let decoder = JSONDecoder() let decoder = JSONDecoder()
XCTAssertThrowsError(try decoder.decode([Path].self, from: data)) XCTAssertThrowsError(try decoder.decode([Path].self, from: data))
@@ -130,7 +160,448 @@ class PathTests: XCTestCase {
XCTAssertEqual(prefix/b/c, Path("/Users/mxcl/b/c")) XCTAssertEqual(prefix/b/c, Path("/Users/mxcl/b/c"))
XCTAssertEqual(Path.root/"~b", Path("/~b")) XCTAssertEqual(Path.root/"~b", Path("/~b"))
XCTAssertEqual(Path.root/"~/b", Path("/~/b")) XCTAssertEqual(Path.root/"~/b", Path("/~/b"))
XCTAssertEqual(Path("~/foo"), Path.home/"foo") XCTAssertEqual(Path("~/foo"), Path.home/"foo")
XCTAssertEqual(Path("~"), Path.home)
XCTAssertEqual(Path("~/"), Path.home)
XCTAssertEqual(Path("~///"), Path.home)
XCTAssertEqual(Path("/~///"), Path.root/"~")
XCTAssertNil(Path("~foo")) XCTAssertNil(Path("~foo"))
XCTAssertNil(Path("~foo/bar"))
XCTAssertEqual(Path("~\(NSUserName())"), Path.home)
XCTAssertEqual(Path.root/"a/foo"/"../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"/../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"../../bar", Path.root/"bar")
XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar")
}
func testDynamicMember() {
XCTAssertEqual(Path.root.Documents, Path.root/"Documents")
let a = Path.home.foo
XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
// verify use of the dynamic-member-subscript works according to our rules
XCTAssertEqual(Path.home[dynamicMember: "../~foo"].string, "\(Path.home.parent.string)/~foo")
}
func testCopyTo() throws {
try Path.mktemp { root in
try root.foo.touch().copy(to: root.bar)
XCTAssert(root.foo.isFile)
XCTAssert(root.bar.isFile)
XCTAssertThrowsError(try root.foo.copy(to: root.bar))
try root.foo.copy(to: root.bar, overwrite: true)
}
// test copy errors if directory exists at destination, even with overwrite
try Path.mktemp { root in
try root.foo.touch()
XCTAssert(root.foo.isFile)
XCTAssertThrowsError(try root.foo.copy(to: root.bar.mkdir()))
XCTAssertThrowsError(try root.foo.copy(to: root.bar, overwrite: true))
}
}
func testCopyInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.copy(into: root2))
try bar1.copy(into: root2, overwrite: true)
XCTAssertTrue(bar1.exists)
XCTAssertTrue(bar2.exists)
}
// test creates intermediary directories
try bar1.copy(into: root1.create.directories)
// test doesnt replace file if copy into a file
let d = try root1.fuz.touch()
XCTAssertThrowsError(try root1.baz.touch().copy(into: d))
XCTAssert(d.isFile)
XCTAssert(root1.baz.isFile)
}
}
func testMoveTo() throws {
try Path.mktemp { tmpdir in
try tmpdir.foo.touch().move(to: tmpdir.bar)
XCTAssertFalse(tmpdir.foo.exists)
XCTAssert(tmpdir.bar.isFile)
XCTAssertThrowsError(try tmpdir.foo.touch().move(to: tmpdir.bar))
try tmpdir.foo.move(to: tmpdir.bar, overwrite: true)
}
// test move errors if directory exists at destination, even with overwrite
try Path.mktemp { root in
try root.foo.touch()
XCTAssert(root.foo.isFile)
XCTAssertThrowsError(try root.foo.move(to: root.bar.mkdir()))
XCTAssertThrowsError(try root.foo.move(to: root.bar, overwrite: true))
}
}
func testMoveInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.move(into: root2))
try bar1.move(into: root2, overwrite: true)
XCTAssertFalse(bar1.exists)
XCTAssertTrue(bar2.exists)
}
// test creates intermediary directories
try root1.baz.touch().move(into: root1.create.directories)
XCTAssertFalse(root1.baz.exists)
XCTAssert(root1.create.directories.baz.isFile)
// test doesnt replace file if move into a file
let d = try root1.fuz.touch()
XCTAssertThrowsError(try root1.baz.touch().move(into: d))
XCTAssert(d.isFile)
XCTAssert(root1.baz.isFile)
}
}
func testRename() throws {
try Path.mktemp { root in
do {
let file = try root.bar.touch()
let foo = try file.rename(to: "foo")
XCTAssertFalse(file.exists)
XCTAssertTrue(foo.isFile)
}
do {
let file = try root.bar.touch()
XCTAssertThrowsError(try file.rename(to: "foo"))
}
}
}
func testCommonDirectories() {
XCTAssertEqual(Path.root.string, "/")
XCTAssertEqual(Path.home.string, NSHomeDirectory())
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents")
#if !os(Linux)
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
XCTAssertEqual(Path.applicationSupport.string, NSHomeDirectory() + "/Library/Application Support")
_ = defaultUrl(for: .documentDirectory)
_ = defaultUrl(for: .cachesDirectory)
_ = defaultUrl(for: .applicationSupportDirectory)
#endif
}
func testStringConvertibles() {
XCTAssertEqual(Path.root.description, "/")
XCTAssertEqual(Path.root.debugDescription, "Path(/)")
}
func testFilesystemAttributes() throws {
XCTAssert(Path(#file)!.isFile)
XCTAssert(Path(#file)!.isReadable)
XCTAssert(Path(#file)!.isWritable)
XCTAssert(Path(#file)!.isDeletable)
XCTAssert(Path(#file)!.parent.isDirectory)
try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.isDirectory)
XCTAssertFalse(tmpdir.isFile)
let bar = try tmpdir.bar.touch().chmod(0o000)
XCTAssertFalse(bar.isReadable)
XCTAssertFalse(bar.isWritable)
XCTAssertFalse(bar.isDirectory)
XCTAssertFalse(bar.isExecutable)
XCTAssertTrue(bar.isFile)
XCTAssertTrue(bar.isDeletable) // can delete even if no read permissions
try bar.chmod(0o777)
XCTAssertTrue(bar.isReadable)
XCTAssertTrue(bar.isWritable)
XCTAssertTrue(bar.isDeletable)
XCTAssertTrue(bar.isExecutable)
try bar.delete()
XCTAssertFalse(bar.exists)
XCTAssertFalse(bar.isReadable)
XCTAssertFalse(bar.isWritable)
XCTAssertFalse(bar.isExecutable)
XCTAssertFalse(bar.isDeletable)
let nonExistantFile = tmpdir.baz
XCTAssertFalse(nonExistantFile.exists)
XCTAssertFalse(nonExistantFile.isExecutable)
XCTAssertFalse(nonExistantFile.isReadable)
XCTAssertFalse(nonExistantFile.isWritable)
XCTAssertFalse(nonExistantFile.isDeletable)
XCTAssertFalse(nonExistantFile.isDirectory)
XCTAssertFalse(nonExistantFile.isFile)
let baz = try tmpdir.baz.touch()
XCTAssertTrue(baz.isDeletable)
try tmpdir.chmod(0o500) // remove write permission on directory
XCTAssertFalse(baz.isDeletable) // this is how deletion is prevented on UNIX
}
}
func testTimes() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let now1 = Date().timeIntervalSince1970.rounded(.down)
#if !os(Linux)
XCTAssertEqual(foo.ctime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
#endif
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
sleep(1)
try foo.touch()
let now2 = Date().timeIntervalSince1970.rounded(.down)
XCTAssertNotEqual(now1, now2)
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now2) //FIXME flakey
XCTAssertNil(tmpdir.void.mtime)
XCTAssertNil(tmpdir.void.ctime)
}
}
func testDelete() throws {
try Path.mktemp { tmpdir in
try tmpdir.bar1.delete()
try tmpdir.bar2.touch().delete()
try tmpdir.bar3.touch().chmod(0o000).delete()
#if !os(Linux)
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
#endif
}
}
func testRelativeCodable() throws {
let path = Path.home.foo
let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home
let data = try encoder.encode([path])
let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home
XCTAssertEqual(try decoder.decode([Path].self, from: data), [path])
decoder.userInfo[.relativePath] = Path.documents
XCTAssertEqual(try decoder.decode([Path].self, from: data), [Path.documents.foo])
XCTAssertThrowsError(try JSONDecoder().decode([Path].self, from: data))
}
func testBundleExtensions() throws {
try Path.mktemp { tmpdir in
let bndl = Bundle(path: tmpdir.string)!
XCTAssertEqual(bndl.path, tmpdir)
XCTAssertEqual(bndl.sharedFrameworks, tmpdir.SharedFrameworks)
XCTAssertEqual(bndl.privateFrameworks, tmpdir.Frameworks)
XCTAssertEqual(bndl.resources, tmpdir)
XCTAssertNil(bndl.path(forResource: "foo", ofType: "bar"))
XCTAssertNil(bndl.executable)
#if os(macOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.Contents.Resources)
#elseif os(tvOS) || os(iOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir)
#else
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.lib)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.share)
#endif
}
}
func testDataExtensions() throws {
let data = try Data(contentsOf: Path(#file)!)
try Path.mktemp { tmpdir in
_ = try data.write(to: tmpdir.foo)
_ = try data.write(to: tmpdir.foo, atomically: true)
}
}
func testStringExtensions() throws {
let string = try String(contentsOf: Path(#file)!)
try Path.mktemp { tmpdir in
_ = try string.write(to: tmpdir.foo)
}
}
func testFileHandleExtensions() throws {
_ = try FileHandle(forReadingAt: Path(#file)!)
_ = try FileHandle(forWritingAt: Path(#file)!)
_ = try FileHandle(forUpdatingAt: Path(#file)!)
}
func testSort() {
XCTAssertEqual([Path.root.a, Path.root.c, Path.root.b].sorted(), [Path.root.a, Path.root.b, Path.root.c])
}
func testLock() throws {
#if !os(Linux)
try Path.mktemp { tmpdir in
let bar = try tmpdir.bar.touch()
try bar.lock()
XCTAssertThrowsError(try bar.touch())
try bar.unlock()
try bar.touch()
// a non existant file is already unlocked
try tmpdir.nonExit.unlock()
}
#endif
}
func testTouchThrowsIfCannotWrite() throws {
try Path.mktemp { tmpdir in
try tmpdir.chmod(0o000)
XCTAssertThrowsError(try tmpdir.bar.touch())
}
}
func testSymlinkFunctions() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssert(bar.isSymlink)
XCTAssertEqual(try bar.readlink(), foo)
}
try Path.mktemp { tmpdir in
let foo1 = try tmpdir.foo.touch()
let foo2 = try foo1.symlink(into: tmpdir.bar)
XCTAssert(foo2.isSymlink)
XCTAssert(tmpdir.bar.isDirectory)
XCTAssertEqual(try foo2.readlink(), foo1)
// cannot symlink into when `into` is an existing entry that is not a directory
let baz = try tmpdir.baz.touch()
XCTAssertThrowsError(try foo1.symlink(into: baz))
}
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try tmpdir.bar.mkdir()
XCTAssertThrowsError(try foo.symlink(as: bar))
XCTAssert(try foo.symlink(as: bar.foo).isSymlink)
}
}
func testReadlinkOnRelativeSymlink() throws {
//TODO how to test on iOS etc.?
#if os(macOS) || os(Linux)
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.mkdir()
let bar = try tmpdir.bar.touch()
let task = Process()
task.currentDirectoryPath = foo.string
task.launchPath = "/bin/ln"
task.arguments = ["-s", "../bar", "baz"]
task.launch()
task.waitUntilExit()
XCTAssertEqual(task.terminationStatus, 0)
XCTAssert(tmpdir.foo.baz.isSymlink)
XCTAssertEqual(try FileManager.default.destinationOfSymbolicLink(atPath: tmpdir.foo.baz.string), "../bar")
XCTAssertEqual(try tmpdir.foo.baz.readlink(), bar)
}
#endif
}
func testReadlinkOnFileReturnsSelf() throws {
try Path.mktemp { tmpdir in
XCTAssertEqual(try tmpdir.foo.touch(), tmpdir.foo)
XCTAssertEqual(try tmpdir.foo.readlink(), tmpdir.foo)
}
}
func testReadlinkOnNonExistantFileThrows() throws {
try Path.mktemp { tmpdir in
XCTAssertThrowsError(try tmpdir.bar.readlink())
}
}
func testReadlinkWhereLinkDestinationDoesNotExist() throws {
try Path.mktemp { tmpdir in
let bar = try tmpdir.foo.symlink(as: tmpdir.bar)
XCTAssertEqual(try bar.readlink(), tmpdir.foo)
}
}
func testNoUndesiredSymlinkResolution() throws {
// this test because NSString.standardizingPath will resolve symlinks
// if the path you give it contains .. and the result is an actual entry
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.mkdir()
try foo.bar.mkdir().fuz.touch()
let baz = try foo.symlink(as: tmpdir.baz)
XCTAssert(baz.isSymlink)
XCTAssert(baz.bar.isDirectory)
XCTAssertEqual(baz.bar.join("..").string, "\(tmpdir)/baz")
XCTAssertEqual(Path("\(tmpdir)/baz/bar/..")?.string, "\(tmpdir)/baz")
}
}
func testRealpath() throws {
try Path.mktemp { tmpdir in
let b = try tmpdir.a.b.mkdir(.p)
let c = try tmpdir.a.c.touch()
let e = try c.symlink(as: b.e)
let f = try e.symlink(as: tmpdir.f)
XCTAssertEqual(try f.readlink(), e)
XCTAssertEqual(try f.realpath(), c)
}
try Path.mktemp { tmpdir in
XCTAssertThrowsError(try tmpdir.foo.realpath())
}
}
func testFileReference() throws {
let ref = Path.home.fileReferenceURL
#if !os(Linux)
XCTAssertTrue(ref?.isFileReferenceURL() ?? false)
#endif
XCTAssertEqual(ref?.path, Path.home.string)
}
func testURLInitializer() throws {
XCTAssertEqual(Path(url: Path.home.url), Path.home)
XCTAssertEqual(Path.home.fileReferenceURL.flatMap(Path.init), Path.home)
XCTAssertNil(Path(url: URL(string: "https://foo.com")!))
XCTAssertNil(Path(url: NSURL(string: "https://foo.com")!))
}
func testInitializerForRelativePath() throws {
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)
} }
} }

View File

@@ -42,7 +42,11 @@ class TemporaryDirectory {
} }
deinit { deinit {
_ = try? FileManager.default.removeItem(at: url) do {
try path.chmod(0o777).delete()
} catch {
//TODO log
}
} }
} }

View File

@@ -1,26 +1,61 @@
#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),
("testCodable", testCodable), ("testCodable", testCodable),
("testCommonDirectories", testCommonDirectories),
("testConcatenation", testConcatenation), ("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto),
("testCopyTo", testCopyTo),
("testDataExtensions", testDataExtensions),
("testDelete", testDelete),
("testDynamicMember", testDynamicMember),
("testEnumeration", testEnumeration), ("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles), ("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists), ("testExists", testExists),
("testExtension", testExtension),
("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes),
("testFlatMap", testFlatMap),
("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testLock", testLock),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo),
("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution),
("testPathComponents", testPathComponents),
("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf),
("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows),
("testReadlinkOnRelativeSymlink", testReadlinkOnRelativeSymlink),
("testReadlinkWhereLinkDestinationDoesNotExist", testReadlinkWhereLinkDestinationDoesNotExist),
("testRealpath", testRealpath),
("testRelativeCodable", testRelativeCodable),
("testRelativePathCodable", testRelativePathCodable), ("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo), ("testRelativeTo", testRelativeTo),
("testRename", testRename),
("testSort", testSort),
("testStringConvertibles", testStringConvertibles),
("testStringExtensions", testStringExtensions),
("testSymlinkFunctions", testSymlinkFunctions),
("testTimes", testTimes),
("testTouchThrowsIfCannotWrite", testTouchThrowsIfCannotWrite),
("testURLInitializer", testURLInitializer),
] ]
} }
#if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] { public func __allTests() -> [XCTestCaseEntry] {
return [ return [
testCase(PathTests.__allTests), testCase(PathTests.__allTests__PathTests),
] ]
} }
#endif #endif