Compare commits

...

48 Commits

Author SHA1 Message Date
Dash2507
dac007e907 Change "pkg" to "package" (#50) 2019-06-10 17:20:05 -04:00
Max Howell
b6b4a74a26 Move deploy script to @mxcl/ops 2019-04-14 12:40:07 -04:00
Max Howell
b76db41ca4 422 means the release already exists, so… succeed 2019-04-14 10:49:17 -04:00
Max Howell
8d5d67b81b Merge pull request #48 from mxcl/travis-swift-5.0-GM
[travis] Swift 5 GM
2019-03-25 22:02:33 -04:00
Max Howell
21ddc7dc3a [travis] Swift 5 GM 2019-03-25 21:40:17 -04:00
Max Howell
f324b4a562 Merge pull request #47 from mxcl/fix-symlink-delete
Adds `kind` fixes deleting broken symlinks
2019-03-18 19:56:39 -04:00
Max Howell
0e061f9cc8 Adds kind fixes deleting broken symlinks
`delete()` and other functions would check `exists` to do certain behaviors, but `exists` will validate a symlink if the entry is a symlink, thus instead we check if the path is an actual entry now instead.
2019-03-18 19:47:49 -04:00
Max Howell
7e774b6cf5 Use 10.2 image for pretest
Also some tidy
2019-03-06 15:55:51 -05:00
Max Howell
8b371fa5d2 Allow initialization from Substring etc. 2019-03-06 15:55:51 -05:00
Max Howell
02fd579f19 mxcl.github.io -> mxcl.dev
[ci skip]
2019-02-28 18:40:19 -05:00
repo-ranger[bot]
e915bc0cfb Merge pull request #43 from mxcl/Bundle.executable
Add Bundle.executable
2019-02-17 15:14:17 +00:00
Max Howell
f4c2c75aa1 Add Bundle.executable 2019-02-17 10:05:30 -05:00
repo-ranger[bot]
dc7affa28c Merge pull request #42 from mxcl/path-components
Add Path.components
2019-02-15 18:49:13 +00:00
Max Howell
476cdc1461 Add Path.components 2019-02-15 13:38:36 -05:00
Max Howell
a644208c62 Test Swift 4.0.3 also 2019-02-13 21:34:35 -05:00
Max Howell
d7a9819350 Fix publishing the release in the deploy stage
[ci skip]
2019-02-13 20:52:39 -05:00
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
14 changed files with 807 additions and 217 deletions

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

View File

@@ -12,20 +12,29 @@ language: swift
osx_image: xcode10.1
xcode_project: Path.swift.xcodeproj
xcode_scheme: Path.swift-Package
jobs:
include:
- script: swift test --parallel
name: macOS / Swift 4.2.1
- name: macOS / Swift 4.0.3
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
before_install: swift package generate-xcodeproj --enable-code-coverage
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS / Swift 4.2.1
after_success: bash <(curl -s https://codecov.io/bash)
- <<: *xcodebuild
xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV
name: tvOS / Swift 4.2.1
- <<: *xcodebuild
name: watchOS / Swift 4.2.1
script: |
@@ -46,41 +55,25 @@ jobs:
sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
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)
env: SWIFT_VERSION='5.0'
name: Linux / Swift 5.0.0
- stage: pretest
name: Check Linux tests are syncd
install: swift test --generate-linuxmain
script: git diff --exit-code
osx_image: xcode10.2
- stage: deploy
name: Jazzy
before_install: |
cat <<\ \ EOF> .jazzy.yaml
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
exclude:
- Sources/Path+StringConvertibles.swift
EOF
sed -i '' "s/TRAVIS_TAG/$TRAVIS_TAG/" .jazzy.yaml
# ^^ this weirdness because Travis multiline YAML is broken and inserts
# two spaces in front of the output which means we need a prefixed
# delimiter which also weirdly stops bash from doing variable substitution
install: gem install jazzy
before_script: swift package generate-xcodeproj
script: jazzy
script: |
jazzy --config .github/jazzy.yml \
--module-version $TRAVIS_TAG \
--github_url "https://github.com/$TRAVIS_REPO_SLUG"
deploy:
provider: pages
skip-cleanup: true
@@ -90,41 +83,11 @@ jobs:
tags: true
- name: CocoaPods
before_install: |
export DESCRIPTION=$(swift - <<\ \ EOF
import Foundation
struct Response: Decodable { let description: String }
let token = ProcessInfo.processInfo.environment["GITHUB_TOKEN"]!
let url = URL(string: "https://api.github.com/repos/mxcl/Path.swift")!
var rq = URLRequest(url: url)
rq.setValue("token \(token)", forHTTPHeaderField: "Authorization")
let semaphore = DispatchSemaphore(value: 0)
var data: Data!
URLSession.shared.dataTask(with: rq) { d, _, _ in
data = d
semaphore.signal()
}.resume()
semaphore.wait()
let rsp = try JSONDecoder().decode(Response.self, from: data)
print(rsp.description, terminator: "")
EOF)
cat <<\ \ EOF> Path.swift.podspec
Pod::Spec.new do |s|
s.name = 'Path.swift'
s.version = ENV['TRAVIS_TAG']
s.summary = ENV['DESCRIPTION']
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
osx_image: xcode10.2
install: |
brew install mxcl/made/swift-sh
curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy
chmod u+x deploy
before_script: ./deploy generate-podspec
script: pod trunk push
after_success: ./deploy publish-release

View File

@@ -9,5 +9,6 @@ let package = Package(
targets: [
.target(name: "Path", path: "Sources"),
.testTarget(name: "PathTests", dependencies: ["Path"]),
]
],
swiftLanguageVersions: [.v4, .v4_2, .version("5")]
)

View File

@@ -1,20 +0,0 @@
// swift-tools-version:5.0
import PackageDescription
let pkg = Package(
name: "Path.swift",
products: [
.library(name: "Path", targets: ["Path"]),
],
targets: [
.target(name: "Path", path: "Sources"),
.testTarget(name: "PathTests", dependencies: ["Path"]),
]
)
pkg.platforms = [
.macOS(.v10_10), .iOS(.v8), .tvOS(.v10), .watchOS(.v3)
]
pkg.swiftLanguageVersions = [
.v4_2, .v5
]

View File

@@ -1,4 +1,4 @@
# Path.swift ![badge-platforms][] ![badge-languages][] [![badge-ci][]][travis] [![badge-jazzy][]][docs] [![badge-codecov][]][codecov] ![badge-version][]
# 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 end
results.
@@ -36,7 +36,7 @@ print(foo.isFile) // => true
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
// a practical example: installing a helper executable
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)
```
We emphasize safety and correctness, just like Swift, and also (again like
@@ -53,11 +53,12 @@ help me continue my work, I appreciate it x
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
[Other donation/tipping options](http://mxcl.github.io/donate/)
[Other donation/tipping options](http://mxcl.dev/#donate)
# Handbook
Our [online API documentation][docs] 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
@@ -107,7 +108,7 @@ 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
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*.
@@ -170,13 +171,18 @@ let swiftFiles = Path.home.ls().files(withExtension: "swift")
# `Path.swift` is robust
Some parts of `FileManager` are not exactly idiomatic. For example
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
@@ -198,6 +204,10 @@ Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash
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:
let b = "b"
let c = "c"
@@ -210,8 +220,24 @@ Path.root/"~/b" // => /~/b
// but is here
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
// 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,
@@ -219,40 +245,88 @@ 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`), we *do* the same for
`Path.init`, but *do not* if you are joining a path that ends up being one of
these paths, (eg. `Path.root.join("var/private')`).
If a `Path` is a symlink but the destination of the link does not exist `exists`
returns `false`. This seems to be the correct thing to do since symlinks are
meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `kind` is `nil`.
## We do not provide change directory functionality
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
SwiftPM:
```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.5.0"))
package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.13.0"))
```
CocoaPods:
```ruby
pod 'Path.swift', '~> 0.5'
pod 'Path.swift', '~> 0.13'
```
Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Please note
## Pre1.0 status
We are pre 1.0, thus we can change the API as we like, and we will (to the
pursuit of getting it *right*)! We will tag 1.0 as soon as possible.
### Get push notifications for new releases
https://codebasesaga.com/canopy/
https://mxcl.dev/canopy/
# Alternatives
* [Pathos](https://github.com/dduan/Pathos) by Daniel Duan
* [PathKit](https://github.com/kylef/PathKit) by Kyle Fuller
* [Files](https://github.com/JohnSundell/Files) by John Sundell
* [Utility](https://github.com/apple/swift-package-manager) by Apple
@@ -260,10 +334,11 @@ https://codebasesaga.com/canopy/
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg
[badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0-orange.svg
[docs]: https://mxcl.github.io/Path.swift/Structs/Path.html
[docs]: https://mxcl.dev/Path.swift/Structs/Path.html
[badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true
[badge-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

@@ -9,38 +9,36 @@ public extension Bundle {
return str.flatMap(Path.init)
}
/// Returns the path for the shared-frameworks directory in this bundle.
/**
Returns the path for the shared-frameworks directory in this bundle.
- Note: This is typically `ShareFrameworks`
*/
var sharedFrameworks: Path {
var `default`: Path {
#if os(macOS)
return path.join("Contents/Frameworks")
#elseif os(Linux)
return path.join("lib")
#else
return path.join("Frameworks")
#endif
}
return sharedFrameworksPath.flatMap(Path.init) ?? `default`
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.
var resources: Path {
var `default`: Path {
#if os(macOS)
return path.join("Contents/Resources")
#elseif os(Linux)
return path.join("share")
#else
return path
#endif
}
return resourcePath.flatMap(Path.init) ?? `default`
return resourcePath.flatMap(Path.init) ?? defaultResourcesPath
}
/// Returns the path for this bundle.
var path: Path {
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`
@@ -107,3 +105,25 @@ public extension FileHandle {
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,7 +1,4 @@
import Foundation
//#if os(Linux)
//import func Glibc.chmod
//#endif
public extension Path {
//MARK: Filesystem Attributes
@@ -40,15 +37,12 @@ public extension Path {
*/
@discardableResult
func chmod(_ octal: Int) throws -> Path {
// #if os(Linux)
// Glibc.chmod(string, __mode_t(octal))
// #else
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
// #endif
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.
@@ -70,6 +64,7 @@ public extension Path {
- Note: If file isnt locked, does nothing.
- Note: If file doesnt exist, does nothing.
- Important: On Linux does nothing.
- SeeAlso: `lock()`
*/
@discardableResult
func unlock() throws -> Path {
@@ -88,4 +83,22 @@ public extension Path {
#endif
return self
}
enum Kind {
case file, symlink, directory
}
var kind: Kind? {
var buf = stat()
guard lstat(string, &buf) == 0 else {
return nil
}
if buf.st_mode & S_IFMT == S_IFLNK {
return .symlink
} else if buf.st_mode & S_IFMT == S_IFDIR {
return .directory
} else {
return .file
}
}
}

View File

@@ -47,18 +47,7 @@ extension Path {
fatalError()
}
#else
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else {
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()
}
}
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { return defaultUrl(for: searchPath) }
return Path(string: pathString)
#endif
}
@@ -90,3 +79,19 @@ extension Path {
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

@@ -25,11 +25,11 @@ public extension Path {
*/
@discardableResult
func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile, isFile {
if overwrite, let tokind = to.kind, tokind != .directory, kind != .directory {
try FileManager.default.removeItem(at: to.url)
}
#if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, to.isFile {
if !overwrite, to.kind != nil {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
@@ -46,7 +46,8 @@ public extension Path {
// 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 first.
If the destination does not exist, this function creates the directory
(including intermediary directories if necessary) first.
- Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`.
@@ -56,19 +57,19 @@ public extension Path {
`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(into:overwrite:)`
- SeeAlso: `copy(to:overwrite:)`
*/
@discardableResult
func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
try FileManager.default.createDirectory(at: into.url, withIntermediateDirectories: true)
if into.kind == nil {
try into.mkdir(.p)
}
let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
if overwrite, let kind = rv.kind, kind != .directory {
try FileManager.default.removeItem(at: rv.url)
}
#if os(Linux) && !swift(>=5.1) // check if fixed
if !overwrite, rv.isFile {
if !overwrite, rv.kind != nil {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
@@ -90,11 +91,11 @@ public extension Path {
`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:)
- SeeAlso: `move(into:overwrite:)`
*/
@discardableResult
func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile {
if overwrite, let kind = to.kind, kind != .directory {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.moveItem(at: url, to: to.url)
@@ -107,27 +108,32 @@ public extension Path {
try Path.root.join("bar").move(into: .home)
// => "/Users/mxcl/bar"
If the destination does not exist, this function creates the directory first.
If the destination does not exist, this function creates the directory
(including intermediary directories if necessary) first.
- Parameter into: Destination directory
- 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.
- SeeAlso: move(into:overwrite:)
- SeeAlso: `move(to:overwrite:)`
*/
@discardableResult
func move(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
switch into.kind {
case nil:
try into.mkdir(.p)
} else if !into.isDirectory {
fallthrough
case .directory?:
let rv = into/basename()
if overwrite, let rvkind = rv.kind, rvkind != .directory {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
case .file?, .symlink?:
throw CocoaError.error(.fileWriteFileExists)
}
let rv = into/basename()
if overwrite, rv.isFile {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
}
/**
@@ -136,11 +142,12 @@ public extension Path {
*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
- Note: If entry is a symlink, deletes the symlink.
- SeeAlso: `lock()`
*/
@inlinable
func delete() throws {
if exists {
if kind != nil {
try FileManager.default.removeItem(at: url)
}
}
@@ -152,7 +159,7 @@ public extension Path {
@inlinable
@discardableResult
func touch() throws -> Path {
if !exists {
if kind == nil {
guard FileManager.default.createFile(atPath: string, contents: nil) else {
throw CocoaError.error(.fileWriteUnknown)
}
@@ -209,6 +216,35 @@ public extension Path {
try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
return newpath
}
/**
Creates a symlink of this file at `as`.
- Note: If `self` does not exist, is **not** an error.
*/
@discardableResult
func symlink(as: Path) throws -> Path {
try FileManager.default.createSymbolicLink(atPath: `as`.string, withDestinationPath: string)
return `as`
}
/**
Creates a symlink of this file with the same filename in the `into` directory.
- Note: If into does not exist, creates the directory with intermediate directories if necessary.
*/
@discardableResult
func symlink(into dir: Path) throws -> Path {
switch dir.kind {
case nil, .symlink?:
try dir.mkdir(.p)
fallthrough
case .directory?:
let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst
case .file?:
throw CocoaError.error(.fileWriteFileExists)
}
}
}
/// Options for `Path.mkdir(_:)`

View File

@@ -1,5 +1,3 @@
import class Foundation.NSString
extension Path: CustomStringConvertible {
/// Returns `Path.string`
public var description: String {

View File

@@ -2,13 +2,16 @@ import Foundation
#if os(Linux)
import func Glibc.access
#else
import func Darwin.access
import Darwin
#endif
public extension Path {
//MARK: Filesystem Properties
/// Returns true if the path represents an actual filesystem entry.
/**
- Returns: `true` if the path represents an actual filesystem entry.
- Note: If `self` is a symlink the return value represents the destination.
*/
var exists: Bool {
return FileManager.default.fileExists(atPath: string)
}
@@ -55,4 +58,11 @@ public extension Path {
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,4 +1,11 @@
import Foundation
#if !os(Linux)
import func Darwin.realpath
let _realpath = Darwin.realpath
#else
import func Glibc.realpath
let _realpath = Glibc.realpath
#endif
/**
A `Path` represents an absolute path on a filesystem.
@@ -32,19 +39,96 @@ import Foundation
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
}
/// 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)
/**
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 pathComponent: String) -> Path {
let str = (string as NSString).appendingPathComponent(pathComponent)
return Path(string: str)
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
@@ -57,6 +141,21 @@ public struct Path: Equatable, Hashable, Comparable {
return URL(fileURLWithPath: string)
}
/**
Returns a file-reference URL.
- 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.
*/
public var fileReferenceURL: NSURL? {
#if !os(Linux)
// https://bugs.swift.org/browse/SR-2728
return (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL
#else
return NSURL(fileURLWithPath: string)
#endif
}
/**
Returns the parent directory for this path.
@@ -66,23 +165,49 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
*/
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 the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
- Remark: If there is no extension returns "".
- Remark: If the filename ends with any number of ".", returns "".
- Note: We special case eg. `foo.tar.gz`.
*/
@inlinable
public var `extension`: String {
if string.hasSuffix(".tar.gz") {
//FIXME efficiency
switch true {
case string.hasSuffix(".tar.gz"):
return "tar.gz"
} else {
return (string as NSString).pathExtension
case string.hasSuffix(".tar.bz"):
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 {
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
/**
@@ -93,14 +218,15 @@ public struct Path: Equatable, Hashable, Comparable {
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>(_ pathComponent: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion)
let str = (string as NSString).appendingPathComponent(String(pathComponent))
return Path(string: (str as NSString).standardizingPath)
public func join<S>(_ addendum: S) -> Path where S: StringProtocol {
return Path(string: join_(prefix: string, appending: addendum))
}
/**
@@ -111,6 +237,9 @@ public struct Path: Equatable, Hashable, Comparable {
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.
@@ -168,17 +297,71 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter dropExtension: If `true` returns the basename without its file extension.
*/
public func basename(dropExtension: Bool = false) -> String {
let str = string as NSString
if !dropExtension {
return str.lastPathComponent
} else {
let ext = str.pathExtension
if !ext.isEmpty {
return String(str.lastPathComponent.dropLast(ext.count + 1))
var lastPathComponent: Substring {
let slash = string.lastIndex(of: "/")!
let index = string.index(after: slash)
return string[index...]
}
var go: Substring {
if !dropExtension {
return lastPathComponent
} else {
return str.lastPathComponent
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.
@@ -188,3 +371,38 @@ public struct Path: Equatable, Hashable, Comparable {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
}
}
@inline(__always)
private func join_<S>(prefix: String, appending: S) -> String where S: StringProtocol {
return join_(prefix: prefix, pathComponents: appending.split(separator: "/"))
}
private func join_<S>(prefix: String, pathComponents: S) -> String where S: Sequence, S.Element: StringProtocol {
assert(prefix.first == "/")
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)"
}
}
}
return rv
}

View File

@@ -1,5 +1,5 @@
@testable import Path
import XCTest
import Path
class PathTests: XCTestCase {
func testConcatenation() {
@@ -9,6 +9,10 @@ class PathTests: XCTestCase {
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.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 {
@@ -65,9 +69,18 @@ class PathTests: XCTestCase {
XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar")
}
func testExists() {
func testExists() throws {
XCTAssert(Path.root.exists)
XCTAssert((Path.root/"bin").exists)
try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.exists)
XCTAssertFalse(try tmpdir.bar.symlink(as: tmpdir.foo).exists)
XCTAssertTrue(tmpdir.foo.kind == .symlink)
XCTAssertTrue(try tmpdir.bar.touch().symlink(as: tmpdir.baz).exists)
XCTAssertTrue(tmpdir.bar.kind == .file)
XCTAssertTrue(tmpdir.kind == .directory)
}
}
func testIsDirectory() {
@@ -76,15 +89,19 @@ class PathTests: XCTestCase {
}
func testExtension() {
XCTAssertEqual(Path.root.join("a.swift").extension, "swift")
XCTAssertEqual(Path.root.join("a").extension, "")
XCTAssertEqual(Path.root.join("a.").extension, "")
XCTAssertEqual(Path.root.join("a..").extension, "")
XCTAssertEqual(Path.root.join("a..swift").extension, "swift")
XCTAssertEqual(Path.root.join("a..swift.").extension, "")
XCTAssertEqual(Path.root.join("a.tar.gz").extension, "tar.gz")
XCTAssertEqual(Path.root.join("a..tar.gz").extension, "tar.gz")
XCTAssertEqual(Path.root.join("a..tar..gz").extension, "gz")
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 {
@@ -107,28 +124,32 @@ class PathTests: XCTestCase {
}
func testBasename() {
XCTAssertEqual(Path.root.join("foo.bar").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo.").basename(dropExtension: true), "foo.")
XCTAssertEqual(Path.root.join("foo.bar.baz").basename(dropExtension: true), "foo.bar")
for prefix in [Path.root, Path.root.foo, Path.root.foo.bar] {
XCTAssertEqual(prefix.join("foo.bar").basename(dropExtension: true), "foo")
XCTAssertEqual(prefix.join("foo").basename(dropExtension: true), "foo")
XCTAssertEqual(prefix.join("foo.").basename(dropExtension: true), "foo.")
XCTAssertEqual(prefix.join("foo.bar.baz").basename(dropExtension: true), "foo.bar")
}
}
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)
}
func testRelativePathCodable() throws {
let root = Path.root/"bar"
let root = Path.root.foo
let input = [
root/"foo"
Path.root,
root,
root.bar
]
let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root
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()
XCTAssertThrowsError(try decoder.decode([Path].self, from: data))
@@ -148,8 +169,16 @@ class PathTests: XCTestCase {
XCTAssertEqual(prefix/b/c, Path("/Users/mxcl/b/c"))
XCTAssertEqual(Path.root/"~b", Path("/~b"))
XCTAssertEqual(Path.root/"~/b", Path("/~/b"))
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/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")
@@ -162,6 +191,9 @@ class PathTests: XCTestCase {
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 {
@@ -172,6 +204,14 @@ class PathTests: XCTestCase {
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 {
@@ -204,6 +244,14 @@ class PathTests: XCTestCase {
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 {
@@ -253,6 +301,10 @@ class PathTests: XCTestCase {
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
}
@@ -336,6 +388,21 @@ class PathTests: XCTestCase {
#if !os(Linux)
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
#endif
// regression test: can delete a symlink that points to a non-existent file
let bar5 = try tmpdir.bar4.symlink(as: tmpdir.bar5)
XCTAssertEqual(bar5.kind, .symlink)
XCTAssertFalse(bar5.exists)
XCTAssertNoThrow(try bar5.delete())
XCTAssertEqual(bar5.kind, nil)
// test that deleting a symlink *only* deletes the symlink
let bar7 = try tmpdir.bar6.touch().symlink(as: tmpdir.bar7)
XCTAssertEqual(bar7.kind, .symlink)
XCTAssertTrue(bar7.exists)
XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.kind, nil)
XCTAssertEqual(tmpdir.bar6.kind, .file)
}
}
@@ -352,13 +419,27 @@ class PathTests: XCTestCase {
XCTAssertThrowsError(try JSONDecoder().decode([Path].self, from: data))
}
func testBundleExtensions() {
XCTAssertTrue(Bundle.main.path.isDirectory)
XCTAssertTrue(Bundle.main.resources.isDirectory)
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)
// dont exist in tests
_ = Bundle.main.path(forResource: "foo", ofType: "bar")
_ = Bundle.main.sharedFrameworks
#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 {
@@ -394,7 +475,167 @@ class PathTests: XCTestCase {
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)
}
func testKind() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssertEqual(tmpdir.kind, .directory)
XCTAssertEqual(foo.kind, .file)
XCTAssertEqual(bar.kind, .symlink)
}
}
}

View File

@@ -1,7 +1,11 @@
#if !canImport(ObjectiveC)
import XCTest
extension PathTests {
static let __allTests = [
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__PathTests = [
("testBasename", testBasename),
("testBundleExtensions", testBundleExtensions),
("testCodable", testCodable),
@@ -17,14 +21,25 @@ extension PathTests {
("testExists", testExists),
("testExtension", testExtension),
("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes),
("testFlatMap", testFlatMap),
("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory),
("testJoin", testJoin),
("testKind", testKind),
("testLock", testLock),
("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo),
("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution),
("testPathComponents", testPathComponents),
("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf),
("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows),
("testReadlinkOnRelativeSymlink", testReadlinkOnRelativeSymlink),
("testReadlinkWhereLinkDestinationDoesNotExist", testReadlinkWhereLinkDestinationDoesNotExist),
("testRealpath", testRealpath),
("testRelativeCodable", testRelativeCodable),
("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo),
@@ -32,14 +47,16 @@ extension PathTests {
("testSort", testSort),
("testStringConvertibles", testStringConvertibles),
("testStringExtensions", testStringExtensions),
("testSymlinkFunctions", testSymlinkFunctions),
("testTimes", testTimes),
("testTouchThrowsIfCannotWrite", testTouchThrowsIfCannotWrite),
("testURLInitializer", testURLInitializer),
]
}
#if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(PathTests.__allTests),
testCase(PathTests.__allTests__PathTests),
]
}
#endif