Compare commits

...

57 Commits

Author SHA1 Message Date
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
Max Howell
aac81b85a4 Merge pull request #8 from mxcl/better-deploy
Better deploy
2019-01-20 18:53:11 -05:00
Max Howell
3644124a36 Fix testing on tvOS/iOS 2019-01-20 18:44:24 -05:00
Max Howell
ca4ac3ec8f Fix stage types in .travis.yml 2019-01-20 17:33:56 -05:00
Max Howell
920f007660 Fix Linux testEnumerationSkippingHiddenFiles() 2019-01-20 17:32:39 -05:00
Max Howell
751b855a26 Pretest that fails if Linux tests aren't current 2019-01-20 17:25:46 -05:00
Max Howell
c0e5023632 Better deployment of Jazzy and Pods 2019-01-20 17:13:03 -05:00
Max Howell
e0c62108e8 Update Linux tests 2019-01-20 16:30:02 -05:00
19 changed files with 898 additions and 317 deletions

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

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

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
/.build
/*.xcodeproj
/build
/docs

View File

@@ -2,6 +2,7 @@
if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/
stages:
- name: pretest
- name: test
- name: deploy
if: branch =~ ^\d+\.\d+\.\d+$
@@ -14,18 +15,19 @@ xcode_scheme: Path.swift-Package
jobs:
include:
- script: swift test
name: macOS
- script: swift test --parallel
name: macOS / Swift 4.2.1
- &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
name: iOS
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
name: tvOS / Swift 4.2.1
- <<: *xcodebuild
name: watchOS
name: watchOS / Swift 4.2.1
script: |
set -o pipefail
xcodebuild \
@@ -33,40 +35,52 @@ jobs:
-scheme Path.swift-Package \
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty
after_success: false
- env: SWIFT_VERSION=4.2.1
- &linux
env: SWIFT_VERSION=4.2.1
os: linux
name: Linux
name: Linux / Swift 4.2.1
language: generic
dist: trusty
sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
script: swift test
script: swift test --parallel
- <<: *xcodebuild
stage: deploy
- <<: *linux
env: SWIFT_VERSION='5.0-DEVELOPMENT-SNAPSHOT-2019-01-22-a'
name: Linux / Swift 5.0.0-dev (2019-01-22)
- stage: pretest
name: Check Linux tests are syncd
install: swift test --generate-linuxmain
script: git diff --exit-code
- stage: deploy
name: Jazzy
before_install: |
cat << EOF > .jazzy.yml
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
touch Contents.md
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
script: |
jazzy \
--no-hide-documentation-coverage \
--theme fullwidth \
--output output \
--readme Contents.md \
--root-url https://mxcl.github.io/Path.swift/ \
--github_url https://github.com/mxcl/Path.swift \
--module Path \
--module-version $TRAVIS_TAG
before_script: swift package generate-xcodeproj
script: jazzy
deploy:
provider: pages
skip-cleanup: true
@@ -75,7 +89,42 @@ jobs:
on:
tags: true
- stage: deploy
name: CocoaPods
- 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
script: pod trunk push --allow-warnings
script: pod trunk push

20
Package@swift-5.0.swift Normal file
View File

@@ -0,0 +1,20 @@
// 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,19 +0,0 @@
Pod::Spec.new do |s|
s.name = 'Path.swift'
s.version = '0.4.1'
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

120
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][]
A file-system pathing library focused on developer experience and robust
endresults.
A file-system pathing library focused on developer experience and robust end
results.
```swift
import Path
@@ -15,27 +15,39 @@ let docs = Path.home/"Documents"
// paths are *always* absolute thus avoiding common bugs
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)
// easy file-management
try Path.root.join("foo").copy(to: Path.root/"bar")
// sensible considerations
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
try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
// ^^ other libraries would make the above `to:` form handle both these cases
// but that can easily lead to bugs where you accidentally write files that
// were meant to be directory destinations
// easy file-management
let bar = try Path.root.join("foo").copy(to: Path.root/"bar")
print(bar) // => /bar
print(bar.isFile) // => true
// 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.join("helper").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
```
We emphasize safety and correctness, just like Swift, and also just
like Swift, we provide a thoughtful and comprehensive (yet concise) API.
We emphasize safety and correctness, just like Swift, and also (again like
Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl
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
can continue to make tools and software you need and love. I appreciate it x.
probably you already use some of it (Homebrew anyone?). I work full-time on
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">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
@@ -45,7 +57,7 @@ can continue to make tools and software you need and love. I appreciate it x.
# Handbook
Our [online API documentation] is automatically updated for new releases.
Our [online API documentation][docs] is automatically updated for new releases.
## Codable
@@ -86,6 +98,19 @@ decoder.userInfo[.relativePath] = Path.home
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
The `Path` initializer returns `nil` unless fed an absolute path; thus to
@@ -99,10 +124,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
expect to be relative.
Our initializer is nameless because we conform to `LosslessStringConvertible`,
the same conformance as that `Int`, `Float` etc. conform. The protocol enforces
a nameless initialization and since it is appropriate for us to conform to it,
we do.
Our initializer is nameless to be consistent with the equivalent operation for
converting strings to `Int`, `Float` etc. in the standard library.
## Extensions
@@ -116,8 +139,7 @@ bashProfile += "\n\nfoo"
try bashProfile.write(to: Path.home/".bash_profile")
try Bundle.main.resources!.join("foo").copy(to: .home)
// ^^ `-> Path?` because the underlying `Bundle` function is `-> String?`
try Bundle.main.resources.join("foo").copy(to: .home)
```
## Directory listings
@@ -139,13 +161,27 @@ 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")
```
# `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.
# `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
Paths are just string representations, there *might not* be a real file there.
@@ -178,22 +214,38 @@ Path("~/foo")! // => /Users/mxcl/foo
Path("~foo") // => nil
```
*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.
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.
# Installation
SwiftPM:
```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", from: "0.5.0"))
```
CocoaPods:
```ruby
pod 'Path.swift' ~> '0.4.1'
pod 'Path.swift', '~> 0.5'
```
Please note! We are pre 1.0, thus we can change the API as we like! We will tag
1.0 as soon as possible.
Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Please note
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
@@ -207,5 +259,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-orange.svg
[online API documentation]: https://mxcl.github.io/Path.swift/
[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
[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

View File

@@ -1,5 +1,6 @@
import Foundation
/// Extensions on Foundations `Bundle` so you get `Path` rather than `String` or `URL`.
public extension Bundle {
/// Returns the path for requested resource in this bundle.
func path(forResource: String, ofType: String?) -> Path? {
@@ -9,21 +10,40 @@ public extension Bundle {
}
/// Returns the path for the shared-frameworks directory in this bundle.
public var sharedFrameworks: Path? {
return sharedFrameworksPath.flatMap(Path.init)
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`
}
/// Returns the path for the resources directory in this bundle.
public var resources: Path? {
return resourcePath.flatMap(Path.init)
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`
}
/// Returns the path for this bundle.
public var path: Path {
var path: Path {
return Path(string: bundlePath)
}
}
/// Extensions on `String` that work with `Path` rather than `String` or `URL`
public extension String {
/// Initializes this `String` with the contents of the provided path.
@inlinable
@@ -40,6 +60,7 @@ public extension String {
}
}
/// Extensions on `Data` that work with `Path` rather than `String` or `URL`
public extension Data {
/// Initializes this `Data` with the contents of the provided path.
@inlinable
@@ -65,3 +86,24 @@ public extension Data {
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)
}
}

View File

@@ -1,23 +1,79 @@
import Foundation
//#if os(Linux)
//import func Glibc.chmod
//#endif
public extension Path {
/// - Note: If file is already locked, does nothing
/// - Note: If file doesnt exist, throws
//MARK: Filesystem Attributes
/**
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
public func lock() throws -> Path {
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
}
/**
- 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)
let b = attrs[.immutable] as? Bool ?? false
if !b {
attrs[.immutable] = true
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
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.
*/
@discardableResult
public func unlock() throws -> Path {
func unlock() throws -> Path {
#if !os(Linux)
var attrs: [FileAttributeKey: Any]
do {
attrs = try FileManager.default.attributesOfItem(atPath: string)
@@ -29,32 +85,7 @@ public extension Path {
attrs[.immutable] = false
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
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
/**
Provided for relative-path coding. See the instructions in our
[README](https://github.com/mxcl/Path.swift/#codable).
*/
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")!
}
/**
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 {
let value = try decoder.singleValueContainer().decode(String.self)
if value.hasPrefix("/") {
@@ -18,6 +38,8 @@ extension Path: Codable {
}
}
/// - SeeAlso: `CodingUserInfoKey.relativePath`
/// :nodoc:
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let root = encoder.userInfo[.relativePath] as? Path {

View File

@@ -1,7 +1,9 @@
import Foundation
extension Path {
/// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`.
//MARK: Common Directories
/// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath)
}
@@ -72,7 +74,7 @@ extension Path {
/**
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.
*/
public static var caches: Path {

View File

@@ -1,17 +1,28 @@
import Foundation
public extension Path {
//MARK: File Management
/**
Copies a file.
try Path.root.join("bar").copy(to: Path.home/"foo")
// => "/Users/mxcl/foo"
- Note: `throws` if `to` is a directory.
- 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
- SeeAlso: copy(into:overwrite:)
- SeeAlso: `copy(into:overwrite:)`
*/
@discardableResult
public func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists {
func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile, isFile {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.copyItem(atPath: string, toPath: to.string)
@@ -21,37 +32,65 @@ public extension Path {
/**
Copies a file into a directory
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 first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`.
- 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(into:overwrite:)`
*/
@discardableResult
public func copy(into: Path, overwrite: Bool = false) throws -> Path {
func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} else if overwrite, !into.isDirectory {
try into.delete()
}
let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
}
#if os(Linux)
#if swift(>=5.1)
// check if fixed
#else
if !overwrite, rv.isFile {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
#endif
try FileManager.default.copyItem(at: url, to: rv.url)
return rv
}
/**
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 overwrite: If true overwrites any file that already exists at `to`.
- Returns: `to` to allow chaining
- 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
public func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists {
func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.moveItem(at: url, to: to.url)
@@ -61,30 +100,45 @@ public extension Path {
/**
Moves a file into a directory
try Path.root.join("bar").move(into: .home)
// => "/Users/mxcl/bar"
If the destination does not exist, this function creates the directory first.
- Note: `throws` if `into` is a file.
- 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.
- SeeAlso: move(into:overwrite:)
*/
@discardableResult
public func move(into: Path) throws -> Path {
func move(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
try into.mkpath()
try into.mkdir(.p)
} else if !into.isDirectory {
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
}
/// 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
public func delete() throws {
try FileManager.default.removeItem(at: url)
func delete() throws {
if exists {
try FileManager.default.removeItem(at: url)
}
}
/**
@@ -97,78 +151,51 @@ public extension Path {
return try "".write(to: self)
}
/// Helper due to Linux Swift being incomplete.
private func _foo(go: () throws -> Void) throws {
#if !os(Linux)
/**
Creates the directory at this path.
- 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 {
try go()
let wid = options == .p
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: wid, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists {
// noop
}
#else
do {
try go()
//noop (fails to trigger on Linux)
} catch {
#if os(Linux)
let error = error as NSError
guard error.domain == NSCocoaErrorDomain, error.code == CocoaError.Code.fileWriteFileExists.rawValue else {
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)
#else
throw error
#endif
}
return self
}
/**
Creates the directory at this path.
- Note: Creates any intermediary directories, if required.
- Returns: `self` to allow chaining.
Renames the file at path.
Path.root.foo.bar.rename(to: "baz") // => /foo/baz
- Parameter to: the new basename for the file
- Returns: The renamed path.
*/
@discardableResult
public func mkpath() throws -> Path {
try _foo {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
}
return self
}
/**
Replaces the contents of the file at this path with the provided string.
- Note: If file doesnt exist, creates file
- 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
public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path {
let resetPerms: Int?
if exists, !isWritable {
resetPerms = try FileManager.default.attributesOfItem(atPath: string)[.posixPermissions] as? Int
let perms = resetPerms ?? 0o777
try chmod(perms | 0o200)
} else {
resetPerms = nil
}
defer {
_ = try? resetPerms.map(self.chmod)
}
try contents.write(to: self)
return self
func rename(to newname: String) throws -> Path {
let newpath = parent/newname
try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
return newpath
}
}
/// Options for `Path.mkdir(_:)`
public enum MakeDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`.
case p
}

View File

@@ -1,13 +1,5 @@
import class Foundation.NSString
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`
public var description: String {
@@ -18,6 +10,6 @@ extension Path: CustomStringConvertible {
extension Path: CustomDebugStringConvertible {
/// Returns eg. `Path(string: "/foo")`
public var debugDescription: String {
return "Path(string: \(string))"
return "Path(\(string))"
}
}

View File

@@ -1,13 +1,39 @@
import Foundation
/**
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 {
/// Same as the `ls` command is shallow
/// - Parameter skipHiddenFiles: Same as the `ls -a` if false. Otherwise returns only the non hidden files. Default is false.
func ls(skipHiddenFiles: Bool = false) throws -> [Entry] {
let options: FileManager.DirectoryEnumerationOptions = skipHiddenFiles ? [.skipsHiddenFiles] : []
let paths = try FileManager.default.contentsOfDirectory(at: url,
includingPropertiesForKeys: nil,
options: options)
//MARK: Directory Listings
/**
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`.
- Important: `includeHiddenFiles` does not work on Linux
*/
func ls(includeHiddenFiles: Bool = true) throws -> [Entry] {
var opts = FileManager.DirectoryEnumerationOptions()
#if !os(Linux)
if !includeHiddenFiles {
opts.insert(.skipsHiddenFiles)
}
#endif
let paths = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: opts)
func convert(url: URL) -> Entry? {
guard let path = Path(url.path) else { return nil }
return Entry(kind: path.isDirectory ? .directory : .file, path: path)
@@ -16,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.
var directories: [Path] {
return compactMap {
@@ -24,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] {
return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil

View File

@@ -1,15 +1,16 @@
import Foundation
#if os(Linux)
import func Glibc.access
#else
import func Darwin.access
#endif
public extension Path {
/// 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)
}
//MARK: Filesystem Properties
/// Returns true if the path represents an actual directory.
var isDirectory: Bool {
var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
/// Returns true if the path represents an actual filesystem entry.
var exists: Bool {
return FileManager.default.fileExists(atPath: string)
}
/// Returns true if the path represents an actual filesystem entry that is *not* a directory.
@@ -18,13 +19,40 @@ public extension Path {
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.
var isExecutable: Bool {
return FileManager.default.isExecutableFile(atPath: string)
/// Returns true if the path represents an actual directory.
var isDirectory: Bool {
var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
}
/// Returns true if the path represents an actual filesystem entry.
var exists: Bool {
return FileManager.default.fileExists(atPath: string)
/// Returns true if the path represents an actual file that is also readable by the current user.
var isReadable: Bool {
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
}
}
}

View File

@@ -1,28 +1,60 @@
import Foundation
/**
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 p2 = Path.root/url.path
let p3 = Path.cwd/relativePathString
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 {
init(string: String) {
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)
}
/// :nodoc:
public subscript(dynamicMember pathComponent: String) -> Path {
let str = (string as NSString).appendingPathComponent(pathComponent)
return Path(string: str)
}
//MARK: Properties
/// The underlying filesystem path
public let string: String
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
*/
@inlinable
public var `extension`: String {
return (string as NSString).pathExtension
/// Returns a `URL` representing this file path.
public var url: URL {
return URL(fileURLWithPath: string)
}
/**
@@ -37,33 +69,56 @@ public struct Path: Equatable, Hashable, Comparable {
return Path(string: (string as NSString).deletingLastPathComponent)
}
/// Returns a `URL` representing this file path.
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
- Note: We special case eg. `foo.tar.gz`.
*/
@inlinable
public var url: URL {
return URL(fileURLWithPath: string)
public var `extension`: String {
if string.hasSuffix(".tar.gz") {
return "tar.gz"
} else {
return (string as NSString).pathExtension
}
}
//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
- 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)
}
/**
The basename for the provided file, optionally dropping the file extension.
Joins a path and a string to produce a new path.
Path.root.join("foo.swift").basename() // => "foo.swift"
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
Path.root/"a" // => /a
Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Returns: A string that is the filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension.
- 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(_:)`
*/
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))
} else {
return str.lastPathComponent
}
}
@inlinable
public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
}
/**
@@ -104,59 +159,32 @@ 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("a/b") // => /a/b
Path.root.join("a").join("b") // => /a/b
Path.root.join("a").join("/b") // => /a/b
Path.root.join("foo.swift").basename() // => "foo.swift"
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
- Parameter pathComponent: The string to join with this path.
- Returns: A new joined path.
- SeeAlso: /(:Path,:String)
- Returns: A string that is the filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension.
*/
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 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))
} else {
return str.lastPathComponent
}
}
}
/// Returns the locale-aware sort order for the two paths.
/// :nodoc:
@inlinable
public static func <(lhs: Path, rhs: Path) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
}
/// A file entry from a directory listing.
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
}
}
/**
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

@@ -14,25 +14,32 @@ class PathTests: XCTestCase {
func testEnumeration() throws {
let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch()
try tmpdir.join("b").touch()
try tmpdir.join("c").touch()
try tmpdir.join(".d").mkdir().join("e").touch()
try tmpdir.a.mkdir().c.touch()
try tmpdir.join("b.swift").touch()
try tmpdir.c.touch()
try tmpdir.join(".d").mkdir().e.touch()
var paths = Set<String>()
let lsrv = try tmpdir.ls()
var dirs = 0
for entry in try tmpdir.ls() {
for entry in lsrv {
if entry.kind == .directory {
dirs += 1
}
paths.insert(entry.path.basename())
}
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"])
}
func testEnumerationSkippingHiddenFiles() throws {
#if !os(Linux)
let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch()
@@ -42,7 +49,7 @@ class PathTests: XCTestCase {
var paths = Set<String>()
var dirs = 0
for entry in try tmpdir.ls(skipHiddenFiles: true) {
for entry in try tmpdir.ls(includeHiddenFiles: false) {
if entry.kind == .directory {
dirs += 1
}
@@ -50,7 +57,7 @@ class PathTests: XCTestCase {
}
XCTAssertEqual(dirs, 1)
XCTAssertEqual(paths, ["a", "b", "c"])
#endif
}
func testRelativeTo() {
@@ -68,6 +75,18 @@ class PathTests: XCTestCase {
XCTAssert((Path.root/"bin").isDirectory)
}
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")
}
func testMktemp() throws {
var path: Path!
try Path.mktemp {
@@ -82,7 +101,7 @@ class PathTests: XCTestCase {
try Path.mktemp {
for _ in 0...1 {
try $0.join("a").mkdir()
try $0.join("b/c").mkpath()
try $0.join("b/c").mkdir(.p)
}
}
}
@@ -131,5 +150,207 @@ class PathTests: XCTestCase {
XCTAssertEqual(Path.root/"~/b", Path("/~/b"))
XCTAssertEqual(Path("~/foo"), Path.home/"foo")
XCTAssertNil(Path("~foo"))
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")
}
func testCopyTo() throws {
try Path.mktemp { root in
try root.foo.touch().copy(to: root.bar)
XCTAssert(root.foo.isFile)
XCTAssert(root.bar.isFile)
}
}
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)
}
}
}
func testMoveTo() throws {
try Path.mktemp { root in
try root.foo.touch().move(to: root.bar)
XCTAssertFalse(root.foo.exists)
XCTAssert(root.bar.isFile)
}
}
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)
}
}
}
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(macOS)
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
XCTAssertEqual(Path.applicationSupport.string, NSHomeDirectory() + "/Library/Application Support")
#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
}
}
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() {
XCTAssertTrue(Bundle.main.path.isDirectory)
XCTAssertTrue(Bundle.main.resources.isDirectory)
// dont exist in tests
_ = Bundle.main.path(forResource: "foo", ofType: "bar")
_ = Bundle.main.sharedFrameworks
}
func testDataExentsions() throws {
let data = try Data(contentsOf: Path(#file)!)
try Path.mktemp { tmpdir in
_ = try data.write(to: tmpdir.foo)
}
}
func testStringExentsions() 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)!)
}
}

View File

@@ -42,7 +42,11 @@ class TemporaryDirectory {
}
deinit {
_ = try? FileManager.default.removeItem(at: url)
do {
try path.chmod(0o777).delete()
} catch {
//TODO log
}
}
}
@@ -52,3 +56,20 @@ extension Path {
return try body(tmp.path)
}
}
#if !os(macOS) && !os(Linux)
import XCTest
// SwiftPM generates code that is improperly escaped thus we require this to
// compile on iOS & tvOS.
public typealias XCTestCaseEntry = (testCaseClass: XCTestCase.Type, allTests: [(String, (XCTestCase) throws -> Void)])
public func testCase<T: XCTestCase>(_ allTests: [(String, (T) -> () throws -> Void)]) -> XCTestCaseEntry {
fatalError()
}
public func testCase<T: XCTestCase>(_ allTests: [(String, (T) -> () -> Void)]) -> XCTestCaseEntry {
fatalError()
}
#endif

View File

@@ -3,19 +3,38 @@ import XCTest
extension PathTests {
static let __allTests = [
("testBasename", testBasename),
("testBundleExtensions", testBundleExtensions),
("testCodable", testCodable),
("testCommonDirectories", testCommonDirectories),
("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto),
("testCopyTo", testCopyTo),
("testDataExentsions", testDataExentsions),
("testDelete", testDelete),
("testDynamicMember", testDynamicMember),
("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists),
("testExtension", testExtension),
("testFileHandleExtensions", testFileHandleExtensions),
("testFilesystemAttributes", testFilesystemAttributes),
("testIsDirectory", testIsDirectory),
("testJoin", testJoin),
("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo),
("testRelativeCodable", testRelativeCodable),
("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo),
("testRename", testRename),
("testStringConvertibles", testStringConvertibles),
("testStringExentsions", testStringExentsions),
("testTimes", testTimes),
]
}
#if os(Linux)
#if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(PathTests.__allTests),