Compare commits

..

141 Commits
0.5.0 ... 1.4.1

Author SHA1 Message Date
Dave Kolas
8e355c28e9 Rename Path->Bool.swift to PathToBool.swift
Makes path Windows-friendly
2023-10-24 10:24:59 -04:00
Max Howell
46b0cd883b Document PathStruct 2023-06-27 10:13:35 -04:00
Max Howell
9c6f807b0a Merge pull request #77 from ConfusedVorlon/find_hidden_modifier
Find hidden modifier
2021-07-30 12:28:04 -04:00
Rob Jonson
dad3d84040 don't implement hidden(false) for swift < 5
and provide a warning
2021-07-30 17:09:37 +01:00
Rob Jonson
5377bceb5f Update linux test list 2021-07-28 22:29:02 +01:00
Rob Jonson
f593437cf5 make find() configurable to ignore hidden files & directories 2021-07-28 15:12:09 +01:00
Rob Jonson
6e8a42f01d Fix assert error message 2021-07-28 15:06:20 +01:00
Max Howell
13d62c3068 Merge pull request #75 from mxcl/ci/macos-11
[ci] can has macos-11
2021-06-23 12:48:10 -04:00
Max Howell
99a0474b0f [ci] can has macos-11 2021-06-23 12:32:23 -04:00
Max Howell
82640e629d Merge pull request #74 from mxcl/ci/5.5
ci/5.5
2021-06-17 12:00:29 -04:00
Max Howell
f49e5c82c7 This seems more correct 2021-06-17 11:55:53 -04:00
Max Howell
287afe3783 Fix docs for ls(.a) 2021-06-17 11:55:52 -04:00
Max Howell
bb449ff412 Merge pull request #73 from mxcl/fixes/55
typealias PathStruct and add Swift 5.5 niceness
2021-06-16 11:11:18 -04:00
Max Howell
14f03abaad typealias PathStruct and add Swift 5.5 niceness
Fixes #55
2021-06-16 11:05:17 -04:00
Max Howell
ecbb3a60fe Merge pull request #71 from mxcl/ci/warnings-as-errors
[ci] warnings as errors
2021-06-07 10:45:37 -04:00
Max Howell
3af771f543 [ci] warnings as errors 2021-06-07 10:14:36 -04:00
Max Howell
0b68e5c011 Merge pull request #70 from mxcl/ci/mxcl/xcodebuild
use mxcl/xcodebuild
2021-06-05 13:45:31 -04:00
Max Howell
fec4ed25de use mxcl/xcodebuild 2021-06-05 10:55:16 -04:00
Max Howell
6e78d9317e Merge pull request #69 from mxcl/continuous-resilience
#continuous-resilience
2021-05-29 15:12:58 -04:00
Max Howell
3035c45808 #continuous-resilience 2021-05-29 15:10:17 -04:00
Max Howell
39f81ae258 Fix pods deploy 2021-05-28 16:23:47 -04:00
Max Howell
670dc1163f Merge pull request #68 from mxcl/ci/more 2021-05-28 16:05:44 -04:00
Max Howell
eb33ff8906 [ci] more; some fixes I found 2021-05-28 16:01:57 -04:00
Max Howell
f9cee2c75f Merge pull request #67
[ci] fix
2021-04-30 08:20:56 -04:00
Max Howell
7a974911d8 [ci] fix 2021-04-30 08:06:10 -04:00
Max Howell
891d70ec7c Specify Swift 5.1 syntax for targets 2020-08-30 16:27:17 -04:00
Max Howell
142d4bc111 Merge pull request #64 from mxcl/Path.source()
Add `Path.source()`
2020-08-19 13:41:50 -04:00
Max Howell
6461a550c6 Add Path.source() 2020-08-19 13:27:40 -04:00
Max Howell
8b90260517 Merge pull request #63 from mxcl/codecov-linux
Code coverage for linux
2020-08-01 19:45:07 -04:00
Max Howell
7924d20c8c Code coverage for linux 2020-08-01 15:59:54 -04:00
Max Howell
07007a5421 GHA CI Badge 2020-07-26 13:59:53 -04:00
Max Howell
8a217b3982 Merge pull request #62 from mxcl/sort-ls
ls() is sorted; Fixes #58
2020-07-26 13:58:44 -04:00
Max Howell
2b50909946 ls() is sorted; Fixes #58 2020-07-26 13:48:15 -04:00
Max Howell
baa6416208 Use GHA instead of Travis where possible 2020-07-26 13:39:10 -04:00
Max Howell
6e99825d9f Probably redundant tests, but why not 2020-02-09 14:52:49 -05:00
Max Howell
6e37bfde4d Update README.md 2020-02-09 14:29:28 -05:00
Max Howell
6e1eeb158a No fatal on linux Swift < 5 2020-01-25 14:00:59 -05:00
Max Howell
260196a27a CodeCoverage++ 2020-01-25 14:00:59 -05:00
Max Howell
0de9715b46 [ci skip] Update README.md 2020-01-25 12:05:58 -05:00
Max Howell
240d699986 Delete pushed version tag on failed deploy 2020-01-25 10:53:23 -05:00
Max Howell
b63b5746dc Delete pushed version tag on failed deploy 2020-01-24 12:14:03 -05:00
Max Howell
f062ed9ce3 Fix CI deploy 2020-01-24 12:01:34 -05:00
Max Howell
f1f7ee33b1 [ci skip] List all support Swift 2020-01-24 11:17:42 -05:00
Max Howell
694d04f18b Prepare 1.0.0 release 2020-01-24 11:03:07 -05:00
Max Howell
5636a7ac65 Update Swift Linux test versions 2020-01-18 12:10:32 -05:00
Max Howell
3e964833ff Fix README documentation for Finder 2020-01-18 12:10:32 -05:00
Max Howell
30122659a5 Update linux-tests; fail if warnings on travis
* Update linux-tests; fail if warnings on travis

* Fix warnings on Linux

* Typo

* Can’t test these on Linux
2019-08-18 16:52:24 -04:00
Max Howell
0ef50dff2e Finder is a iterable Sequence; .type -> .kind 2019-07-24 14:39:47 -04:00
Max Howell
dfad7367b7 Get out documentation %age up 2019-07-21 21:35:48 -04:00
Max Howell
af091cc1f0 Split this test so I can figure out CI failure 2019-07-21 18:58:50 -04:00
Max Howell
e5188bf93b Still not fixed 2019-07-21 18:58:49 -04:00
Max Howell
462a62920f Update Swifts in CI; Test Xcode 11 2019-07-21 18:58:49 -04:00
Max Howell
45b0b59a94 Better rx for tagged version detection on Travis 2019-07-21 17:37:10 -04:00
Max Howell
62073d584b Remove Entry since it is barely worthwhile sugar 2019-07-21 17:37:10 -04:00
Max Howell
621d1b0160 Remove @dynamicMember generally 2019-07-21 17:37:10 -04:00
Max Howell
d2bb2a1fdc Path.find() 2019-07-21 17:22:38 -04:00
Max Howell
38e98ee7fd Jazzy requires Xcode 10.2 now (per their docs) 2019-07-21 09:07:03 -04:00
Dash2507
dac007e907 Change "pkg" to "package" (#50) 2019-06-10 17:20:05 -04:00
Max Howell
b6b4a74a26 Move deploy script to @mxcl/ops 2019-04-14 12:40:07 -04:00
Max Howell
b76db41ca4 422 means the release already exists, so… succeed 2019-04-14 10:49:17 -04:00
Max Howell
8d5d67b81b Merge pull request #48 from mxcl/travis-swift-5.0-GM
[travis] Swift 5 GM
2019-03-25 22:02:33 -04:00
Max Howell
21ddc7dc3a [travis] Swift 5 GM 2019-03-25 21:40:17 -04:00
Max Howell
f324b4a562 Merge pull request #47 from mxcl/fix-symlink-delete
Adds `kind` fixes deleting broken symlinks
2019-03-18 19:56:39 -04:00
Max Howell
0e061f9cc8 Adds kind fixes deleting broken symlinks
`delete()` and other functions would check `exists` to do certain behaviors, but `exists` will validate a symlink if the entry is a symlink, thus instead we check if the path is an actual entry now instead.
2019-03-18 19:47:49 -04:00
Max Howell
7e774b6cf5 Use 10.2 image for pretest
Also some tidy
2019-03-06 15:55:51 -05:00
Max Howell
8b371fa5d2 Allow initialization from Substring etc. 2019-03-06 15:55:51 -05:00
Max Howell
02fd579f19 mxcl.github.io -> mxcl.dev
[ci skip]
2019-02-28 18:40:19 -05:00
repo-ranger[bot]
e915bc0cfb Merge pull request #43 from mxcl/Bundle.executable
Add Bundle.executable
2019-02-17 15:14:17 +00:00
Max Howell
f4c2c75aa1 Add Bundle.executable 2019-02-17 10:05:30 -05:00
repo-ranger[bot]
dc7affa28c Merge pull request #42 from mxcl/path-components
Add Path.components
2019-02-15 18:49:13 +00:00
Max Howell
476cdc1461 Add Path.components 2019-02-15 13:38:36 -05:00
Max Howell
a644208c62 Test Swift 4.0.3 also 2019-02-13 21:34:35 -05:00
Max Howell
d7a9819350 Fix publishing the release in the deploy stage
[ci skip]
2019-02-13 20:52:39 -05:00
repo-ranger[bot]
24a54c2ee0 Merge pull request #39 from mxcl/less-manifests-test
You can specify future Swifts in a 4.2 manifest!
2019-02-14 01:00:02 +00:00
Max Howell
3735ed4476 You can specify future Swifts in a 4.2 manifest! 2019-02-13 19:40:07 -05:00
Max Howell
2880aa556b This hack did not work in fact 2019-02-13 16:28:16 -05:00
repo-ranger[bot]
a125a871f5 Merge pull request #38 from mxcl/tweaks
Tweaks
2019-02-13 21:13:29 +00:00
Max Howell
d79844cf2b Use a symlink to prevent Package.swift divergence 2019-02-13 15:53:41 -05:00
Max Howell
d0648411ea Get minimum supported Swift version for CocoaPods 2019-02-13 15:47:40 -05:00
Max Howell
e74cc63271 [ci skip] Fix publish release command 2019-02-13 15:27:06 -05:00
Max Howell
28f84d3961 Deploy needs Swift 5 2019-02-13 10:54:10 -05:00
repo-ranger[bot]
db184a13a3 Merge pull request #37 from mxcl/deploy-script
“Scriptify” deployment
2019-02-13 15:28:21 +00:00
Max Howell
b65d167937 “Scriptify” deployment 2019-02-13 10:12:32 -05:00
repo-ranger[bot]
9a770ca576 Merge pull request #36 from mxcl/remove-deployment-targets
These deployment versions are the defaults
2019-02-13 14:49:00 +00:00
Max Howell
b7c189e6af These deployment versions are the defaults
Well, almost, but adjusted we still work so in fact the ”bump” was spurious.
2019-02-13 09:32:19 -05:00
repo-ranger[bot]
2758f0f698 Merge pull request #35 from mxcl/ci-xcode10.2
[ci] Xcode 10.2
2019-02-12 20:41:53 +00:00
Max Howell
e68ad25cc0 [ci] Xcode 10.2 2019-02-12 15:23:27 -05:00
Max Howell
c9d300a7b6 Path(_ url:) -> Path(url:) 2019-02-11 20:40:27 -05:00
Max Howell
ed4b773870 Fill in this TODO in README
[skip ci]
2019-02-11 15:13:14 -05:00
Max Howell
097e020735 There are no usernames on iOS etc. 2019-02-11 15:11:22 -05:00
Max Howell
be49fb9e49 Merge pull request #34 from mxcl/symlinks
Symlink funcs & support `NSURL` file-refs
2019-02-11 15:06:56 -05:00
Max Howell
164cd2b413 Fix iOS, etc. 2019-02-11 14:19:27 -05:00
Max Howell
709c3fb99d Symlink funcs & support NSURL file-refs
* Also removes most `NSString` usage
* Also does more thorough testing in some places
* Also adds
* Fixes `Path?(_:)` resolving symlinks in some cases
2019-02-11 14:04:06 -05:00
repo-ranger[bot]
6c84754ad8 Merge pull request #33 from mxcl/bundle-private-frameworks
Bundle.privateFrameworks
2019-02-09 18:32:02 +00:00
Max Howell
8469565b06 Bundle.privateFrameworks 2019-02-09 13:24:57 -05:00
Max Howell
ed45d10179 Docs updates; CocoaPods 16.0 release; [ci skip] 2019-02-08 09:14:23 -05:00
Max Howell
8033ae49b4 Add [pathos]
[pathos]: https://github.com/dduan/Pathos

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

This makes more elegant chains, the chain will fail when you operate on it, but you don’t have to do a check for optional first. Or risk a bang.
2019-01-26 15:20:32 -05:00
Max Howell
93e2701950 Docs tweaks
[ci skip]
2019-01-26 15:10:52 -05:00
Max Howell
bbf1f24ef6 Fix Cocoapods deploy 2019-01-26 14:42:00 -05:00
Max Howell
c08ccdfb30 Merge pull request #15 from mxcl/dynamic-members
Dynamic members
2019-01-26 13:34:17 -05:00
Max Howell
859164e59f Dynamic Members 2019-01-26 13:23:25 -05:00
Max Howell
44be1c45a9 Add Path.ctime 2019-01-26 13:17:39 -05:00
Max Howell
99b948f9c1 Minor documentation fixes
[ci skip]
2019-01-26 13:17:39 -05:00
Max Howell
3beba13677 Merge pull request #14 from mxcl/delete-noop
Delete noop
2019-01-26 11:12:17 -05:00
Max Howell
bafb05ff54 Document noop behavior 2019-01-26 11:05:49 -05:00
Max Howell
356a1b3ac2 Delete is a noop if file doesn’t exist
Closes #11.
2019-01-26 11:05:31 -05:00
Max Howell
6d8712f4d6 Remove mkpath, add mkdir(.p) 2019-01-26 11:05:10 -05:00
Max Howell
8744b68709 Make nodoc work for Codable 2019-01-26 10:59:20 -05:00
Max Howell
9ea32048f7 Merge pull request #13 from mxcl/more-docs
Improved documentation; Fixes #12
2019-01-25 21:20:24 -05:00
Max Howell
4b16dac3bf Improved documentation; Fixes #12 2019-01-25 20:46:37 -05:00
Max Howell
b613449232 Making this static will fix documentation location
[ci skip]
2019-01-25 12:23:54 -05:00
27 changed files with 2492 additions and 494 deletions

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

@@ -0,0 +1,3 @@
ignore:
- Tests/PathTests/etc.swift
- Tests/PathTests/TemporaryDirectory.swift

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

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

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

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

24
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: CD
on:
release:
types: published
jobs:
docs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: steven0351/publish-jazzy-docs@v1
with:
personal_access_token: ${{ secrets.PAT }}
config: .github/jazzy.yml
version: ${{ github.event.release.tag_name }}
history: false
pods:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- run: pod trunk push --allow-warnings
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
VERSION: ${{ github.event.release.tag_name }}

14
.github/workflows/checks.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
on:
push:
branches:
- master
paths:
- '**/*.swift'
- .github/workflows/checks.yml
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: swift --version
- run: swift test --parallel

87
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: CI
on:
pull_request:
paths:
- '**/*.swift'
- .github/workflows/ci.yml
schedule:
- cron: '3 3 * * 5' # 3:03 AM, every Friday
concurrency:
group: ${{ github.head_ref || 'push' }}
cancel-in-progress: true
jobs:
verify-linuxmain:
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2
- run: swift test --generate-linuxmain
- run: git diff --exit-code
apple:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- macos-10.15
- macos-11
platform:
- iOS
- tvOS
- macOS
- watchOS
steps:
- uses: actions/checkout@v2
- uses: mxcl/xcodebuild@v1
with:
platform: ${{ matrix.platform }}
code-coverage: true
warnings-as-errors: true
- uses: codecov/codecov-action@v1
linux:
runs-on: ubuntu-latest
strategy:
matrix:
swift:
- swift:4.2
- swift:5.0
- swift:5.1
- swift:5.2
- swift:5.3
- swift:5.4
- swiftlang/swift:nightly-5.5
container:
image: ${{ matrix.swift }}
steps:
- uses: mxcl/get-swift-version@v1
id: swift
- uses: actions/checkout@v2
- run: useradd -ms /bin/bash mxcl
- run: chown -R mxcl .
# ^^ we need to be a normal user and not root for the tests to be valid
- run: echo ARGS=--enable-code-coverage >> $GITHUB_ENV
if: ${{ steps.swift.outputs.marketing-version > 5 }}
- run: su mxcl -c "swift test --parallel $ARGS"
- name: Generate `.lcov`
if: ${{ steps.swift.outputs.marketing-version > 5 }}
run: |
apt-get -qq update && apt-get -qq install curl
b=$(swift build --show-bin-path)
llvm-cov export \
-format lcov \
-instr-profile="$b"/codecov/default.profdata \
--ignore-filename-regex='\.build|Tests' \
"$b"/*.xctest \
> info.lcov
- uses: codecov/codecov-action@v1
if: ${{ steps.swift.outputs.marketing-version > 5 }}
with:
file: ./info.lcov

3
.gitignore vendored
View File

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

View File

@@ -1,82 +1,43 @@
# only run for: merge commits, releases and pull-requests
if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/
# Travis does CD, GHA does CI
if: branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/ OR branch =~ /^\d+\.\d+\.\d+(-.*)?$/
stages:
- name: pretest
- name: test
- name: deploy
if: branch =~ ^\d+\.\d+\.\d+$
if: branch =~ /^deploy-\d+\.\d+\.\d+(-.*)?$/
- name: publish
if: branch =~ /^\d+\.\d+\.\d+(-.*)?$/
os: osx
language: swift
osx_image: xcode10.1
xcode_project: Path.swift.xcodeproj
xcode_scheme: Path.swift-Package
jobs:
include:
- script: swift test --parallel
name: macOS / Swift 4.2.1
- &xcodebuild
before_install: swift package generate-xcodeproj
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS / Swift 4.2.1
- <<: *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
- &linux
env: SWIFT_VERSION=4.2.1
os: 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 --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: Deploy
osx_image: xcode11
env: HOMEBREW_NO_INSTALL_CLEANUP=1
install: brew install mxcl/made/swift-sh
git.depth: false
script:
- set -e
- export VERSION=$(echo $TRAVIS_TAG | cut -c 8-)
- git tag "$VERSION" --force
- git remote set-url origin "https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG.git"
- git push origin "$VERSION"
- swift sh <(curl https://raw.githubusercontent.com/mxcl/ops/master/deploy) publish-release
- git push origin :$TRAVIS_TAG
after_failure: |
export VERSION=$(echo $TRAVIS_TAG | cut -c 8-)
git push origin :$VERSION
- stage: publish
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
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
osx_image: xcode11
install: gem install jazzy
before_script: swift package generate-xcodeproj
script: jazzy
script: |
jazzy --config .github/jazzy.yml \
--module-version $TRAVIS_TAG \
--github_url "https://github.com/$TRAVIS_REPO_SLUG"
deploy:
provider: pages
skip-cleanup: true
@@ -86,26 +47,11 @@ jobs:
tags: true
- name: CocoaPods
before_install: |
cat <<\ \ EOF> Path.swift.podspec
Pod::Spec.new do |s|
s.name = 'Path.swift'
s.version = 'TRAVIS_TAG'
s.summary = 'Delightful, robust file-pathing functions'
s.homepage = 'https://github.com/mxcl/Path.swift'
s.license = { :type => 'Unlicense', :file => 'LICENSE.md' }
s.author = { 'mxcl' => 'mxcl@me.com' }
s.source = { :git => 'https://github.com/mxcl/Path.swift.git', :tag => s.version.to_s }
s.social_media_url = 'https://twitter.com/mxcl'
s.osx.deployment_target = '10.10'
s.ios.deployment_target = '8.0'
s.tvos.deployment_target = '10.0'
s.watchos.deployment_target = '3.0'
s.source_files = 'Sources/*'
s.swift_version = '4.2'
end
EOF
sed -i '' "s/TRAVIS_TAG/$TRAVIS_TAG/" Path.swift.podspec
# ^^ see the Jazzy deployment for explanation
install: gem install cocoapods --pre
env: HOMEBREW_NO_INSTALL_CLEANUP=1
osx_image: xcode11
install:
- brew install mxcl/made/swift-sh
- curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy
- chmod u+x deploy
before_script: ./deploy generate-podspec
script: pod trunk push

View File

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

View File

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

16
Path.swift.podspec Normal file
View File

@@ -0,0 +1,16 @@
Pod::Spec.new do |spec|
spec.name = "Path.swift"
spec.version = ENV['VERSION'] || "0.0.1"
spec.summary = "Delightful, robust, cross-platform and chainable file-pathing functions."
spec.homepage = "https://github.com/mxcl/Path.swift"
spec.license = "Unlicense"
spec.author = { "Max Howell" => "mxcl@me.com" }
spec.source = { :git => "https://github.com/mxcl/Path.swift.git", :tag => "#{spec.version}" }
spec.source_files = "Sources/*.swift"
spec.swift_versions = ['4.2', '5']
spec.module_name = 'Path'
spec.osx.deployment_target = '10.10'
spec.ios.deployment_target = '8.0'
spec.tvos.deployment_target = '9.0'
spec.watchos.deployment_target = '2.0'
end

328
README.md
View File

@@ -1,7 +1,7 @@
# Path.swift ![badge-platforms] ![badge-languages] [![Build Status](https://travis-ci.com/mxcl/Path.swift.svg)](https://travis-ci.com/mxcl/Path.swift)
# Path.swift ![badge-platforms][] ![badge-languages][] [![badge-ci][]][travis] [![badge-jazzy][]][docs] [![badge-codecov][]][codecov] [![badge-version][]][cocoapods]
A file-system pathing library focused on developer experience and robust
endresults.
A file-system pathing library focused on developer experience and robust end
results.
```swift
import Path
@@ -28,32 +28,35 @@ 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.mkdir("bar"))
let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
print(foo) // => /bar/foo
print(foo.isFile) // => true
// ^^ the `into:` version will only copy *into* a directory, the `to:` version copies
// to a file at that path, thus you will not accidentally copy into directories you
// may not have realized existed.
// A practical example: installing a helper executable
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkpath()).chmod(0o500)
// we support dynamic-member-syntax when joining named static members, eg:
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences
// a practical example: installing a helper executable
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)
```
We emphasize safety and correctness, just like Swift, and also (again like
Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl
# Sponsor @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.
Hi, Im Max Howell and I have written a lot of open source software—generally
a good deal of my free time 👨🏻‍💻. Sponsorship helps me justify creating new open
source and maintaining it. Thank you.
<a href="https://www.patreon.com/mxcl">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
[Other donation/tipping options](http://mxcl.github.io/donate/)
[Sponsor @mxcl].
# Handbook
Our [online API documentation] is automatically updated for new releases.
Our [online API documentation][docs] covers 100% of our public API and is
automatically updated for new releases.
## Codable
@@ -70,30 +73,69 @@ try JSONEncoder().encode([Path.home, Path.home/"foo"])
]
```
However, often you want to encode relative paths:
Though we recommend encoding *relative* paths:
```swift
let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home
encoder.encode([Path.home, Path.home/"foo"])
encoder.encode([Path.home, Path.home/"foo", Path.home/"../baz"])
```
```json
[
"",
"foo",
"../baz"
]
```
**Note** make sure you decode with this key set *also*, otherwise we `fatal`
(unless the paths are absolute obv.)
**Note** if you encode with this key set you *must* decode with the key
set also:
```swift
let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home
decoder.decode(from: data)
try decoder.decode(from: data) // would throw if `.relativePath` not set
```
> ‡ If you are saving files to a system provided location, eg. Documents then
> the directory could change at Apples choice, or if say the user changes their
> username. Using relative paths also provides you with the flexibility in
> future to change where you are storing your files without hassle.
## Dynamic members
We support `@dynamicMemberLookup`:
```swift
let ls = Path.root.usr.bin.ls // => /usr/bin/ls
```
We only provide this for “starting” function, eg. `Path.home` or `Bundle.path`.
This is because we found in practice it was easy to write incorrect code, since
everything would compile if we allowed arbituary variables to take *any* named
property as valid syntax. What we have is what you want most of the time but
much less (potentially) dangerous (at runtime).
### Pathish
`Path`, and `DynamicPath` (the result of eg. `Path.root`) both conform to
`Pathish` which is a protocol that contains all pathing functions. Thus if
you create objects from a mixture of both you need to create generic
functions or convert any `DynamicPath`s to `Path` first:
```swift
let path1 = Path("/usr/lib")!
let path2 = Path.root.usr.bin
var paths = [Path]()
paths.append(path1) // fine
paths.append(path2) // error
paths.append(Path(path2)) // ok
```
This is inconvenient but as Swift stands theres nothing we can think of
that would help.
## Initializing from user-input
The `Path` initializer returns `nil` unless fed an absolute path; thus to
@@ -107,10 +149,30 @@ 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.
## Initializing from known strings
Theres no need to use the optional initializer in general if you have known
strings that you need to be paths:
```swift
let absolutePath = "/known/path"
let path1 = Path.root/absolutePath
let pathWithoutInitialSlash = "known/path"
let path2 = Path.root/pathWithoutInitialSlash
assert(path1 == path2)
let path3 = Path(absolutePath)! // at your options
assert(path2 == path3)
// be cautious:
let path4 = Path(pathWithoutInitialSlash)! // CRASH!
```
## Extensions
@@ -124,39 +186,100 @@ 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
We provide `ls()`, called because it behaves like the Terminal `ls` function,
the name thus implies its behavior, ie. that it is not recursive.
the name thus implies its behavior, ie. that it is not recursive and doesnt
list hidden files.
```swift
for entry in Path.home.ls() {
print(entry.path)
print(entry.kind) // .directory or .file
}
for entry in Path.home.ls() where entry.kind == .file {
for path in Path.home.ls() {
//
}
for entry in Path.home.ls() where entry.path.mtime > yesterday {
for path in Path.home.ls() where path.isFile {
//
}
let dirs = Path.home.ls().directories().filter {
for path in Path.home.ls() where path.mtime > yesterday {
//
}
let swiftFiles = Path.home.ls().files(withExtension: "swift")
let dirs = Path.home.ls().directories
// ^^ directories that *exist*
let files = Path.home.ls().files
// ^^ files that both *exist* and are *not* directories
let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" }
let includingHiddenFiles = Path.home.ls(.a)
```
**Note** `ls()` does not throw, instead outputing a warning to the console if it
fails to list the directory. The rationale for this is weak, please open a
ticket for discussion.
We provide `find()` for recursive listing:
```swift
for path in Path.home.find() {
// descends all directories, and includes hidden files by default
// so it behaves the same as the terminal command `find`
}
```
It is configurable:
```swift
for path in Path.home.find().depth(max: 1).extension("swift").type(.file).hidden(false) {
//
}
```
It can be controlled with a closure syntax:
```swift
Path.home.find().depth(2...3).execute { path in
guard path.basename() != "foo.lock" else { return .abort }
if path.basename() == ".build", path.isDirectory { return .skip }
//
return .continue
}
```
Or get everything at once as an array:
```swift
let paths = Path.home.find().map(\.self)
```
# `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 just
get on with it and not have to worry.
There is also some magic going on in Foundations filesystem APIs, which we look
for and ensure our API is deterministic, eg. [this test].
[this test]: https://github.com/mxcl/Path.swift/blob/master/Tests/PathTests/PathTests.swift#L539-L554
# `Path.swift` is properly cross-platform
`FileManager` on Linux is full of holes. We have found the holes and worked
round them where necessary.
# Rules & Caveats
Paths are just string representations, there *might not* be a real file there.
Paths are just (normalized) string representations, there *might not* be a real
file there.
```swift
Path.home/"b" // => /Users/mxcl/b
@@ -164,12 +287,19 @@ 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
// joining multiple parts simultaneously is fine
Path.home/"b/c" // => /Users/mxcl/b/c
// joining with absolute paths omits prefixed slash
Path.home/"/b" // => /Users/mxcl/b
// joining with .. or . works as expected
Path.home.foo.bar.join("..") // => /Users/mxcl/foo
Path.home.foo.bar.join(".") // => /Users/mxcl/foo/bar
// though note that we provide `.parent`:
Path.home.foo.bar.parent // => /Users/mxcl/foo
// of course, feel free to join variables:
let b = "b"
let c = "c"
@@ -182,44 +312,140 @@ Path.root/"~/b" // => /~/b
// but is here
Path("~/foo")! // => /Users/mxcl/foo
// this does not work though
// this works provided the user `Guest` exists
Path("~Guest") // => /Users/Guest
// but if the user does not exist
Path("~foo") // => nil
// paths with .. or . are resolved
Path("/foo/bar/../baz") // => /foo/baz
// symlinks are not resolved
Path.root.bar.symlink(as: "foo")
Path("/foo") // => /foo
Path.root.foo // => /foo
// unless you do it explicitly
try Path.root.foo.readlink() // => /bar
// `readlink` only resolves the *final* path component,
// thus use `realpath` if there are multiple symlinks
```
*Path.swift* has the general policy that if the desired end result preexists,
then its a noop:
* If you try to delete a file, but the file doesn't exist, we do nothing.
* If you try to make a directory and it already exists, we do nothing.
* If you call `readlink` on a non-symlink, we return `self`
However notably if you try to copy or move a file without specifying `overwrite`
and the file already exists at the destination and is identical, we dont check
for that as the check was deemed too expensive to be worthwhile.
## Symbolic links
* Two paths may represent the same *resolved* path yet not be equal due to
symlinks in such cases you should use `realpath` on both first if an
equality check is required.
* There are several symlink paths on Mac that are typically automatically
resolved by Foundation, eg. `/private`, we attempt to do the same for
functions that you would expect it (notably `realpath`), we *do* the same
for `Path.init`, but *do not* if you are joining a path that ends up being
one of these paths, (eg. `Path.root.join("var/private')`).
If a `Path` is a symlink but the destination of the link does not exist `exists`
returns `false`. This seems to be the correct thing to do since symlinks are
meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `type` is `nil`.
## We do not provide change directory functionality
Changing directory is dangerous, you should *always* try to avoid it and thus
we dont even provide the method. If you are executing a sub-process then
use `Process.currentDirectoryURL` to change *its* working directory when it
executes.
If you must change directory then use `FileManager.changeCurrentDirectory` as
early in your process as *possible*. Altering the global state of your apps
environment is fundamentally dangerous creating hard to debug issues that
you wont find for potentially *years*.
# I thought I should only use `URL`s?
Apple recommend this because they provide a magic translation for
[file-references embodied by URLs][file-refs], which gives you URLs like so:
file:///.file/id=6571367.15106761
Therefore, if you are not using this feature you are fine. If you have URLs the
correct way to get a `Path` is:
```swift
if let path = Path(url: url) {
/**/
}
```
Our initializer calls `path` on the URL which resolves any reference to an
actual filesystem path, however we also check the URL has a `file` scheme first.
[file-refs]: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
# In defense of our naming scheme
Chainable syntax demands short method names, thus we adopted the naming scheme
of the terminal, which is absolutely not very “Apple” when it comes to how they
design their APIs, however for users of the terminal (which *surely* is most
developers) it is snappy and familiar.
# Installation
SwiftPM:
```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.5.0"))
package.append(
.package(url: "https://github.com/mxcl/Path.swift.git", from: "1.0.0")
)
package.targets.append(
.target(name: "Foo", dependencies: [
.product(name: "Path", package: "Path.swift")
])
)
```
CocoaPods:
```ruby
pod 'Path.swift' ~> '0.5.0'
pod 'Path.swift', '~> 1.0.0'
```
Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Please note
# Naming Conflicts with `SwiftUI.Path`, etc.
We are pre 1.0, thus we can change the API as we like, and we will (to the
pursuit of getting it *right*)! We will tag 1.0 as soon as possible.
### Get push notifications for new releases
https://codebasesaga.com/canopy/
We have a typealias of `PathStruct` you can use instead.
# Alternatives
* [Pathos](https://github.com/dduan/Pathos) by Daniel Duan
* [PathKit](https://github.com/kylef/PathKit) by Kyle Fuller
* [Files](https://github.com/JohnSundell/Files) by John Sundell
* [Utility](https://github.com/apple/swift-package-manager) by Apple
[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/Structs/Path.html
[badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.x-orange.svg
[docs]: https://mxcl.dev/Path.swift/Structs/Path.html
[badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true
[badge-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg
[badge-ci]: https://github.com/mxcl/Path.swift/workflows/Checks/badge.svg
[travis]: https://travis-ci.com/mxcl/Path.swift
[codecov]: https://codecov.io/gh/mxcl/Path.swift
[badge-version]: https://img.shields.io/cocoapods/v/Path.swift.svg?label=version
[cocoapods]: https://cocoapods.org/pods/Path.swift
[Sponsor @mxcl]: https://github.com/sponsors/mxcl

View File

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

View File

@@ -1,35 +1,56 @@
import Foundation
public extension Path {
/// - Note: If file is already locked, does nothing
/// - Note: If file doesnt exist, throws
@discardableResult
func lock() throws -> Path {
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)
public extension Pathish {
//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
}
return self
}
/// - Note: If file isnt locked, does nothing
/// - Note: If file doesnt exist, does nothing
@discardableResult
func unlock() throws -> Path {
var attrs: [FileAttributeKey: Any]
/**
Returns the modification-time of the file.
- Note: If this returns `nil` and the file exists, something is very wrong.
*/
var mtime: Date? {
do {
attrs = try FileManager.default.attributesOfItem(atPath: string)
} catch CocoaError.fileReadNoSuchFile {
return self
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date
} catch {
return nil
}
let b = attrs[.immutable] as? Bool ?? false
if b {
attrs[.immutable] = false
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
/// The type of the entry.
/// - SeeAlso: `Path.EntryType`
@available(*, deprecated, message: "- SeeAlso: Path.type")
var kind: Path.EntryType? {
return type
}
/// The type of the entry.
/// - SeeAlso: `Path.EntryType`
var type: Path.EntryType? {
var buf = stat()
guard lstat(string, &buf) == 0 else {
return nil
}
if buf.st_mode & S_IFMT == S_IFLNK {
return .symlink
} else if buf.st_mode & S_IFMT == S_IFDIR {
return .directory
} else {
return .file
}
return self
}
/**
@@ -40,21 +61,64 @@ public extension Path {
@discardableResult
func chmod(_ octal: Int) throws -> Path {
try FileManager.default.setAttributes([.posixPermissions: octal], ofItemAtPath: string)
return self
return Path(self)
}
//MARK: Filesystem Locking
/**
Applies the macOS filesystem lock attribute.
- Note: If file is already locked, does nothing.
- Note: If file doesnt exist, throws.
- Important: On Linux does nothing.
*/
@discardableResult
func lock() throws -> Path {
#if !os(Linux)
var attrs = try FileManager.default.attributesOfItem(atPath: string)
let b = attrs[.immutable] as? Bool ?? false
if !b {
attrs[.immutable] = true
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
return Path(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.
- Note: If file isnt locked, does nothing.
- Note: If file doesnt exist, does nothing.
- Important: On Linux does nothing.
- SeeAlso: `lock()`
*/
var mtime: Date {
@discardableResult
func unlock() throws -> Path {
#if !os(Linux)
var attrs: [FileAttributeKey: Any]
do {
let attrs = try FileManager.default.attributesOfItem(atPath: string)
return attrs[.modificationDate] as? Date ?? attrs[.creationDate] as? Date ?? Date(timeIntervalSince1970: 0)
} catch {
//TODO log error
return Date(timeIntervalSince1970: 0)
attrs = try FileManager.default.attributesOfItem(atPath: string)
} catch CocoaError.fileReadNoSuchFile {
return Path(self)
}
let b = attrs[.immutable] as? Bool ?? false
if b {
attrs[.immutable] = false
try FileManager.default.setAttributes(attrs, ofItemAtPath: string)
}
#endif
return Path(self)
}
}
/// The `extension` that provides `Kind`.
public extension Path {
/// A filesystem entrys kind, file, directory, symlink etc.
enum EntryType: CaseIterable {
/// The entry is a file.
case file
/// The entry is a symlink.
case symlink
/// The entry is a directory.
case directory
}
}

View File

@@ -1,29 +1,52 @@
import Foundation
/// Provided for relative-path coding. See the instructions in our `README`.
/**
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`.
/**
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("/") {
string = value
} else {
guard let root = decoder.userInfo[.relativePath] as? Path else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
}
} else if let root = decoder.userInfo[.relativePath] as? Path {
string = (root/value).string
} else if let root = decoder.userInfo[.relativePath] as? DynamicPath {
string = (root/value).string
} else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Path cannot decode a relative path if `userInfo[.relativePath]` not set to a Path object."))
}
}
/// - SeeAlso: `CodingUserInfoKey.relativePath`
/// :nodoc:
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let root = encoder.userInfo[.relativePath] as? Path {
try container.encode(relative(to: root))
} else if let root = encoder.userInfo[.relativePath] as? DynamicPath {
try container.encode(relative(to: root))
} else {
try container.encode(string)
}

View File

@@ -1,18 +1,33 @@
import Foundation
extension Path {
/// Returns a `Path` containing ``FileManager.default.currentDirectoryPath`.
public static var cwd: Path {
return Path(string: FileManager.default.currentDirectoryPath)
/// The `extension` that provides static properties that are common directories.
private enum Foo {
//MARK: Common Directories
/// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
static var cwd: DynamicPath {
return .init(string: FileManager.default.currentDirectoryPath)
}
/// Returns a `Path` representing the root path.
public static var root: Path {
return Path(string: "/")
static var root: DynamicPath {
return .init(string: "/")
}
#if swift(>=5.3)
public static func source(for filePath: String = #filePath) -> (file: DynamicPath, directory: DynamicPath) {
let file = DynamicPath(string: filePath)
return (file: file, directory: .init(file.parent))
}
#else
public static func source(for filePath: String = #file) -> (file: DynamicPath, directory: DynamicPath) {
let file = DynamicPath(string: filePath)
return (file: file, directory: .init(file.parent))
}
#endif
/// Returns a `Path` representing the users home directory
public static var home: Path {
static var home: DynamicPath {
let string: String
#if os(macOS)
if #available(OSX 10.12, *) {
@@ -23,41 +38,30 @@ extension Path {
#else
string = NSHomeDirectory()
#endif
return Path(string: string)
return .init(string: string)
}
/// Helper to allow search path and domain mask to be passed in.
private static func path(for searchPath: FileManager.SearchPathDirectory) -> Path {
private static func path(for searchPath: FileManager.SearchPathDirectory) -> DynamicPath {
#if os(Linux)
// 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 }
let foo = { ProcessInfo.processInfo.environment[$0].flatMap(Path.init).map(DynamicPath.init) ?? $1 }
switch searchPath {
case .documentDirectory:
return Path.home/"Documents"
return Path.home.Documents
case .applicationSupportDirectory:
return foo("XDG_DATA_HOME", Path.home/".local/share")
return foo("XDG_DATA_HOME", Path.home[dynamicMember: ".local/share"])
case .cachesDirectory:
return foo("XDG_CACHE_HOME", Path.home/".cache")
return foo("XDG_CACHE_HOME", Path.home[dynamicMember: ".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)
guard let pathString = FileManager.default.urls(for: searchPath, in: .userDomainMask).first?.path else { return defaultUrl(for: searchPath) }
return DynamicPath(string: pathString)
#endif
}
@@ -66,16 +70,16 @@ extension Path {
- 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 {
static var documents: DynamicPath {
return path(for: .documentDirectory)
}
/**
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 {
static var caches: DynamicPath {
return path(for: .cachesDirectory)
}
@@ -84,7 +88,55 @@ extension Path {
- Note: On Linux is `XDG_DATA_HOME`.
- Note: You should create a subdirectory before creating any files.
*/
public static var applicationSupport: Path {
static var applicationSupport: DynamicPath {
return path(for: .applicationSupportDirectory)
}
}
#if !os(Linux)
func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> DynamicPath {
switch searchPath {
case .documentDirectory:
return Path.home.Documents
case .applicationSupportDirectory:
return Path.home.Library[dynamicMember: "Application Support"]
case .cachesDirectory:
return Path.home.Library.Caches
default:
fatalError()
}
}
#endif
/// The `extension` that provides static properties that are common directories.
#if swift(>=5.5)
public extension Pathish where Self == Path {
static var home: DynamicPath { return Foo.home }
static var root: DynamicPath { return Foo.root }
static var cwd: DynamicPath { return Foo.cwd }
static var documents: DynamicPath { return Foo.documents }
static var caches: DynamicPath { return Foo.caches }
static var applicationSupport: DynamicPath { return Foo.applicationSupport }
static func source(for filePath: String = #filePath) -> (file: DynamicPath, directory: DynamicPath) {
return Foo.source(for: filePath)
}
}
#else
public extension Path {
static var home: DynamicPath { return Foo.home }
static var root: DynamicPath { return Foo.root }
static var cwd: DynamicPath { return Foo.cwd }
static var documents: DynamicPath { return Foo.documents }
static var caches: DynamicPath { return Foo.caches }
static var applicationSupport: DynamicPath { return Foo.applicationSupport }
#if swift(>=5.3)
static func source(for filePath: String = #filePath) -> (file: DynamicPath, directory: DynamicPath) {
return Foo.source(for: filePath)
}
#else
static func source(for file: String = #file) -> (file: DynamicPath, directory: DynamicPath) {
return Foo.source(for: file)
}
#endif
}
#endif

View File

@@ -1,55 +1,80 @@
import Foundation
#if os(Linux)
import Glibc
#endif
public extension Pathish {
//MARK: File Management
public extension Path {
/**
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` 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:)`
*/
@discardableResult
func copy(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.isFile, isFile {
func copy<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, let tokind = to.type, tokind != .directory, type != .directory {
try FileManager.default.removeItem(at: to.url)
}
#if os(Linux)
//NOTE doing manually due to inconsistency in Linux Foundation behavior
if !overwrite, to.type != nil {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
try FileManager.default.copyItem(atPath: string, toPath: to.string)
return to
return Path(to)
}
/**
Copies a file into a directory
If the destination does not exist, this function creates the directory first.
// 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").mkpath()).chmod(0o500)
try Path.root.join("bar").copy(into: .home)
// => "/Users/mxcl/bar"
// Create ~/.local/bin, copy `ls` there and make the new copy executable
try Path.root.join("bin/ls").copy(into: Path.home.join(".local/bin").mkdir(.p)).chmod(0o500)
If the destination does not exist, this function creates the directory
(including intermediary directories if necessary) first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory
- Parameter 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(to:overwrite:)`
*/
@discardableResult
func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
func copy<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
if into.type == nil {
try into.mkdir(.p)
}
let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
if overwrite, let kind = rv.type, kind != .directory {
try FileManager.default.removeItem(at: rv.url)
}
#if os(Linux)
#if swift(>=5.1)
// check if fixed
#else
if !overwrite, rv.isFile {
//NOTE doing manually due to inconsistency in Linux Foundation behavior
if !overwrite, rv.type != nil {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
#endif
try FileManager.default.copyItem(at: url, to: rv.url)
return rv
@@ -57,103 +82,176 @@ public extension Path {
/**
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
- SeeAlso: move(into:overwrite:)
- Note: `throws` if `to` is a directory.
- Note: Throws if `overwrite` is `false` yet `to` is *already* identical to
`self` because even though *Path.swifts* policy is to noop if the desired
end result preexists, checking for this condition is too expensive a
trade-off.
- SeeAlso: `move(into:overwrite:)`
*/
@discardableResult
func move(to: Path, overwrite: Bool = false) throws -> Path {
if overwrite, to.exists {
func move<P: Pathish>(to: P, overwrite: Bool = false) throws -> Path {
if overwrite, let kind = to.type, kind != .directory {
try FileManager.default.removeItem(at: to.url)
}
try FileManager.default.moveItem(at: url, to: to.url)
return to
return Path(to)
}
/**
Moves a file into a directory
If the destination does not exist, this function creates the directory first.
try Path.root.join("bar").move(into: .home)
// => "/Users/mxcl/bar"
If the destination does not exist, this function creates the directory
(including intermediary directories if necessary) first.
- Note: `throws` if `into` is a file.
- Parameter into: Destination directory
- Parameter 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:)
- SeeAlso: `move(to:overwrite:)`
*/
@discardableResult
func move(into: Path) throws -> Path {
if !into.exists {
try into.mkpath()
} else if !into.isDirectory {
func move<P: Pathish>(into: P, overwrite: Bool = false) throws -> Path {
switch into.type {
case nil:
try into.mkdir(.p)
fallthrough
case .directory?:
let rv = into/basename()
if overwrite, let rvkind = rv.type, rvkind != .directory {
try FileManager.default.removeItem(at: rv.url)
}
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
case .file?, .symlink?:
throw CocoaError.error(.fileWriteFileExists)
}
let rv = into/basename()
try FileManager.default.moveItem(at: url, to: rv.url)
return rv
}
/// Deletes the path, recursively if a directory.
@inlinable
func delete() throws {
try FileManager.default.removeItem(at: url)
}
/**
Creates an empty file at this path.
- Returns: `self` to allow chaining.
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
- Note: If entry is a symlink, deletes the symlink.
- SeeAlso: `lock()`
*/
@inlinable
func delete() throws {
if type != nil {
try FileManager.default.removeItem(at: url)
}
}
/**
Creates an empty file at this path or if the file exists, updates its modification time.
- Returns: A copy of `self` to allow chaining.
*/
@inlinable
@discardableResult
func touch() throws -> Path {
return try "".write(to: self)
if type == nil {
guard FileManager.default.createFile(atPath: string, contents: nil) else {
throw CocoaError.error(.fileWriteUnknown)
}
} else {
#if os(Linux)
let fd = open(string, O_WRONLY)
defer { close(fd) }
futimens(fd, nil)
#else
try FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: string)
#endif
}
return Path(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: A copy of `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
}
#else
throw error
#endif
}
#endif
return Path(self)
}
/**
Creates the directory at this path.
- Note: Does not create any intermediary directories.
- Returns: `self` to allow chaining.
Renames the file (basename only) at path.
Path.root.foo.bar.rename(to: "baz") // => /foo/baz
- Parameter to: the new basename for the file
- Returns: The renamed path.
*/
@discardableResult
func mkdir() throws -> Path {
try _foo {
try FileManager.default.createDirectory(at: self.url, withIntermediateDirectories: false, attributes: nil)
}
return self
func rename(to newname: String) throws -> Path {
let newpath = parent/newname
try FileManager.default.moveItem(atPath: string, toPath: newpath.string)
return newpath
}
/**
Creates the directory at this path.
- Note: Creates any intermediary directories, if required.
- Returns: `self` to allow chaining.
Creates a symlink of this file at `as`.
- Note: If `self` does not exist, is **not** an error.
*/
@discardableResult
func mkpath() throws -> Path {
try _foo {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
func symlink<P: Pathish>(as: P) throws -> Path {
try FileManager.default.createSymbolicLink(atPath: `as`.string, withDestinationPath: string)
return Path(`as`)
}
/**
Creates a symlink of this file with the same filename in the `into` directory.
- Note: If into does not exist, creates the directory with intermediate directories if necessary.
*/
@discardableResult
func symlink<P: Pathish>(into dir: P) throws -> Path {
switch dir.type {
case nil, .symlink?:
try dir.mkdir(.p)
fallthrough
case .directory?:
let dst = dir/basename()
try FileManager.default.createSymbolicLink(atPath: dst.string, withDestinationPath: string)
return dst
case .file?:
throw CocoaError.error(.fileWriteFileExists)
}
return self
}
}
/// Options for `Path.mkdir(_:)`
public enum MakeDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`.
case p
}

View File

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

View File

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

View File

@@ -1,28 +1,163 @@
import Foundation
#if !os(Linux)
import func Darwin.realpath
let _realpath = Darwin.realpath
#else
import func Glibc.realpath
let _realpath = Glibc.realpath
#endif
public typealias PathStruct = Path
/**
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 filesystem entry 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
However we only provide this support off of the static members like `root` due
to the anti-pattern where Path.swift suddenly feels like Javascript otherwise.
- Note: A `Path` does not necessarily represent an actual filesystem entry.
- SeeAlso: `Pathish` for most methods you will use on `Path` instances.
*/
public struct Path: Equatable, Hashable, Comparable {
/// The underlying filesystem path
public struct Path: Pathish {
/// The normalized string representation of the underlying filesystem path
public let string: String
init(string: String) {
assert(string.first == "/")
assert(string.last != "/" || string == "/")
assert(string.split(separator: "/").contains("..") == false)
self.string = string
}
/**
Returns the filename extension of this path.
- Remark: Implemented via `NSString.pathExtension`.
Creates a new absolute, standardized path.
- Note: Resolves any .. or . components.
- Note: Removes multiple subsequent and trailing occurences of `/`.
- Note: Does *not* resolve any symlinks.
- Note: On macOS, removes an initial component of /private/var/automount, /var/automount, or /private from the path, if the result still indicates an existing file or directory (checked by consulting the file system).
- Returns: The path or `nil` if fed a relative path or a `~foo` string where there is no user `foo`.
*/
@inlinable
public var `extension`: String {
return (string as NSString).pathExtension
public init?<S: StringProtocol>(_ description: S) {
var pathComponents = description.split(separator: "/")
switch description.first {
case "/":
#if os(macOS)
func ifExists(withPrefix prefix: String, removeFirst n: Int) {
assert(prefix.split(separator: "/").count == n)
if description.hasPrefix(prefix), FileManager.default.fileExists(atPath: String(description)) {
pathComponents.removeFirst(n)
}
}
ifExists(withPrefix: "/private/var/automount", removeFirst: 3)
ifExists(withPrefix: "/var/automount", removeFirst: 2)
ifExists(withPrefix: "/private", removeFirst: 1)
#endif
string = join_(prefix: "/", pathComponents: pathComponents)
case "~":
if description == "~" {
string = Path.home.string
return
}
let tilded: String
if description.hasPrefix("~/") {
tilded = Path.home.string
} else {
let username = String(pathComponents[0].dropFirst())
#if os(macOS) || os(Linux)
if #available(OSX 10.12, *) {
guard let url = FileManager.default.homeDirectory(forUser: username) else { return nil }
assert(url.scheme == "file")
tilded = url.path
} else {
guard let dir = NSHomeDirectoryForUser(username) else { return nil }
tilded = dir
}
#else
return nil // there are no usernames on iOS, etc.
#endif
}
pathComponents.remove(at: 0)
string = join_(prefix: tilded, pathComponents: pathComponents)
default:
return nil
}
}
/**
Creates a new absolute, standardized path from the provided file-scheme URL.
- Note: If the URL is not a file URL, returns `nil`.
*/
public init?(url: URL) {
guard url.scheme == "file" else { return nil }
self.init(url.path)
//NOTE: URL cannot be a file-reference url, unlike NSURL, so this always works
}
/**
Creates a new absolute, standardized path from the provided file-scheme URL.
- Note: If the URL is not a file URL, returns `nil`.
- Note: If the URL is a file reference URL, converts it to a POSIX path first.
*/
public init?(url: NSURL) {
guard url.scheme == "file", let path = url.path else { return nil }
self.init(string: path)
// ^^ works even if the url is a file-reference url
}
/// Converts anything that is `Pathish` to a `Path`
public init<P: Pathish>(_ path: P) {
string = path.string
}
}
public extension Pathish {
//MARK: Filesystem Representation
/// Returns a `URL` representing this file path.
var url: URL {
return URL(fileURLWithPath: string)
}
/**
Returns a file-reference URL.
- Note: Only NSURL can be a file-reference-URL, hence we return NSURL.
- SeeAlso: https://developer.apple.com/documentation/foundation/nsurl/1408631-filereferenceurl
- Important: On Linux returns an file scheme NSURL for this path string.
*/
var fileReferenceURL: NSURL? {
#if !os(Linux)
// https://bugs.swift.org/browse/SR-2728
return (url as NSURL).perform(#selector(NSURL.fileReferenceURL))?.takeUnretainedValue() as? NSURL
#else
return NSURL(fileURLWithPath: string)
#endif
}
/**
@@ -33,39 +168,93 @@ public struct Path: Equatable, Hashable, Comparable {
- Note: always returns a valid path, `Path.root.parent` *is* `Path.root`.
*/
public var parent: Path {
return Path(string: (string as NSString).deletingLastPathComponent)
}
/// Returns a `URL` representing this file path.
@inlinable
public var url: URL {
return URL(fileURLWithPath: string)
var parent: Path {
let index = string.lastIndex(of: "/")!
guard index != string.indices.startIndex else { return Path(string: "/") }
let substr = string[string.indices.startIndex..<index]
return Path(string: String(substr))
}
/**
The basename for the provided file, optionally dropping the file extension.
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.
Returns the filename extension of this path.
- Remark: If there is no extension returns "".
- Remark: If the filename ends with any number of ".", returns "".
- Note: We special case eg. `foo.tar.gz`there are a limited number of these specializations, feel free to PR more.
*/
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))
@inlinable
var `extension`: String {
//FIXME efficiency
switch true {
case string.hasSuffix(".tar.gz"):
return "tar.gz"
case string.hasSuffix(".tar.bz"):
return "tar.bz"
case string.hasSuffix(".tar.bz2"):
return "tar.bz2"
case string.hasSuffix(".tar.xz"):
return "tar.xz"
default:
let slash = string.lastIndex(of: "/")!
if let dot = string.lastIndex(of: "."), slash < dot {
let foo = string.index(after: dot)
return String(string[foo...])
} else {
return str.lastPathComponent
return ""
}
}
}
/**
Splits the string representation on the directory separator.
- Important: `NSString.pathComponents` will always return an initial `/` in its array for absolute paths to indicate that the path was absolute, we dont do this because we are *always* absolute paths.
*/
@inlinable
var components: [String] {
return string.split(separator: "/").map(String.init)
}
//MARK:- Pathing
/**
Joins a path and a string to produce a new path.
Path.root.join("a") // => /a
Path.root.join("a/b") // => /a/b
Path.root.join("a").join("b") // => /a/b
Path.root.join("a").join("/b") // => /a/b
- Note: `..` and `.` components are interpreted.
- Note: pathComponent *may* be multiple components.
- Note: symlinks are *not* resolved.
- Parameter pathComponent: The string to join with this path.
- Returns: A new joined path.
- SeeAlso: `Path./(_:_:)`
*/
func join<S>(_ addendum: S) -> Path where S: StringProtocol {
return Path(string: join_(prefix: string, appending: addendum))
}
/**
Joins a path and a string to produce a new path.
Path.root/"a" // => /a
Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Note: `..` and `.` components are interpreted.
- Note: pathComponent *may* be multiple components.
- Note: symlinks are *not* resolved.
- Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path.
- SeeAlso: `join(_:)`
*/
@inlinable
static func /<S>(lhs: Self, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
}
/**
Returns a string representing the relative path to `base`.
@@ -73,7 +262,7 @@ public struct Path: Equatable, Hashable, Comparable {
- 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 {
func relative<P: Pathish>(to base: P) -> String {
// Split the two paths into their components.
// FIXME: The is needs to be optimized to avoid unncessary copying.
let pathComps = (string as NSString).pathComponents
@@ -104,59 +293,143 @@ 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: `/(_:, _:)`
- 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)
func basename(dropExtension: Bool = false) -> String {
var lastPathComponent: Substring {
let slash = string.lastIndex(of: "/")!
let index = string.index(after: slash)
return string[index...]
}
var go: Substring {
if !dropExtension {
return lastPathComponent
} else {
let ext = self.extension
if !ext.isEmpty {
return lastPathComponent.dropLast(ext.count + 1)
} else {
return lastPathComponent
}
}
}
return String(go)
}
/**
If the path represents an actual entry that is a symlink, returns the symlinks
absolute destination.
- Important: This is not exhaustive, the resulting path may still contain a symlink.
- Important: The path will only be different if the last path component is a symlink, any symlinks in prior components are not resolved.
- Note: If file exists but isnt a symlink, returns `self`.
- Note: If symlink destination does not exist, is **not** an error.
*/
func readlink() throws -> Path {
do {
let rv = try FileManager.default.destinationOfSymbolicLink(atPath: string)
return Path(rv) ?? parent/rv
} catch CocoaError.fileReadUnknown {
// file is not symlink, return `self`
assert(exists)
return Path(string: string)
} catch {
#if os(Linux)
// ugh: Swift on Linux
let nsError = error as NSError
if nsError.domain == NSCocoaErrorDomain, nsError.code == CocoaError.fileReadUnknown.rawValue, exists {
return Path(self)
}
#endif
throw error
}
}
/// Recursively resolves symlinks in this path.
func realpath() throws -> Path {
guard let rv = _realpath(string, nil) else { throw CocoaError.error(.fileNoSuchFile) }
defer { free(rv) }
guard let rvv = String(validatingUTF8: rv) else { throw CocoaError.error(.fileReadUnknownStringEncoding) }
// Removing an initial component of /private/var/automount, /var/automount,
// or /private from the path, if the result still indicates an existing file or
// directory (checked by consulting the file system).
// ^^ we do this to not conflict with the results that other Apple APIs give
// which is necessary if we are to have equality checks work reliably
let rvvv = (rvv as NSString).standardizingPath
return Path(string: rvvv)
}
/// Returns the locale-aware sort order for the two paths.
/// :nodoc:
@inlinable
public static func <(lhs: Path, rhs: Path) -> Bool {
static func <(lhs: Self, rhs: Self) -> Bool {
return lhs.string.compare(rhs.string, locale: .current) == .orderedAscending
}
}
/// 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
@inline(__always)
private func join_<S>(prefix: String, appending: S) -> String where S: StringProtocol {
return join_(prefix: prefix, pathComponents: appending.split(separator: "/"))
}
private func join_<S>(prefix: String, pathComponents: S) -> String where S: Sequence, S.Element: StringProtocol {
assert(prefix.first == "/")
var rv = prefix
for component in pathComponents {
assert(!component.contains("/"))
switch component {
case "..":
let start = rv.indices.startIndex
let index = rv.lastIndex(of: "/")!
if start == index {
rv = "/"
} else {
rv = String(rv[start..<index])
}
case ".":
break
default:
if rv == "/" {
rv = "/\(component)"
} else {
rv = "\(rv)/\(component)"
}
}
/// The kind of this entry.
public let kind: Kind
/// The path of this entry.
public let path: Path
}
return rv
}
/// A path that supports arbituary dot notation, eg. `Path.root.usr.bin`
@dynamicMemberLookup
public struct DynamicPath: Pathish {
/// The normalized string representation of the underlying filesystem path
public let string: String
init(string: String) {
assert(string.hasPrefix("/"))
self.string = string
}
/// Converts a `Path` to a `DynamicPath`
public init<P: Pathish>(_ path: P) {
string = path.string
}
/// :nodoc:
public subscript(dynamicMember addendum: String) -> DynamicPath {
//NOTE its possible for the string to be anything if we are invoked via
// explicit subscript thus we use our fully sanitized `join` function
return DynamicPath(string: join_(prefix: string, appending: addendum))
}
}
/**
Joins a path and a string to produce a new path.
Path.root/"a" // => /a
Path.root/"a/b" // => /a/b
Path.root/"a"/"b" // => /a/b
Path.root/"a"/"/b" // => /a/b
- Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path.
- SeeAlso: `Path.join(_:)`
*/
@inlinable
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {
return lhs.join(rhs)
}

View File

@@ -1,25 +1,21 @@
import Foundation
#if os(Linux)
import func Glibc.access
#else
import Darwin
#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)
}
/// 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 deletable by the current user.
var isDeletable: Bool {
return FileManager.default.isDeletableFile(atPath: string)
}
public extension Pathish {
/// 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
//MARK: Filesystem Properties
/**
- Returns: `true` if the path represents an actual filesystem entry.
- Note: If `self` is a symlink the return value represents the destination, thus if the destination doesnt exist this returns `false` even if the symlink exists.
- Note: To determine if a symlink exists, use `.type`.
*/
var exists: Bool {
return FileManager.default.fileExists(atPath: string)
}
/// Returns true if the path represents an actual filesystem entry that is *not* a directory.
@@ -28,13 +24,47 @@ 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
}
}
/// Returns `true` if the file is a symbolic-link (symlink).
var isSymlink: Bool {
var sbuf = stat()
lstat(string, &sbuf)
return (sbuf.st_mode & S_IFMT) == S_IFLNK
}
}

6
Sources/Pathish.swift Normal file
View File

@@ -0,0 +1,6 @@
/// A type that represents a filesystem path, if you conform your type
/// to `Pathish` it is your responsibility to ensure the string is correctly normalized
public protocol Pathish: Hashable, Comparable {
/// The normalized string representation of the underlying filesystem path
var string: String { get }
}

View File

@@ -0,0 +1,251 @@
import XCTest
import Path
extension PathTests {
func testFindMaxDepth1() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.touch()
try tmpdir.c.mkdir().join("e").touch()
do {
let finder = tmpdir.find().depth(max: 1)
XCTAssertEqual(finder.depth, 1...1)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(Set(finder), Set([tmpdir.a, tmpdir.b, tmpdir.c].map(Path.init)))
#endif
}
do {
let finder = tmpdir.find().depth(max: 0)
XCTAssertEqual(finder.depth, 0...0)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(Set(finder), Set())
#endif
}
}
}
func testFindMaxDepth2() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
do {
let finder = tmpdir.find().depth(max: 2)
XCTAssertEqual(finder.depth, 1...2)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c].map(Path.init)))
#endif
}
do {
let finder = tmpdir.find().depth(max: 3)
XCTAssertEqual(finder.depth, 1...3)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e].map(Path.init)))
#endif
}
}
}
func testFindMinDepth() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let finder = tmpdir.find().depth(min: 2)
XCTAssertEqual(finder.depth, 2...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindDepth0() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let finder = tmpdir.find().depth(min: 0)
XCTAssertEqual(finder.depth, 0...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
do {
// this should work, even though its weird
let finder = tmpdir.find().depth(min: -1)
XCTAssertEqual(finder.depth, 0...Int.max)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.c, tmpdir.b.d, tmpdir.b.d.e, tmpdir.b.d.f, tmpdir.b.d.f.g].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindDepthRange() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
do {
let range = 2...3
let finder = tmpdir.find().depth(range)
XCTAssertEqual(finder.depth, range)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e, tmpdir.b.d.f].map(Path.init)),
relativeTo: tmpdir)
#endif
}
do {
let range = 2..<4
let finder = tmpdir.find().depth(range)
XCTAssertEqual(finder.depth, 2...3)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(finder),
Set([tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e, tmpdir.b.d.f].map(Path.init)),
relativeTo: tmpdir)
#endif
}
}
}
func testFindHidden() throws {
try Path.mktemp { tmpdir in
let dotFoo = try tmpdir.join(".foo.txt").touch()
let tmpDotA = try tmpdir.join(".a").mkdir()
let tmpDotAFoo = try tmpdir.join(".a").join("foo.txt").touch()
let tmpB = try tmpdir.b.mkdir()
let tmpBFoo = try tmpdir.b.join("foo.txt").touch()
XCTAssertEqual(
Set(tmpdir.find().hidden(true)),
Set([dotFoo,tmpDotA,tmpDotAFoo,tmpB,tmpBFoo]),
relativeTo: tmpdir)
#if !os(Linux) || swift(>=5)
XCTAssertEqual(
Set(tmpdir.find().hidden(false)),
Set([tmpB,tmpBFoo]),
relativeTo: tmpdir)
#endif
}
}
func testFindExtension() throws {
try Path.mktemp { tmpdir in
try tmpdir.join("foo.json").touch()
try tmpdir.join("bar.txt").touch()
XCTAssertEqual(
Set(tmpdir.find().extension("json")),
[tmpdir.join("foo.json")])
XCTAssertEqual(
Set(tmpdir.find().extension("txt").extension("json")),
[tmpdir.join("foo.json"), tmpdir.join("bar.txt")])
}
}
//NOTE this is how iterators work, so we have a test to validate that behavior
func testFindCallingExecuteTwice() throws {
try Path.mktemp { tmpdir in
try tmpdir.join("foo.json").touch()
try tmpdir.join("bar.txt").touch()
let finder = tmpdir.find()
XCTAssertEqual(finder.map{ $0 }.count, 2)
XCTAssertEqual(finder.map{ $0 }.count, 0)
}
}
func testFindExecute() throws {
try Path.mktemp { tmpdir in
try tmpdir.a.touch()
try tmpdir.b.mkdir().join("c").touch()
try tmpdir.b.d.mkdir().join("e").touch()
try tmpdir.b.d.f.mkdir().join("g").touch()
#if !os(Linux) || swift(>=5)
do {
var rv = Set<Path>()
tmpdir.find().execute {
switch $0 {
case Path(tmpdir.b.d): return .skip
default:
rv.insert($0)
return .continue
}
}
XCTAssertEqual(rv, Set([tmpdir.a, tmpdir.b, tmpdir.b.c].map(Path.init)))
}
#endif
do {
var x = 0
tmpdir.find().execute { _ in
x += 1
return .abort
}
XCTAssertEqual(x, 1)
}
}
}
func testFindTypes() throws {
try Path.mktemp { tmpdir in
try tmpdir.foo.mkdir()
try tmpdir.bar.touch()
XCTAssertEqual(
Set(tmpdir.find().type(.file)),
[tmpdir.join("bar")])
XCTAssertEqual(
Set(tmpdir.find().type(.directory)),
[tmpdir.join("foo")])
XCTAssertEqual(
Set(tmpdir.find().type(.file).type(.directory)),
Set(["foo", "bar"].map(tmpdir.join)))
}
}
func testLsOnNonexistentDirectoryReturnsEmptyArray() throws {
try Path.mktemp { tmpdir in
XCTAssertEqual(tmpdir.a.ls(), [])
}
}
func testFindOnNonexistentDirectoryHasNoContent() throws {
try Path.mktemp { tmpdir in
XCTAssertNil(tmpdir.a.find().next())
}
}
}

View File

@@ -1,7 +1,22 @@
@testable import Path
import func XCTest.XCTAssertEqual
import Foundation
import XCTest
import Path
extension PathStruct {
var foo: Int { fatalError()}
}
class PathTests: XCTestCase {
func testNewStuff() {
#if swift(>=5.5)
func foo<P: Pathish>(_ path: P) {}
foo(.home)
foo(.root)
#endif
}
func testConcatenation() {
XCTAssertEqual((Path.root/"bar").string, "/bar")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
@@ -9,31 +24,40 @@ class PathTests: XCTestCase {
XCTAssertEqual((Path.root/"///bar").string, "/bar")
XCTAssertEqual((Path.root/"foo///bar////").string, "/foo/bar")
XCTAssertEqual((Path.root/"foo"/"/bar").string, "/foo/bar")
XCTAssertEqual(Path.root.foo.bar.join(".."), Path.root.foo)
XCTAssertEqual(Path.root.foo.bar.join("."), Path.root.foo.bar)
XCTAssertEqual(Path.root.foo.bar.join("../baz"), Path.root.foo.baz)
}
func testEnumeration() throws {
let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch()
try tmpdir.join("b").touch()
try tmpdir.join("b.swift").touch()
try tmpdir.join("c").touch()
try tmpdir.join(".d").mkdir().join("e").touch()
var paths = Set<String>()
let lsrv = tmpdir.ls(.a)
var dirs = 0
for entry in try tmpdir.ls() {
if entry.kind == .directory {
for path in lsrv {
if path.isDirectory {
dirs += 1
}
paths.insert(entry.path.basename())
paths.insert(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.filter{ $0.extension == "swift" }.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(["c"], Set(lsrv.files.filter{ $0.extension == "" }.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"])
}
func testEnumerationSkippingHiddenFiles() throws {
#if !os(Linux)
let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch()
@@ -43,25 +67,33 @@ class PathTests: XCTestCase {
var paths = Set<String>()
var dirs = 0
for entry in try tmpdir.ls(includeHiddenFiles: false) {
if entry.kind == .directory {
for path in tmpdir.ls() {
if path.isDirectory {
dirs += 1
}
paths.insert(entry.path.basename())
paths.insert(path.basename())
}
XCTAssertEqual(dirs, 1)
XCTAssertEqual(paths, ["a", "b", "c"])
#endif
}
func testRelativeTo() {
XCTAssertEqual((Path.root/"tmp/foo").relative(to: .root/"tmp"), "foo")
XCTAssertEqual((Path.root/"tmp/foo/bar").relative(to: .root/"tmp/baz"), "../foo/bar")
XCTAssertEqual((Path.root.tmp.foo).relative(to: Path.root/"tmp"), "foo")
XCTAssertEqual((Path.root.tmp.foo.bar).relative(to: Path.root/"tmp/baz"), "../foo/bar")
}
func testExists() {
func testExists() throws {
XCTAssert(Path.root.exists)
XCTAssert((Path.root/"bin").exists)
try Path.mktemp { tmpdir in
XCTAssertTrue(tmpdir.exists)
XCTAssertFalse(try tmpdir.bar.symlink(as: tmpdir.foo).exists)
XCTAssertTrue(tmpdir.foo.type == .symlink)
XCTAssertTrue(try tmpdir.bar.touch().symlink(as: tmpdir.baz).exists)
XCTAssertTrue(tmpdir.bar.type == .file)
XCTAssertTrue(tmpdir.type == .directory)
}
}
func testIsDirectory() {
@@ -69,8 +101,24 @@ class PathTests: XCTestCase {
XCTAssert((Path.root/"bin").isDirectory)
}
func testExtension() {
for prefix in [Path.root, Path.root.foo, Path.root.foo.bar] {
XCTAssertEqual(prefix.join("a.swift").extension, "swift")
XCTAssertEqual(prefix.join("a").extension, "")
XCTAssertEqual(prefix.join("a.").extension, "")
XCTAssertEqual(prefix.join("a..").extension, "")
XCTAssertEqual(prefix.join("a..swift").extension, "swift")
XCTAssertEqual(prefix.join("a..swift.").extension, "")
XCTAssertEqual(prefix.join("a.tar.gz").extension, "tar.gz")
XCTAssertEqual(prefix.join("a.tar.bz2").extension, "tar.bz2")
XCTAssertEqual(prefix.join("a.tar.xz").extension, "tar.xz")
XCTAssertEqual(prefix.join("a..tar.bz").extension, "tar.bz")
XCTAssertEqual(prefix.join("a..tar..xz").extension, "xz")
}
}
func testMktemp() throws {
var path: Path!
var path: DynamicPath!
try Path.mktemp {
path = $0
XCTAssert(path.isDirectory)
@@ -83,39 +131,49 @@ 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)
}
}
}
func testBasename() {
XCTAssertEqual(Path.root.join("foo.bar").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo").basename(dropExtension: true), "foo")
XCTAssertEqual(Path.root.join("foo.").basename(dropExtension: true), "foo.")
XCTAssertEqual(Path.root.join("foo.bar.baz").basename(dropExtension: true), "foo.bar")
for prefix in [Path.root, Path.root.foo, Path.root.foo.bar] {
XCTAssertEqual(prefix.join("foo.bar").basename(dropExtension: true), "foo")
XCTAssertEqual(prefix.join("foo").basename(dropExtension: true), "foo")
XCTAssertEqual(prefix.join("foo.").basename(dropExtension: true), "foo.")
XCTAssertEqual(prefix.join("foo.bar.baz").basename(dropExtension: true), "foo.bar")
}
}
func testCodable() throws {
let input = [Path.root/"bar"]
let input = [Path.root.foo, Path.root.foo.bar, Path.root].map(Path.init)
XCTAssertEqual(try JSONDecoder().decode([Path].self, from: try JSONEncoder().encode(input)), input)
}
func testRelativePathCodable() throws {
let root = Path.root/"bar"
let root = Path.root.foo
let input = [
root/"foo"
]
Path.root,
root,
root.bar
].map(Path.init)
let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root
let data = try encoder.encode(input)
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["foo"])
func test<P: Pathish>(relativePath: P, line: UInt = #line) throws {
encoder.userInfo[.relativePath] = relativePath
let data = try encoder.encode(input)
let decoder = JSONDecoder()
XCTAssertThrowsError(try decoder.decode([Path].self, from: data))
decoder.userInfo[.relativePath] = root
XCTAssertEqual(try decoder.decode([Path].self, from: data), input)
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"], line: line)
let decoder = JSONDecoder()
XCTAssertThrowsError(try decoder.decode([Path].self, from: data), line: line)
decoder.userInfo[.relativePath] = relativePath
XCTAssertEqual(try decoder.decode([Path].self, from: data), input, line: line)
}
try test(relativePath: root) // DynamicPath
try test(relativePath: Path(root)) // Path
}
func testJoin() {
@@ -130,8 +188,16 @@ class PathTests: XCTestCase {
XCTAssertEqual(prefix/b/c, Path("/Users/mxcl/b/c"))
XCTAssertEqual(Path.root/"~b", Path("/~b"))
XCTAssertEqual(Path.root/"~/b", Path("/~/b"))
XCTAssertEqual(Path("~/foo"), Path.home/"foo")
XCTAssertEqual(Path("~"), Path.home)
XCTAssertEqual(Path("~/"), Path.home)
XCTAssertEqual(Path("~///"), Path.home)
XCTAssertEqual(Path("/~///"), Path.root/"~")
XCTAssertNil(Path("~foo"))
XCTAssertNil(Path("~foo/bar"))
XCTAssertEqual(Path("~\(NSUserName())"), Path.home)
XCTAssertEqual(Path.root/"a/foo"/"../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"/../bar", Path.root/"a/bar")
@@ -139,14 +205,493 @@ class PathTests: XCTestCase {
XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar")
}
func testCopyInto() throws {
func testParent() {
XCTAssertEqual(Path("/root/boot")!.parent.string, "/root")
XCTAssertEqual(Path("/root/boot")!.parent.parent.string, "/")
XCTAssertEqual(Path("/root/boot")!.parent.parent.parent.string, "/")
XCTAssertEqual(Path("/root")!.parent.string, "/")
XCTAssertEqual(Path("/root")!.parent.parent.string, "/")
}
func testDynamicMember() {
XCTAssertEqual(Path.root.Documents, Path.root/"Documents")
let a = Path.home.foo
XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
// verify use of the dynamic-member-subscript works according to our rules
XCTAssertEqual(Path.home[dynamicMember: "../~foo"].string, Path(Path.home).parent.join("~foo").string)
}
func testCopyTo() throws {
try Path.mktemp { root in
let bar = try root.join("bar").touch()
try Path.mktemp { root in
try root.join("bar").touch()
XCTAssertThrowsError(try bar.copy(into: root))
try bar.copy(into: root, overwrite: true)
try root.foo.touch().copy(to: root.bar)
XCTAssert(root.foo.isFile)
XCTAssert(root.bar.isFile)
XCTAssertThrowsError(try root.foo.copy(to: root.bar))
try root.foo.copy(to: root.bar, overwrite: true)
}
}
func testCopyToExistingDirectoryFails() throws {
// test copy errors if directory exists at destination, even with overwrite
try Path.mktemp { root in
try root.foo.touch()
XCTAssert(root.foo.isFile)
XCTAssertThrowsError(try root.foo.copy(to: root.bar.mkdir()))
XCTAssertThrowsError(try root.foo.copy(to: root.bar, overwrite: true))
}
}
func testCopyInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.copy(into: root2))
try bar1.copy(into: root2, overwrite: true)
XCTAssertTrue(bar1.exists)
XCTAssertTrue(bar2.exists)
}
// test creates intermediary directories
try bar1.copy(into: root1.create.directories)
// test doesnt replace file if copy into a file
let d = try root1.fuz.touch()
XCTAssertThrowsError(try root1.baz.touch().copy(into: d))
XCTAssert(d.isFile)
XCTAssert(root1.baz.isFile)
}
}
func testMoveTo() throws {
try Path.mktemp { tmpdir in
try tmpdir.foo.touch().move(to: tmpdir.bar)
XCTAssertFalse(tmpdir.foo.exists)
XCTAssert(tmpdir.bar.isFile)
XCTAssertThrowsError(try tmpdir.foo.touch().move(to: tmpdir.bar))
try tmpdir.foo.move(to: tmpdir.bar, overwrite: true)
}
// test move errors if directory exists at destination, even with overwrite
try Path.mktemp { root in
try root.foo.touch()
XCTAssert(root.foo.isFile)
XCTAssertThrowsError(try root.foo.move(to: root.bar.mkdir()))
XCTAssertThrowsError(try root.foo.move(to: root.bar, overwrite: true))
}
}
func testMoveInto() throws {
try Path.mktemp { root1 in
let bar1 = try root1.join("bar").touch()
try Path.mktemp { root2 in
let bar2 = try root2.join("bar").touch()
XCTAssertThrowsError(try bar1.move(into: root2))
try bar1.move(into: root2, overwrite: true)
XCTAssertFalse(bar1.exists)
XCTAssertTrue(bar2.exists)
}
// test creates intermediary directories
try root1.baz.touch().move(into: root1.create.directories)
XCTAssertFalse(root1.baz.exists)
XCTAssert(root1.create.directories.baz.isFile)
// test doesnt replace file if move into a file
let d = try root1.fuz.touch()
XCTAssertThrowsError(try root1.baz.touch().move(into: d))
XCTAssert(d.isFile)
XCTAssert(root1.baz.isFile)
}
}
func testRename() throws {
try Path.mktemp { root in
do {
let file = try root.bar.touch()
let foo = try file.rename(to: "foo")
XCTAssertFalse(file.exists)
XCTAssertTrue(foo.isFile)
}
do {
let file = try root.bar.touch()
XCTAssertThrowsError(try file.rename(to: "foo"))
}
}
}
func testCommonDirectories() {
XCTAssertEqual(Path.root.string, "/")
XCTAssertEqual(Path.home.string, NSHomeDirectory())
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents")
#if swift(>=5.3)
let filePath = Path(#filePath)!
#else
let filePath = Path(#file)!
#endif
XCTAssertEqual(Path.source().file, filePath)
XCTAssertEqual(Path.source().directory, filePath.parent)
#if !os(Linux)
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
XCTAssertEqual(Path.applicationSupport.string, NSHomeDirectory() + "/Library/Application Support")
_ = defaultUrl(for: .documentDirectory)
_ = defaultUrl(for: .cachesDirectory)
_ = defaultUrl(for: .applicationSupportDirectory)
#endif
}
func testStringConvertibles() {
XCTAssertEqual(Path.root.description, "/")
XCTAssertEqual(Path.root.debugDescription, "Path(/)")
XCTAssertEqual(Path(Path.root).description, "/")
XCTAssertEqual(Path(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 now1 = Date().timeIntervalSince1970.rounded(.down)
sleep(1)
let foo = try tmpdir.foo.touch()
#if !os(Linux)
XCTAssertGreaterThan(foo.ctime?.timeIntervalSince1970.rounded(.down) ?? 0, now1) //FIXME flakey
#endif
XCTAssertGreaterThan(foo.mtime?.timeIntervalSince1970.rounded(.down) ?? 0, now1) //FIXME flakey
XCTAssertNil(tmpdir.void.mtime)
XCTAssertNil(tmpdir.void.ctime)
}
}
func testDelete() throws {
try Path.mktemp { tmpdir in
try tmpdir.bar1.delete()
try tmpdir.bar2.touch().delete()
try tmpdir.bar3.touch().chmod(0o000).delete()
#if !os(Linux)
XCTAssertThrowsError(try tmpdir.bar3.touch().lock().delete())
#endif
// regression test: can delete a symlink that points to a non-existent file
let bar5 = try tmpdir.bar4.symlink(as: tmpdir.bar5)
XCTAssertEqual(bar5.type, .symlink)
XCTAssertFalse(bar5.exists)
XCTAssertNoThrow(try bar5.delete())
XCTAssertEqual(bar5.type, nil)
// test that deleting a symlink *only* deletes the symlink
let bar7 = try tmpdir.bar6.touch().symlink(as: tmpdir.bar7)
XCTAssertEqual(bar7.type, .symlink)
XCTAssertTrue(bar7.exists)
XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.type, nil)
XCTAssertEqual(tmpdir.bar6.type, .file)
// for code-coverage
XCTAssertEqual(tmpdir.bar6.kind, .file)
}
}
func testRelativeCodable() throws {
let path = 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(Path.documents.foo)])
XCTAssertThrowsError(try JSONDecoder().decode([Path].self, from: data))
}
func testBundleExtensions() throws {
try Path.mktemp { tmpdir -> Void in
guard let bndl = Bundle(path: tmpdir.string) else {
return XCTFail("Couldnt make Bundle for \(tmpdir)")
}
XCTAssertEqual(bndl.path, tmpdir)
XCTAssertEqual(bndl.sharedFrameworks, tmpdir.SharedFrameworks)
XCTAssertEqual(bndl.privateFrameworks, tmpdir.Frameworks)
XCTAssertEqual(bndl.resources, tmpdir)
XCTAssertNil(bndl.path(forResource: "foo", ofType: "bar"))
XCTAssertNil(bndl.executable)
#if os(macOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.Contents.Resources)
#elseif os(tvOS) || os(iOS) || os(watchOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir)
#else
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.lib)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.share)
#endif
}
}
func testDataExtensions() throws {
let data = try Data(contentsOf: Path(#file)!)
try Path.mktemp { tmpdir in
_ = try data.write(to: tmpdir.foo)
_ = try data.write(to: tmpdir.foo, atomically: true)
}
}
func testStringExtensions() throws {
let string = try String(contentsOf: Path(#file)!)
try Path.mktemp { tmpdir in
_ = try string.write(to: tmpdir.foo)
}
}
func testFileHandleExtensions() throws {
_ = try FileHandle(forReadingAt: Path(#file)!)
_ = try FileHandle(forWritingAt: Path(#file)!)
_ = try FileHandle(forUpdatingAt: Path(#file)!)
}
func testSort() {
XCTAssertEqual([Path.root.a, Path.root.c, Path.root.b].sorted(), [Path.root.a, Path.root.b, Path.root.c])
}
func testLock() throws {
#if !os(Linux)
try Path.mktemp { tmpdir in
let bar = try tmpdir.bar.touch()
try bar.lock()
XCTAssertThrowsError(try bar.touch())
try bar.unlock()
try bar.touch()
// a non existant file is already unlocked
try tmpdir.nonExit.unlock()
}
#endif
}
func testTouchThrowsIfCannotWrite() throws {
try Path.mktemp { tmpdir in
print(try FileManager.default.attributesOfItem(atPath: tmpdir.string)[.posixPermissions])
//FIXME fails in Docker image (only)
try tmpdir.chmod(0o000)
let attrs = try FileManager.default.attributesOfItem(atPath: tmpdir.string)
XCTAssertEqual(attrs[.posixPermissions] as? Int, 0)
print(attrs[.posixPermissions])
XCTAssertThrowsError(try tmpdir.bar.touch())
XCTAssertFalse(tmpdir.bar.exists)
}
}
func testSymlinkFunctions() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssert(bar.isSymlink)
XCTAssertEqual(try bar.readlink(), foo)
}
try Path.mktemp { tmpdir in
let foo1 = try tmpdir.foo.touch()
let foo2 = try foo1.symlink(into: tmpdir.bar)
XCTAssert(foo2.isSymlink)
XCTAssert(tmpdir.bar.isDirectory)
XCTAssertEqual(try foo2.readlink(), foo1)
// cannot symlink into when `into` is an existing entry that is not a directory
let baz = try tmpdir.baz.touch()
XCTAssertThrowsError(try foo1.symlink(into: baz))
}
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try tmpdir.bar.mkdir()
XCTAssertThrowsError(try foo.symlink(as: bar))
XCTAssert(try foo.symlink(as: bar/"foo").isSymlink)
}
}
func testReadlinkOnRelativeSymlink() throws {
//TODO how to test on iOS etc.?
#if os(macOS) || os(Linux)
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.mkdir()
let bar = try tmpdir.bar.touch()
let task = Process()
task.currentDirectoryPath = foo.string
task.launchPath = "/bin/ln"
task.arguments = ["-s", "../bar", "baz"]
task.launch()
task.waitUntilExit()
XCTAssertEqual(task.terminationStatus, 0)
XCTAssert(tmpdir.foo.baz.isSymlink)
XCTAssertEqual(try FileManager.default.destinationOfSymbolicLink(atPath: tmpdir.foo.baz.string), "../bar")
XCTAssertEqual(try tmpdir.foo.baz.readlink(), bar)
}
#endif
}
func testReadlinkOnFileReturnsSelf() throws {
try Path.mktemp { tmpdir in
XCTAssertEqual(try tmpdir.foo.touch(), tmpdir.foo)
XCTAssertEqual(try tmpdir.foo.readlink(), tmpdir.foo)
}
}
func testReadlinkOnNonExistantFileThrows() throws {
try Path.mktemp { tmpdir in
XCTAssertThrowsError(try tmpdir.bar.readlink())
}
}
func testReadlinkWhereLinkDestinationDoesNotExist() throws {
try Path.mktemp { tmpdir in
let bar = try tmpdir.foo.symlink(as: tmpdir.bar)
XCTAssertEqual(try bar.readlink(), tmpdir.foo)
}
}
func testNoUndesiredSymlinkResolution() throws {
// this test because NSString.standardizingPath will resolve symlinks
// if the path you give it contains .. and the result is an actual entry
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.mkdir()
try foo.join("bar").mkdir().join("fuz").touch()
let baz = DynamicPath(try foo.symlink(as: tmpdir.baz))
XCTAssert(baz.isSymlink)
XCTAssert(baz.bar.isDirectory)
XCTAssertEqual(baz.bar.join("..").string, "\(tmpdir)/baz")
XCTAssertEqual(Path("\(tmpdir)/baz/bar/..")?.string, "\(tmpdir)/baz")
}
}
func testRealpath() throws {
try Path.mktemp { tmpdir in
let b = try tmpdir.a.b.mkdir(.p)
let c = try tmpdir.a.c.touch()
let e = try c.symlink(as: b/"e")
let f = try e.symlink(as: tmpdir.f)
XCTAssertEqual(try f.readlink(), e)
XCTAssertEqual(try f.realpath(), c)
}
try Path.mktemp { tmpdir in
XCTAssertThrowsError(try tmpdir.foo.realpath())
}
}
func testFileReference() throws {
let ref = Path.home.fileReferenceURL
#if !os(Linux)
XCTAssertTrue(ref?.isFileReferenceURL() ?? false)
#endif
XCTAssertEqual(ref?.path, Path.home.string)
}
func testURLInitializer() throws {
XCTAssertEqual(Path(url: Path.home.url), Path.home)
XCTAssertEqual(Path.home.fileReferenceURL.flatMap(Path.init), Path.home)
XCTAssertNil(Path(url: URL(string: "https://foo.com")!))
XCTAssertNil(Path(url: NSURL(string: "https://foo.com")!))
}
func testInitializerForRelativePath() throws {
XCTAssertNil(Path("foo"))
XCTAssertNil(Path("../foo"))
XCTAssertNil(Path("./foo"))
}
func testPathComponents() throws {
XCTAssertEqual(Path.root.foo.bar.components, ["foo", "bar"])
XCTAssertEqual(Path.root.components, [])
}
func testFlatMap() throws {
// testing compile works
let foo: String? = "/a"
_ = foo.flatMap(Path.init)
let bar: Substring? = "/a"
_ = bar.flatMap(Path.init)
let baz: String.SubSequence? = "/a/b:1".split(separator: ":").first
_ = baz.flatMap(Path.init)
}
func testKind() throws {
try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let bar = try foo.symlink(as: tmpdir.bar)
XCTAssertEqual(tmpdir.type, .directory)
XCTAssertEqual(foo.type, .file)
XCTAssertEqual(bar.type, .symlink)
}
}
func testOptionalInitializer() throws {
XCTAssertNil(Path(""))
XCTAssertNil(Path("./foo"))
XCTAssertEqual(Path("/foo"), Path.root.foo)
}
}

View File

@@ -3,7 +3,7 @@ import Foundation
class TemporaryDirectory {
let url: URL
var path: Path { return Path(string: url.path) }
var path: DynamicPath { return DynamicPath(Path(string: url.path)) }
/**
Creates a new temporary directory.
@@ -42,12 +42,16 @@ class TemporaryDirectory {
}
deinit {
_ = try? FileManager.default.removeItem(at: url)
do {
try path.chmod(0o777).delete()
} catch {
//TODO log
}
}
}
extension Path {
static func mktemp<T>(body: (Path) throws -> T) throws -> T {
static func mktemp<T>(body: (DynamicPath) throws -> T) throws -> T {
let tmp = try TemporaryDirectory()
return try body(tmp.path)
}

View File

@@ -1,27 +1,78 @@
#if !canImport(ObjectiveC)
import XCTest
extension PathTests {
static let __allTests = [
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__PathTests = [
("testBasename", testBasename),
("testBundleExtensions", testBundleExtensions),
("testCodable", testCodable),
("testCommonDirectories", testCommonDirectories),
("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto),
("testCopyTo", testCopyTo),
("testCopyToExistingDirectoryFails", testCopyToExistingDirectoryFails),
("testDataExtensions", testDataExtensions),
("testDelete", testDelete),
("testDynamicMember", testDynamicMember),
("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists),
("testExtension", testExtension),
("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes),
("testFindCallingExecuteTwice", testFindCallingExecuteTwice),
("testFindDepth0", testFindDepth0),
("testFindDepthRange", testFindDepthRange),
("testFindExecute", testFindExecute),
("testFindExtension", testFindExtension),
("testFindHidden", testFindHidden),
("testFindMaxDepth1", testFindMaxDepth1),
("testFindMaxDepth2", testFindMaxDepth2),
("testFindMinDepth", testFindMinDepth),
("testFindOnNonexistentDirectoryHasNoContent", testFindOnNonexistentDirectoryHasNoContent),
("testFindTypes", testFindTypes),
("testFlatMap", testFlatMap),
("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory),
("testJoin", testJoin),
("testKind", testKind),
("testLock", testLock),
("testLsOnNonexistentDirectoryReturnsEmptyArray", testLsOnNonexistentDirectoryReturnsEmptyArray),
("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo),
("testNewStuff", testNewStuff),
("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution),
("testOptionalInitializer", testOptionalInitializer),
("testParent", testParent),
("testPathComponents", testPathComponents),
("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf),
("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows),
("testReadlinkOnRelativeSymlink", testReadlinkOnRelativeSymlink),
("testReadlinkWhereLinkDestinationDoesNotExist", testReadlinkWhereLinkDestinationDoesNotExist),
("testRealpath", testRealpath),
("testRelativeCodable", testRelativeCodable),
("testRelativePathCodable", testRelativePathCodable),
("testRelativeTo", testRelativeTo),
("testRename", testRename),
("testSort", testSort),
("testStringConvertibles", testStringConvertibles),
("testStringExtensions", testStringExtensions),
("testSymlinkFunctions", testSymlinkFunctions),
("testTimes", testTimes),
("testTouchThrowsIfCannotWrite", testTouchThrowsIfCannotWrite),
("testURLInitializer", testURLInitializer),
]
}
#if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(PathTests.__allTests),
testCase(PathTests.__allTests__PathTests),
]
}
#endif

43
Tests/PathTests/etc.swift Normal file
View File

@@ -0,0 +1,43 @@
import XCTest
import Path
#if swift(>=5.3)
func XCTAssertEqual<P: Pathish>(_ set1: Set<Path>, _ set2: Set<Path>, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line, relativeTo: P) {
logic(set1, set2, relativeTo: relativeTo) {
XCTFail($0, file: file, line: line)
}
}
#else
func XCTAssertEqual<P: Pathish>(_ set1: Set<Path>, _ set2: Set<Path>, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line, relativeTo: P) {
logic(set1, set2, relativeTo: relativeTo) {
XCTFail($0, file: file, line: line)
}
}
#endif
private func logic<P: Pathish>(_ set1: Set<Path>, _ set2: Set<Path>, relativeTo: P, fail: (String) -> Void) {
if set1 != set2 {
let cvt: (Path) -> String = { $0.relative(to: relativeTo) }
let out1 = set1.map(cvt).sorted()
let out2 = set2.map(cvt).sorted()
fail("Set(\(out1)) is not equal to Set(\(out2))")
}
}
#if swift(>=5.3)
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P, _ q: Q, file: StaticString = #filePath, line: UInt = #line) {
XCTAssertEqual(p.string, q.string, file: file, line: line)
}
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P?, _ q: Q?, file: StaticString = #filePath, line: UInt = #line) {
XCTAssertEqual(p?.string, q?.string, file: file, line: line)
}
#else
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P, _ q: Q, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(p.string, q.string, file: file, line: line)
}
func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P?, _ q: Q?, file: StaticString = #file, line: UInt = #line) {
XCTAssertEqual(p?.string, q?.string, file: file, line: line)
}
#endif