Compare commits

..

73 Commits

Author SHA1 Message Date
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
Max Howell
5cc2fcbf30 Tag 0.4.1 2019-01-20 16:26:22 -05:00
Max Howell
7595c601e8 Attempt custom Jazzy index/contents 2019-01-20 16:24:06 -05:00
Luciano Almeida
d8ea357459 Adding ls -a like functionality to Path.ls() 2019-01-20 16:23:40 -05:00
Max Howell
86798755be Deploy to CocoaPods 2019-01-20 13:37:34 -05:00
Max Howell
152ad8a8ae Implementations of CommonDirs for Linux 2019-01-20 13:03:38 -05:00
Max Howell
a98ba37e59 Merge pull request #1 from itsallmememe/master
Common Directories
2019-01-20 12:30:29 -05:00
Max Howell
1c2cffada5 Support CocoaPods 2019-01-19 22:12:06 -05:00
Max Howell
6c24de4875 Fix actually deploying 2019-01-19 15:08:11 -05:00
Max Howell
6df12b3bb9 Refer to alternative packages 2019-01-19 15:07:47 -05:00
Max Howell
d52cdf96c4 Merge pull request #4 from mxcl/more-docs
More docs
2019-01-19 14:46:46 -05:00
Max Howell
3eda9a9741 100% documentation please 2019-01-19 14:36:27 -05:00
Max Howell
29149da72b Use FileManager.homeDirectory where possible 2019-01-19 14:35:42 -05:00
Max Howell
cd30e89808 Remove TemporaryDirectory
This violates the responsibility of this framework.
2019-01-19 14:20:12 -05:00
Max Howell
9f76eeb507 Link to Jazzy docs 2019-01-19 12:55:18 -05:00
Max Howell
b5bdaa6ceb Fix Jazzy root-url 2019-01-19 12:52:47 -05:00
Max Howell
17dd706115 Merge pull request #3 from mxcl/jazzy
Generate Jazzy docs in Travis
2019-01-19 12:40:21 -05:00
Max Howell
903038ae80 Generate Jazzy docs in Travis 2019-01-19 12:34:35 -05:00
Max Howell
f56811d64f Document various rules and caveats 2019-01-18 17:47:04 -05:00
Max Howell
f99e7b5ae7 Test iOS, tvOS & watchOS 2019-01-18 13:37:28 -05:00
Max Howell
9553968d66 Add Travis badge 2019-01-18 12:27:05 -05:00
Niall McCormack
12c7b348d6 added hooks for common directories - Documents, Caches and Application Support 2019-01-19 00:24:20 +08:00
Max Howell
3541c6ec8d Tidy 2019-01-18 09:25:24 -05:00
Max Howell
b4c92f86dc Fix tests on Linux 2019-01-17 18:23:22 -05:00
19 changed files with 1026 additions and 228 deletions

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

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

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

@@ -0,0 +1,3 @@
merges:
- action: delete_branch
- action: tag

2
.gitignore vendored
View File

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

View File

@@ -1,16 +1,130 @@
# only run for: merge commits, releases and pull-requests # only run for: merge commits, releases and pull-requests
if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/ if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/
jobs: stages:
include: - name: pretest
- os: osx - name: test
- name: deploy
if: branch =~ ^\d+\.\d+\.\d+$
os: osx
language: swift language: swift
osx_image: xcode10.1 osx_image: xcode10.1
script: swift test xcode_project: Path.swift.xcodeproj
- env: SWIFT_VERSION=4.2.1 xcode_scheme: Path.swift-Package
jobs:
include:
- script: swift test --parallel
name: macOS / Swift 4.2.1
- &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: |
set -o pipefail
xcodebuild \
-project Path.swift.xcodeproj \
-scheme Path.swift-Package \
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty
after_success: false
- &linux
env: SWIFT_VERSION=4.2.1
os: linux os: linux
name: Linux / Swift 4.2.1
language: generic language: generic
dist: trusty dist: trusty
sudo: false sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
script: swift test script: swift test --parallel
- <<: *linux
env: SWIFT_VERSION='5.0-DEVELOPMENT-SNAPSHOT-2019-01-22-a'
name: Linux / Swift 5.0.0-dev (2019-01-22)
- stage: pretest
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.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
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: output
on:
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
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
]

158
README.md
View File

@@ -1,9 +1,11 @@
# Path.swift # Path.swift ![badge-platforms] ![badge-languages][] [![Build Status](https://travis-ci.com/mxcl/Path.swift.svg)](https://travis-ci.com/mxcl/Path.swift)
A file-system pathing library focused on developer experience and robust A file-system pathing library focused on developer experience and robust end
endresults. results.
```swift ```swift
import Path
// convenient static members // convenient static members
let home = Path.home let home = Path.home
@@ -13,31 +15,50 @@ let docs = Path.home/"Documents"
// paths are *always* absolute thus avoiding common bugs // paths are *always* absolute thus avoiding common bugs
let path = Path(userInput) ?? Path.cwd/userInput let path = Path(userInput) ?? Path.cwd/userInput
// chainable syntax so you have less boilerplate // elegant, chainable syntax
try Path.home.join("foo").mkpath().join("bar").chmod(0o555) try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555)
// sensible considerations
try Path.home.join("bar").mkdir()
try Path.home.join("bar").mkdir() // doesnt throw we already have the desired result
// easy file-management // easy file-management
try Path.root.join("foo").copy(to: Path.root.join("bar")) let bar = try Path.root.join("foo").copy(to: Path.root/"bar")
print(bar) // => /bar
print(bar.isFile) // => true
// careful API to avoid common bugs // careful API considerations so as to avoid common bugs
try Path.root.join("foo").copy(into: Path.root.mkdir("bar")) let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
// ^^ other libraries would make the `to:` form handle both these cases print(foo) // => /bar/foo
// but that can easily lead to bugs where you accidentally write files that print(foo.isFile) // => true
// were meant to be directory destinations
// 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)
``` ```
Paths are just string representations, there *may not* be a real file there. We emphasize safety and correctness, just like Swift, and also (again like
Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Support mxcl
Hi, Im Max Howell and I have written a lot of open source software, and Hi, Im Max Howell and I have written a lot of open source software, and
probably you already use some of it (Homebrew anyone?). Please help me so I probably you already use some of it (Homebrew anyone?). I work full-time on
can continue to make tools and software you need and love. I appreciate it x. open source and its hard; currently I earn *less* than minimum wage. Please
help me continue my work, I appreciate it x
<a href="https://www.patreon.com/mxcl"> <a href="https://www.patreon.com/mxcl">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160"> <img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a> </a>
[Other donation/tipping options](http://mxcl.github.io/donate/)
# Handbook
Our [online API documentation] is automatically updated for new releases.
## Codable ## Codable
We support `Codable` as you would expect: We support `Codable` as you would expect:
@@ -77,6 +98,19 @@ decoder.userInfo[.relativePath] = Path.home
decoder.decode(from: data) decoder.decode(from: data)
``` ```
## Dynamic members
We support `@dynamicMemberLookup`:
```swift
let ls = Path.root.usr.bin.ls // => /usr/bin/ls
```
This is less commonly useful than you would think, hence our documentation
does not use it. Usually you are joining variables or other `String` arguments
or trying to describe files (and files usually have extensions). However when
you need it, its *lovely*.
## Initializing from user-input ## Initializing from user-input
The `Path` initializer returns `nil` unless fed an absolute path; thus to The `Path` initializer returns `nil` unless fed an absolute path; thus to
@@ -90,6 +124,9 @@ This is explicit, not hiding anything that code-review may miss and preventing
common bugs like accidentally creating `Path` objects from strings you did not common bugs like accidentally creating `Path` objects from strings you did not
expect to be relative. expect to be relative.
Our initializer is nameless to be consistent with the equivalent operation for
converting strings to `Int`, `Float` etc. in the standard library.
## Extensions ## Extensions
We have some extensions to Apple APIs: We have some extensions to Apple APIs:
@@ -102,8 +139,7 @@ bashProfile += "\n\nfoo"
try bashProfile.write(to: Path.home/".bash_profile") try bashProfile.write(to: Path.home/".bash_profile")
try Bundle.main.resources!.join("foo").copy(to: .home) try Bundle.main.resources.join("foo").copy(to: .home)
// ^^ `-> Path?` because the underlying `Bundle` function is `-> String?`
``` ```
## Directory listings ## Directory listings
@@ -112,34 +148,102 @@ We provide `ls()`, called because it behaves like the Terminal `ls` function,
the name thus implies its behavior, ie. that it is not recursive. the name thus implies its behavior, ie. that it is not recursive.
```swift ```swift
for path in Path.home.ls() { for entry in Path.home.ls() {
print(path.path) print(entry.path)
print(path.kind) // .directory or .file print(entry.kind) // .directory or .file
} }
for path in Path.home.ls() where path.kind == .file { for entry in Path.home.ls() where entry.kind == .file {
// //
} }
for path in Path.home.ls() where path.mtime > yesterday { for entry in Path.home.ls() where entry.path.mtime > yesterday {
// //
} }
let dirs = Path.home.ls().directories().filter { let dirs = Path.home.ls().directories
//
} let files = Path.home.ls().files
let swiftFiles = Path.home.ls().files(withExtension: "swift") let swiftFiles = Path.home.ls().files(withExtension: "swift")
``` ```
# Installation # Rules & Caveats
SwiftPM only: Paths are just string representations, there *might not* be a real file there.
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.0.0")) Path.home/"b" // => /Users/mxcl/b
// joining multiple strings works as youd expect
Path.home/"b"/"c" // => /Users/mxcl/b/c
// joining multiple parts at a time is fine
Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash
Path.home/"/b" // => /Users/mxcl/b
// of course, feel free to join variables:
let b = "b"
let c = "c"
Path.home/b/c // => /Users/mxcl/b/c
// tilde is not special here
Path.root/"~b" // => /~b
Path.root/"~/b" // => /~/b
// but is here
Path("~/foo")! // => /Users/mxcl/foo
// this does not work though
Path("~foo") // => nil
``` ```
*Path.swift* has the general policty 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.5.0"))
```
CocoaPods:
```ruby
pod 'Path.swift', '~> 0.5'
```
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 ### Get push notifications for new releases
https://codebasesaga.com/canopy/ https://codebasesaga.com/canopy/
# Alternatives
* [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
[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
[online API documentation]: https://mxcl.github.io/Path.swift/Structs/Path.html

View File

@@ -1,26 +1,51 @@
import Foundation import Foundation
/// Extensions on Foundations `Bundle` so you get `Path` rather than `String` or `URL`.
public extension Bundle { public extension Bundle {
/// Returns the path for requested resource in this bundle.
func path(forResource: String, ofType: String?) -> Path? { func path(forResource: String, ofType: String?) -> Path? {
let f: (String?, String?) -> String? = path(forResource:ofType:) let f: (String?, String?) -> String? = path(forResource:ofType:)
let str = f(forResource, ofType) let str = f(forResource, ofType)
return str.flatMap(Path.init) return str.flatMap(Path.init)
} }
public var sharedFrameworks: Path? { /// Returns the path for the shared-frameworks directory in this bundle.
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`
} }
public var resources: Path? { /// Returns the path for the resources directory in this bundle.
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`
} }
public var path: Path { /// Returns the path for this bundle.
var path: Path {
return Path(string: bundlePath) return Path(string: bundlePath)
} }
} }
/// Extensions on `String` that work with `Path` rather than `String` or `URL`
public extension String { public extension String {
/// Initializes this `String` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOfFile: path.string) try self.init(contentsOfFile: path.string)
@@ -35,7 +60,9 @@ public extension String {
} }
} }
/// Extensions on `Data` that work with `Path` rather than `String` or `URL`
public extension Data { public extension Data {
/// Initializes this `Data` with the contents of the provided path.
@inlinable @inlinable
init(contentsOf path: Path) throws { init(contentsOf path: Path) throws {
try self.init(contentsOf: path.url) try self.init(contentsOf: path.url)
@@ -47,7 +74,7 @@ public extension Data {
func write(to: Path, atomically: Bool = false) throws -> Path { func write(to: Path, atomically: Bool = false) throws -> Path {
let opts: NSData.WritingOptions let opts: NSData.WritingOptions
if atomically { if atomically {
#if os(macOS) #if !os(Linux)
opts = .atomicWrite opts = .atomicWrite
#else #else
opts = .atomic opts = .atomic
@@ -59,3 +86,24 @@ public extension Data {
return to return to
} }
} }
/// Extensions on `FileHandle` that work with `Path` rather than `String` or `URL`
public extension FileHandle {
/// Initializes this `FileHandle` for reading at the location of the provided path.
@inlinable
convenience init(forReadingAt path: Path) throws {
try self.init(forReadingFrom: path.url)
}
/// Initializes this `FileHandle` for writing at the location of the provided path.
@inlinable
convenience init(forWritingAt path: Path) throws {
try self.init(forWritingTo: path.url)
}
/// Initializes this `FileHandle` for reading and writing at the location of the provided path.
@inlinable
convenience init(forUpdatingAt path: Path) throws {
try self.init(forUpdating: path.url)
}
}

View File

@@ -1,10 +1,52 @@
import Foundation import Foundation
public extension Path { public extension Path {
//MARK: Filesystem Attributes
/**
Returns the creation-time of the file.
- Note: Returns UNIX-time-zero if there is no creation-time, this should only happen if the file doesnt exist.
*/
var ctime: Date {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.creationDate] as? Date ?? Date(timeIntervalSince1970: 0)
} catch {
//TODO log error
return Date(timeIntervalSince1970: 0)
}
}
/**
Returns the modification-time of the file.
- Note: Returns the creation time if there is no modification time.
- Note: Returns UNIX-time-zero if neither are available, this should only happen if the file doesnt exist.
*/
var mtime: Date {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? ctime
} catch {
//TODO log error
return Date(timeIntervalSince1970: 0)
}
}
/**
Sets the files attributes using UNIX octal notation.
Path.home.join("foo").chmod(0o555)
*/
@discardableResult
func chmod(_ octal: Int) throws -> Path {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return self
}
/// - Note: If file is already locked, does nothing /// - Note: If file is already locked, does nothing
/// - Note: If file doesnt exist, throws /// - Note: If file doesnt exist, throws
@discardableResult @discardableResult
public func lock() throws -> Path { func lock() throws -> Path {
var attrs = try FileManager.default.attributesOfItem(atPath: string) var attrs = try FileManager.default.attributesOfItem(atPath: string)
let b = attrs[.immutable] as? Bool ?? false let b = attrs[.immutable] as? Bool ?? false
if !b { if !b {
@@ -17,7 +59,7 @@ public extension Path {
/// - Note: If file isnt locked, does nothing /// - Note: If file isnt locked, does nothing
/// - Note: If file doesnt exist, does nothing /// - Note: If file doesnt exist, does nothing
@discardableResult @discardableResult
public func unlock() throws -> Path { func unlock() throws -> Path {
var attrs: [FileAttributeKey: Any] var attrs: [FileAttributeKey: Any]
do { do {
attrs = try FileManager.default.attributesOfItem(atPath: string) attrs = try FileManager.default.attributesOfItem(atPath: string)
@@ -31,26 +73,4 @@ public extension Path {
} }
return self return self
} }
/**
Sets the files attributes using UNIX octal notation.
Path.home.join("foo").chmod(0o555)
*/
@discardableResult
public func chmod(_ octal: Int) throws -> Path {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return self
}
/// - Returns: modification-time or creation-time if none
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 print(error)
return Date()
}
}
} }

View File

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

View File

@@ -0,0 +1,92 @@
import Foundation
extension Path {
//MARK: Common Directories
/// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath)
}
/// Returns a `Path` representing the root path.
public static var root: Path {
return Path(string: "/")
}
/// Returns a `Path` representing the users home directory
public static var home: Path {
let string: String
#if os(macOS)
if #available(OSX 10.12, *) {
string = FileManager.default.homeDirectoryForCurrentUser.path
} else {
string = NSHomeDirectory()
}
#else
string = NSHomeDirectory()
#endif
return Path(string: string)
}
/// Helper to allow search path and domain mask to be passed in.
private static func path(for searchPath: FileManager.SearchPathDirectory) -> Path {
#if os(Linux)
// the urls(for:in:) function is not implemented on Linux
//TODO strictly we should first try to use the provided binary tool
let foo = { ProcessInfo.processInfo.environment[$0].flatMap(Path.init) ?? $1 }
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
case .applicationSupportDirectory:
return foo("XDG_DATA_HOME", Path.home/".local/share")
case .cachesDirectory:
return foo("XDG_CACHE_HOME", Path.home/".cache")
default:
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()
}
}
return Path(string: pathString)
#endif
}
/**
The root for user documents.
- Note: There is no standard location for documents on Linux, thus we return `~/Documents`.
- Note: You should create a subdirectory before creating any files.
*/
public static var documents: Path {
return path(for: .documentDirectory)
}
/**
The root for cache files.
- Note: On Linux this is `XDG_CACHE_HOME`.
- Note: You should create a subdirectory before creating any files.
*/
public static var caches: Path {
return path(for: .cachesDirectory)
}
/**
For data that supports your running application.
- Note: On Linux is `XDG_DATA_HOME`.
- Note: You should create a subdirectory before creating any files.
*/
public static var applicationSupport: Path {
return path(for: .applicationSupportDirectory)
}
}

View File

@@ -1,17 +1,28 @@
import Foundation import Foundation
public extension Path { public extension Path {
//MARK: File Management
/** /**
Copies a file. Copies a file.
try Path.root.join("bar").copy(to: Path.home/"foo")
// => "/Users/mxcl/foo"
- Note: `throws` if `to` is a directory. - Note: `throws` if `to` is a directory.
- Parameter to: Destination filename. - Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`. - Parameter overwrite: If `true` and both `self` and `to` are files, overwrites `to`.
- Note: If either `self` or `to are directories, `overwrite` is ignored.
- Note: Throws if `overwrite` is `false` yet `to` is *already* identical to
`self` because even though *Path.swifts* policy is to noop if the desired
end result preexists, checking for this condition is too expensive a
trade-off.
- Returns: `to` to allow chaining - Returns: `to` to allow chaining
- SeeAlso: copy(into:overwrite:) - SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
public func copy(to: Path, overwrite: Bool = false) throws -> Path { func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.isFile, isFile {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
try FileManager.default.copyItem(atPath: string, toPath: to.string) try FileManager.default.copyItem(atPath: string, toPath: to.string)
@@ -21,99 +32,159 @@ public extension Path {
/** /**
Copies a file into a directory 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. If the destination does not exist, this function creates the directory first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory - Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`. - Parameter overwrite: If true overwrites any file that already exists at `into`.
- Returns: The `Path` of the newly copied file. - Returns: The `Path` of the newly copied file.
- SeeAlso: copy(into:overwrite:) - Note: `throws` if `into` is a file.
- Note: Throws if `overwrite` is `false` yet `to` is *already* identical to
`self` because even though *Path.swifts* policy is to noop if the desired
end result preexists, checking for this condition is too expensive a
trade-off.
- SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
public func copy(into: Path, overwrite: Bool = false) throws -> Path { func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} else if overwrite, !into.isDirectory {
try into.delete()
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
}
#if os(Linux)
#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) try FileManager.default.copyItem(at: url, to: rv.url)
return rv return rv
} }
/**
Moves a file.
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 @discardableResult
public func move(to: Path, overwrite: Bool = false) throws -> Path { func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists { if overwrite, to.isFile {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
try FileManager.default.moveItem(at: url, to: to.url) try FileManager.default.moveItem(at: url, to: to.url)
return to return to
} }
/**
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.
- 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:)
*/
@discardableResult @discardableResult
public func move(into: Path) throws -> Path { func move(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if !into.exists {
try into.mkpath() try into.mkdir(.p)
} else if !into.isDirectory { } else if !into.isDirectory {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url) try FileManager.default.moveItem(at: url, to: rv.url)
return rv return rv
} }
/**
Deletes the path, recursively if a directory.
- Note: noop: if the path doesnt exist
*Path.swift* doesnt error if desired end result preexists.
*/
@inlinable @inlinable
public func delete() throws { func delete() throws {
if exists {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
} }
}
/**
Creates an empty file at this path.
- Returns: `self` to allow chaining.
*/
@inlinable @inlinable
@discardableResult @discardableResult
func touch() throws -> Path { func touch() throws -> Path {
return try "".write(to: self) return try "".write(to: self)
} }
@inlinable /**
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 @discardableResult
public func mkdir() throws -> Path { func mkdir(_ options: MakeDirectoryOptions? = nil) throws -> Path {
do { do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) let wid = options == .p
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: wid, attributes: nil)
} catch CocoaError.Code.fileWriteFileExists { } catch CocoaError.Code.fileWriteFileExists {
// noop //noop (fails to trigger on Linux)
} catch {
#if os(Linux)
let error = error as NSError
guard error.domain == NSCocoaErrorDomain, error.code == CocoaError.Code.fileWriteFileExists.rawValue else {
throw error
}
#else
throw error
#endif
} }
return self return self
} }
@inlinable
@discardableResult @discardableResult
public func mkpath() throws -> Path { func rename(_ newname: String) throws -> Path {
do { let newpath = parent/newname
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
} catch CocoaError.Code.fileWriteFileExists { return newpath
// noop
} }
return self
} }
/// - Note: If file doesnt exist, creates file /// Options for `Path.mkdir(_:)`
/// - Note: If file is not writable, makes writable first, resetting permissions after the write public enum MakeDirectoryOptions {
@discardableResult /// Creates intermediary directories; works the same as `mkdir -p`.
public func replaceContents(with contents: String, atomically: Bool = false, encoding: String.Encoding = .utf8) throws -> Path { case p
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
}
} }

View File

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

View File

@@ -1,24 +1,64 @@
import Foundation 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 { public extension Path {
/// same as the `ls` command is shallow //MARK: Directory Listings
func ls() throws -> [Entry] {
let relativePaths = try FileManager.default.contentsOfDirectory(atPath: string) /**
func convert(relativePath: String) -> Entry { Same as the `ls -a` command output is shallow and unsorted.
let path = self/relativePath - 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) return Entry(kind: path.isDirectory ? .directory : .file, path: path)
} }
return relativePaths.map(convert) return paths.compactMap(convert)
} }
} }
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] { var directories: [Path] {
return compactMap { return compactMap {
$0.kind == .directory ? $0.path : nil $0.kind == .directory ? $0.path : nil
} }
} }
/// Filters the list of entries to be a list of Paths that are files.
var files: [Path] {
return compactMap {
$0.kind == .file ? $0.path : nil
}
}
/// Filters the list of entries to be a list of Paths that are files with the specified extension.
func files(withExtension ext: String) -> [Path] { func files(withExtension ext: String) -> [Path] {
return compactMap { return compactMap {
$0.kind == .file && $0.path.extension == ext ? $0.path : nil $0.kind == .file && $0.path.extension == ext ? $0.path : nil

View File

@@ -1,25 +1,42 @@
import Foundation import Foundation
public extension Path { public extension Path {
var isWritable: Bool { //MARK: Filesystem Properties
return FileManager.default.isWritableFile(atPath: string)
} /// Returns true if the path represents an actual filesystem entry.
var exists: Bool {
var isDirectory: Bool { return FileManager.default.fileExists(atPath: string)
var isDir: ObjCBool = false
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && isDir.boolValue
} }
/// Returns true if the path represents an actual filesystem entry that is *not* a directory.
var isFile: Bool { var isFile: Bool {
var isDir: ObjCBool = true var isDir: ObjCBool = true
return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue return FileManager.default.fileExists(atPath: string, isDirectory: &isDir) && !isDir.boolValue
} }
/// Returns true if the path represents an actual 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 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 {
return FileManager.default.isDeletableFile(atPath: string)
}
/// Returns true if the path represents an actual file that is also executable by the current user.
var isExecutable: Bool { var isExecutable: Bool {
return FileManager.default.isExecutableFile(atPath: string) return FileManager.default.isExecutableFile(atPath: string)
} }
var exists: Bool {
return FileManager.default.fileExists(atPath: string)
}
} }

View File

@@ -1,50 +1,134 @@
import Foundation import Foundation
/**
A `Path` represents an absolute path on a filesystem.
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 `Path`s 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
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: There may not be an actual filesystem entry at the path. The underlying
representation for `Path` is `String`.
*/
@dynamicMemberLookup
public struct Path: Equatable, Hashable, Comparable { 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 public let string: String
public static var cwd: Path { /// Returns a `URL` representing this file path.
return Path(string: FileManager.default.currentDirectoryPath)
}
public static var root: Path {
return Path(string: "/")
}
public static var home: Path {
return Path(string: NSHomeDirectory())
}
@inlinable
public var `extension`: String {
return (string as NSString).pathExtension
}
/// - Note: always returns a valid path, `Path.root.parent` *is* `Path.root`
public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent)
}
@inlinable
public var url: URL { public var url: URL {
return URL(fileURLWithPath: string) return URL(fileURLWithPath: string)
} }
public func basename(dropExtension: Bool = false) -> String { /**
let str = string as NSString Returns the parent directory for this path.
if !dropExtension {
return str.lastPathComponent Path is not aware of the nature of the underlying file, but this is
} else { irrlevant since the operation is the same irrespective of this fact.
let ext = str.pathExtension
if !ext.isEmpty { - Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
return String(str.lastPathComponent.dropLast(ext.count + 1)) */
} else { public var parent: Path {
return str.lastPathComponent return Path(string: (string as NSString).deletingLastPathComponent)
} }
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
- Note: We special case eg. `foo.tar.gz`.
*/
@inlinable
public var `extension`: String {
if string.hasSuffix(".tar.gz") {
return "tar.gz"
} else {
return (string as NSString).pathExtension
} }
} }
//TODO another variant that returns `nil` if result would start with `..` //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)
}
/**
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: `join(_:)`
*/
@inlinable
public static func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
}
/**
Returns a string representing the relative path to `base`.
- Note: If `base` is not a logical prefix for `self` your result will be prefixed some number of `../` components.
- Parameter base: The base to which we calculate the relative path.
- ToDo: Another variant that returns `nil` if result would start with `..`
*/
public func relative(to base: Path) -> String { public func relative(to base: Path) -> String {
// Split the two paths into their components. // Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying. // FIXME: The is needs to be optimized to avoid unncessary copying.
@@ -75,28 +159,33 @@ public struct Path: Equatable, Hashable, Comparable {
} }
} }
public func join<S>(_ part: S) -> Path where S: StringProtocol { /**
//TODO standardizingPath does more than we want really (eg tilde expansion) The basename for the provided file, optionally dropping the file extension.
let str = (string as NSString).appendingPathComponent(String(part))
return Path(string: (str as NSString).standardizingPath) Path.root.join("foo.swift").basename() // => "foo.swift"
Path.root.join("foo.swift").basename(dropExtension: true) // => "foo"
- Returns: A string that is the filenames basename.
- Parameter dropExtension: If `true` returns the basename without its file extension.
*/
public func basename(dropExtension: Bool = false) -> String {
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 @inlinable
public static func <(lhs: Path, rhs: Path) -> Bool { public static func <(lhs: Path, rhs: Path) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
} }
public struct Entry {
public enum Kind {
case file
case directory
}
public let kind: Kind
public let path: Path
}
}
@inlinable
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
} }

View File

@@ -1,30 +0,0 @@
import Foundation
public class TemporaryDirectory {
public let url: URL
public var path: Path { return Path(string: url.path) }
public init() throws {
#if os(macOS)
url = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: URL(fileURLWithPath: "/"), create: true)
#else
let envs = ProcessInfo.processInfo.environment
let env = envs["TMPDIR"] ?? envs["TEMP"] ?? envs["TMP"] ?? "/tmp"
let dir = Path.root/env/"swift-sh.XXXXXX"
var template = [UInt8](dir.string.utf8).map({ Int8($0) }) + [Int8(0)]
guard mkdtemp(&template) != nil else { throw CocoaError.error(.featureUnsupported) }
url = URL(fileURLWithPath: String(cString: template))
#endif
}
deinit {
_ = try? FileManager.default.removeItem(at: url)
}
}
public extension Path {
static func mktemp<T>(body: (Path) throws -> T) throws -> T {
let tmp = try TemporaryDirectory()
return try body(tmp.path)
}
}

View File

@@ -12,15 +12,44 @@ class PathTests: XCTestCase {
} }
func testEnumeration() throws { func testEnumeration() throws {
let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path
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 lsrv {
if entry.kind == .directory {
dirs += 1
}
paths.insert(entry.path.basename())
}
XCTAssertEqual(dirs, 2)
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_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch() try tmpdir.join("a").mkdir().join("c").touch()
try tmpdir.join("b").touch() try tmpdir.join("b").touch()
try tmpdir.join("c").touch() try tmpdir.join("c").touch()
try tmpdir.join(".d").mkdir().join("e").touch()
var paths = Set<String>() var paths = Set<String>()
var dirs = 0 var dirs = 0
for entry in try tmpdir.ls() { for entry in try tmpdir.ls(includeHiddenFiles: false) {
if entry.kind == .directory { if entry.kind == .directory {
dirs += 1 dirs += 1
} }
@@ -28,6 +57,7 @@ class PathTests: XCTestCase {
} }
XCTAssertEqual(dirs, 1) XCTAssertEqual(dirs, 1)
XCTAssertEqual(paths, ["a", "b", "c"]) XCTAssertEqual(paths, ["a", "b", "c"])
#endif
} }
func testRelativeTo() { func testRelativeTo() {
@@ -37,12 +67,24 @@ class PathTests: XCTestCase {
func testExists() { func testExists() {
XCTAssert(Path.root.exists) XCTAssert(Path.root.exists)
XCTAssert((Path.root/"Users").exists) XCTAssert((Path.root/"bin").exists)
} }
func testIsDirectory() { func testIsDirectory() {
XCTAssert(Path.root.isDirectory) XCTAssert(Path.root.isDirectory)
XCTAssert((Path.root/"Users").isDirectory) 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 { func testMktemp() throws {
@@ -59,7 +101,7 @@ class PathTests: XCTestCase {
try Path.mktemp { try Path.mktemp {
for _ in 0...1 { for _ in 0...1 {
try $0.join("a").mkdir() try $0.join("a").mkdir()
try $0.join("b/c").mkpath() try $0.join("b/c").mkdir(.p)
} }
} }
} }
@@ -93,4 +135,73 @@ class PathTests: XCTestCase {
decoder.userInfo[.relativePath] = root decoder.userInfo[.relativePath] = root
XCTAssertEqual(try decoder.decode([Path].self, from: data), input) XCTAssertEqual(try decoder.decode([Path].self, from: data), input)
} }
func testJoin() {
let prefix = Path.root/"Users/mxcl"
XCTAssertEqual(prefix/"b", Path("/Users/mxcl/b"))
XCTAssertEqual(prefix/"b"/"c", Path("/Users/mxcl/b/c"))
XCTAssertEqual(prefix/"b/c", Path("/Users/mxcl/b/c"))
XCTAssertEqual(prefix/"/b", Path("/Users/mxcl/b"))
let b = "b"
let c = "c"
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")
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 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 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("foo")
XCTAssertFalse(file.exists)
XCTAssertTrue(foo.isFile)
}
do {
let file = try root.bar.touch()
XCTAssertThrowsError(try file.rename("foo"))
}
}
}
} }

View File

@@ -0,0 +1,71 @@
@testable import Path
import Foundation
class TemporaryDirectory {
let url: URL
var path: Path { return Path(string: url.path) }
/**
Creates a new temporary directory.
The directory is recursively deleted when this object deallocates.
If you need a temporary directory on a specific volume use the `appropriateFor`
parameter.
- Important: If you are moving a file, ensure to use the `appropriateFor`
parameter, since it is volume aware and moving the file across volumes will take
exponentially longer!
- Important: The `appropriateFor` parameter does not work on Linux.
- Parameter appropriateFor: The temporary directory will be located on this
volume.
*/
init(appropriateFor: URL? = nil) throws {
#if !os(Linux)
let appropriate: URL
if let appropriateFor = appropriateFor {
appropriate = appropriateFor
} else if #available(OSX 10.12, iOS 10, tvOS 10, watchOS 3, *) {
appropriate = FileManager.default.temporaryDirectory
} else {
appropriate = URL(fileURLWithPath: NSTemporaryDirectory())
}
url = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: appropriate, create: true)
#else
let envs = ProcessInfo.processInfo.environment
let env = envs["TMPDIR"] ?? envs["TEMP"] ?? envs["TMP"] ?? "/tmp"
let dir = Path.root/env/"swift-sh.XXXXXX"
var template = [UInt8](dir.string.utf8).map({ Int8($0) }) + [Int8(0)]
guard mkdtemp(&template) != nil else { throw CocoaError.error(.featureUnsupported) }
url = URL(fileURLWithPath: String(cString: template))
#endif
}
deinit {
_ = try? FileManager.default.removeItem(at: url)
}
}
extension Path {
static func mktemp<T>(body: (Path) throws -> T) throws -> T {
let tmp = try TemporaryDirectory()
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

@@ -5,13 +5,20 @@ extension PathTests {
("testBasename", testBasename), ("testBasename", testBasename),
("testCodable", testCodable), ("testCodable", testCodable),
("testConcatenation", testConcatenation), ("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto),
("testDynamicMember", testDynamicMember),
("testEnumeration", testEnumeration), ("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists), ("testExists", testExists),
("testExtension", testExtension),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
("testRelativePathCodable", testRelativePathCodable), ("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo), ("testRelativeTo", testRelativeTo),
("testRename", testRename),
] ]
} }