Compare commits

..

49 Commits

Author SHA1 Message Date
T. R. Bernstein
5d164c34f3 Adapt for new repository
Some checks failed
CI / verify-linuxmain (push) Has been cancelled
CI / apple (macos-10.15, iOS) (push) Has been cancelled
CI / apple (macos-10.15, macOS) (push) Has been cancelled
CI / apple (macos-10.15, tvOS) (push) Has been cancelled
CI / apple (macos-10.15, watchOS) (push) Has been cancelled
CI / apple (macos-11, iOS) (push) Has been cancelled
CI / apple (macos-11, macOS) (push) Has been cancelled
CI / apple (macos-11, tvOS) (push) Has been cancelled
CI / apple (macos-11, watchOS) (push) Has been cancelled
CI / linux (swift:4.2) (push) Has been cancelled
CI / linux (swift:5.0) (push) Has been cancelled
CI / linux (swift:5.1) (push) Has been cancelled
CI / linux (swift:5.2) (push) Has been cancelled
CI / linux (swift:5.3) (push) Has been cancelled
CI / linux (swift:5.4) (push) Has been cancelled
CI / linux (swiftlang/swift:nightly-5.5) (push) Has been cancelled
2025-09-30 17:25:19 +02:00
Max Howell
dbdc3aeef6 participate in tea protocol 2024-02-22 05:59:31 -05:00
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
18 changed files with 974 additions and 300 deletions

3
.github/codecov.yml vendored
View File

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

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

View File

@@ -1,101 +0,0 @@
# only run for: merge commits, releases and pull-requests
if: type != push OR branch = master OR branch =~ /^\d+\.\d+\.\d+(-.*)?$/
stages:
- name: pretest
- name: test
- name: deploy
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:
- name: macOS / Swift 4.0.3
script: swift test --parallel -Xswiftc -swift-version -Xswiftc 4
- name: macOS / Swift 4.2.1
script: swift test --parallel
- name: macOS / Swift 5.0
osx_image: xcode10.2
script: swift test --parallel
- name: macOS / Swift 5.1
osx_image: xcode11
script: swift test --parallel
- &xcodebuild
before_install: swift package generate-xcodeproj --enable-code-coverage
xcode_destination: platform=iOS Simulator,OS=latest,name=iPhone XS
name: iOS / Swift 4.2.1
after_success: bash <(curl -s https://codecov.io/bash)
- <<: *xcodebuild
xcode_destination: platform=tvOS Simulator,OS=latest,name=Apple TV
name: tvOS / Swift 4.2.1
- <<: *xcodebuild
name: watchOS / Swift 4.2.1
script: |
set -o pipefail
xcodebuild \
-project Path.swift.xcodeproj \
-scheme Path.swift-Package \
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty
after_success: false
- &linux
env: SWIFT_VERSION=4.2.4
os: linux
name: Linux / Swift 4.2.4
language: generic
sudo: false
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
script: swift test --parallel
- <<: *linux
env: SWIFT_VERSION='5.0.2'
name: Linux / Swift 5.0.2
- <<: *linux
env: SWIFT_VERSION=5.1-DEVELOPMENT-SNAPSHOT-2019-07-03-a
name: Linux / Swift 5.1 (2019-07-03)
- stage: pretest
name: Check Linux tests are syncd
install: swift test --generate-linuxmain
script: git diff --exit-code
osx_image: xcode10.2
- stage: deploy
name: Jazzy
osx_image: xcode10.2
install: gem install jazzy
before_script: swift package generate-xcodeproj
script: |
jazzy --config .github/jazzy.yml \
--module-version $TRAVIS_TAG \
--github_url "https://github.com/$TRAVIS_REPO_SLUG"
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: output
on:
tags: true
- name: CocoaPods
osx_image: xcode10.2
install: |
brew install mxcl/made/swift-sh
curl -O https://raw.githubusercontent.com/mxcl/ops/master/deploy
chmod u+x deploy
before_script: ./deploy generate-podspec
script: pod trunk push
after_success: ./deploy publish-release

View File

@@ -1,27 +1,287 @@
Unlicense (Public Domain) EUROPEAN UNION PUBLIC LICENCE v. 1.2
============================ EUPL © the European Union 2007, 2016
This is free and unencumbered software released into the public domain. This European Union Public Licence (the EUPL) applies to the Work (as defined
below) which is provided under the terms of this Licence. Any use of the Work,
other than as authorised under this Licence is prohibited (to the extent such
use is covered by a right of the copyright holder of the Work).
Anyone is free to copy, modify, publish, use, compile, sell, or The Work is provided under the terms of this Licence when the Licensor (as
distribute this software, either in source code form or as a compiled defined below) has placed the following notice immediately following the
binary, for any purpose, commercial or non-commercial, and by any copyright notice for the Work:
means.
In jurisdictions that recognize copyright laws, the author or authors Licensed under the EUPL
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, or has expressed by any other means his willingness to license under the EUPL.
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to &lt;<http://unlicense.org/>&gt; 1. Definitions
In this Licence, the following terms have the following meaning:
- The Licence: this Licence.
- The Original Work: the work or software distributed or communicated by the
Licensor under this Licence, available as Source Code and also as Executable
Code as the case may be.
- Derivative Works: the works or software that could be created by the
Licensee, based upon the Original Work or modifications thereof. This Licence
does not define the extent of modification or dependence on the Original Work
required in order to classify a work as a Derivative Work; this extent is
determined by copyright law applicable in the country mentioned in Article 15.
- The Work: the Original Work or its Derivative Works.
- The Source Code: the human-readable form of the Work which is the most
convenient for people to study and modify.
- The Executable Code: any code which has generally been compiled and which is
meant to be interpreted by a computer as a program.
- The Licensor: the natural or legal person that distributes or communicates
the Work under the Licence.
- Contributor(s): any natural or legal person who modifies the Work under the
Licence, or otherwise contributes to the creation of a Derivative Work.
- The Licensee or You: any natural or legal person who makes any usage of
the Work under the terms of the Licence.
- Distribution or Communication: any act of selling, giving, lending,
renting, distributing, communicating, transmitting, or otherwise making
available, online or offline, copies of the Work or providing access to its
essential functionalities at the disposal of any other natural or legal
person.
2. Scope of the rights granted by the Licence
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
sublicensable licence to do the following, for the duration of copyright vested
in the Original Work:
- use the Work in any circumstance and for all usage,
- reproduce the Work,
- modify the Work, and make Derivative Works based upon the Work,
- communicate to the public, including the right to make available or display
the Work or copies thereof to the public and perform publicly, as the case may
be, the Work,
- distribute the Work or copies thereof,
- lend and rent the Work or copies thereof,
- sublicense rights in the Work or copies thereof.
Those rights can be exercised on any media, supports and formats, whether now
known or later invented, as far as the applicable law permits so.
In the countries where moral rights apply, the Licensor waives his right to
exercise his moral right to the extent allowed by law in order to make effective
the licence of the economic rights here above listed.
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
any patents held by the Licensor, to the extent necessary to make use of the
rights granted on the Work under this Licence.
3. Communication of the Source Code
The Licensor may provide the Work either in its Source Code form, or as
Executable Code. If the Work is provided as Executable Code, the Licensor
provides in addition a machine-readable copy of the Source Code of the Work
along with each copy of the Work that the Licensor distributes or indicates, in
a notice following the copyright notice attached to the Work, a repository where
the Source Code is easily and freely accessible for as long as the Licensor
continues to distribute or communicate the Work.
4. Limitations on copyright
Nothing in this Licence is intended to deprive the Licensee of the benefits from
any exception or limitation to the exclusive rights of the rights owners in the
Work, of the exhaustion of those rights or of other applicable limitations
thereto.
5. Obligations of the Licensee
The grant of the rights mentioned above is subject to some restrictions and
obligations imposed on the Licensee. Those obligations are the following:
Attribution right: The Licensee shall keep intact all copyright, patent or
trademarks notices and all notices that refer to the Licence and to the
disclaimer of warranties. The Licensee must include a copy of such notices and a
copy of the Licence with every copy of the Work he/she distributes or
communicates. The Licensee must cause any Derivative Work to carry prominent
notices stating that the Work has been modified and the date of modification.
Copyleft clause: If the Licensee distributes or communicates copies of the
Original Works or Derivative Works, this Distribution or Communication will be
done under the terms of this Licence or of a later version of this Licence
unless the Original Work is expressly distributed only under this version of the
Licence — for example by communicating EUPL v. 1.2 only. The Licensee
(becoming Licensor) cannot offer or impose any additional terms or conditions on
the Work or Derivative Work that alter or restrict the terms of the Licence.
Compatibility clause: If the Licensee Distributes or Communicates Derivative
Works or copies thereof based upon both the Work and another work licensed under
a Compatible Licence, this Distribution or Communication can be done under the
terms of this Compatible Licence. For the sake of this clause, Compatible
Licence refers to the licences listed in the appendix attached to this Licence.
Should the Licensee's obligations under the Compatible Licence conflict with
his/her obligations under this Licence, the obligations of the Compatible
Licence shall prevail.
Provision of Source Code: When distributing or communicating copies of the Work,
the Licensee will provide a machine-readable copy of the Source Code or indicate
a repository where this Source will be easily and freely available for as long
as the Licensee continues to distribute or communicate the Work.
Legal Protection: This Licence does not grant permission to use the trade names,
trademarks, service marks, or names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the copyright notice.
6. Chain of Authorship
The original Licensor warrants that the copyright in the Original Work granted
hereunder is owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.
Each Contributor warrants that the copyright in the modifications he/she brings
to the Work are owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.
Each time You accept the Licence, the original Licensor and subsequent
Contributors grant You a licence to their contributions to the Work, under the
terms of this Licence.
7. Disclaimer of Warranty
The Work is a work in progress, which is continuously improved by numerous
Contributors. It is not a finished work and may therefore contain defects or
bugs inherent to this type of development.
For the above reason, the Work is provided under the Licence on an as is basis
and without warranties of any kind concerning the Work, including without
limitation merchantability, fitness for a particular purpose, absence of defects
or errors, accuracy, non-infringement of intellectual property rights other than
copyright as stated in Article 6 of this Licence.
This disclaimer of warranty is an essential part of the Licence and a condition
for the grant of any rights to the Work.
8. Disclaimer of Liability
Except in the cases of wilful misconduct or damages directly caused to natural
persons, the Licensor will in no event be liable for any direct or indirect,
material or moral, damages of any kind, arising out of the Licence or of the use
of the Work, including without limitation, damages for loss of goodwill, work
stoppage, computer failure or malfunction, loss of data or any commercial
damage, even if the Licensor has been advised of the possibility of such damage.
However, the Licensor will be liable under statutory product liability laws as
far such laws apply to the Work.
9. Additional agreements
While distributing the Work, You may choose to conclude an additional agreement,
defining obligations or services consistent with this Licence. However, if
accepting obligations, You may act only on your own behalf and on your sole
responsibility, not on behalf of the original Licensor or any other Contributor,
and only if You agree to indemnify, defend, and hold each Contributor harmless
for any liability incurred by, or claims asserted against such Contributor by
the fact You have accepted any warranty or additional liability.
10. Acceptance of the Licence
The provisions of this Licence can be accepted by clicking on an icon I agree
placed under the bottom of a window displaying the text of this Licence or by
affirming consent in any other similar way, in accordance with the rules of
applicable law. Clicking on that icon indicates your clear and irrevocable
acceptance of this Licence and all of its terms and conditions.
Similarly, you irrevocably accept this Licence and all of its terms and
conditions by exercising any rights granted to You by Article 2 of this Licence,
such as the use of the Work, the creation by You of a Derivative Work or the
Distribution or Communication by You of the Work or copies thereof.
11. Information to the public
In case of any Distribution or Communication of the Work by means of electronic
communication by You (for example, by offering to download the Work from a
remote location) the distribution channel or media (for example, a website) must
at least provide to the public the information requested by the applicable law
regarding the Licensor, the Licence and the way it may be accessible, concluded,
stored and reproduced by the Licensee.
12. Termination of the Licence
The Licence and the rights granted hereunder will terminate automatically upon
any breach by the Licensee of the terms of the Licence.
Such a termination will not terminate the licences of any person who has
received the Work from the Licensee under the Licence, provided such persons
remain in full compliance with the Licence.
13. Miscellaneous
Without prejudice of Article 9 above, the Licence represents the complete
agreement between the Parties as to the Work.
If any provision of the Licence is invalid or unenforceable under applicable
law, this will not affect the validity or enforceability of the Licence as a
whole. Such provision will be construed or reformed so as necessary to make it
valid and enforceable.
The European Commission may publish other linguistic versions or new versions of
this Licence or updated versions of the Appendix, so far this is required and
reasonable, without reducing the scope of the rights granted by the Licence. New
versions of the Licence will be published with a unique version number.
All linguistic versions of this Licence, approved by the European Commission,
have identical value. Parties can take advantage of the linguistic version of
their choice.
14. Jurisdiction
Without prejudice to specific agreement between parties,
- any litigation resulting from the interpretation of this License, arising
between the European Union institutions, bodies, offices or agencies, as a
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
of Justice of the European Union, as laid down in article 272 of the Treaty on
the Functioning of the European Union,
- any litigation arising between other parties and resulting from the
interpretation of this License, will be subject to the exclusive jurisdiction
of the competent court where the Licensor resides or conducts its primary
business.
15. Applicable Law
Without prejudice to specific agreement between parties,
- this Licence shall be governed by the law of the European Union Member State
where the Licensor has his seat, resides or has his registered office,
- this licence shall be governed by Belgian law if the Licensor has no seat,
residence or registered office inside a European Union Member State.
Appendix
Compatible Licences according to Article 5 EUPL are:
- GNU General Public License (GPL) v. 2, v. 3
- GNU Affero General Public License (AGPL) v. 3
- Open Software License (OSL) v. 2.1, v. 3.0
- Eclipse Public License (EPL) v. 1.0
- CeCILL v. 2.0, v. 2.1
- Mozilla Public Licence (MPL) v. 2
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
works other than software
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
Reciprocity (LiLiQ-R+).
The European Commission may update this Appendix to later versions of the above
licences without producing a new version of the EUPL, as long as they provide
the rights granted in Article 2 of this Licence and protect the covered Source
Code from exclusive appropriation.
All other changes or additions to this Appendix require the production of a new
EUPL version.

View File

@@ -2,13 +2,13 @@
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "Path.swift", name: "swiftpm-pathkit",
products: [ products: [
.library(name: "Path", targets: ["Path"]), .library(name: "PathKit", targets: ["PathKit"]),
], ],
targets: [ targets: [
.target(name: "Path", path: "Sources"), .target(name: "PathKit", path: "Sources"),
.testTarget(name: "PathTests", dependencies: ["Path"]), .testTarget(name: "PathTests", dependencies: ["PathKit"]),
], ],
swiftLanguageVersions: [.v4, .v4_2, .version("5")] swiftLanguageVersions: [.v4, .v4_2, .version("5")]
) )

271
README.md
View File

@@ -1,10 +1,18 @@
# Path.swift ![badge-platforms][] ![badge-languages][] [![badge-ci][]][travis] [![badge-jazzy][]][docs] [![badge-codecov][]][codecov] [![badge-version][]][cocoapods] # Swift PathKit ![badge-platforms][] ![badge-languages][]
A file-system pathing library focused on developer experience and robust end A file-system pathing library focused on developer experience and robust end
results. results.
> [!NOTE]
> This repository is a fork of [mxcl/Path.swift][] by [mxcl](), which due to inactivity does not receive new features.
[mxcl/Path.swift]: https://github.com/mxcl/Path.swift
[mxcl]: https://github.com/mxcl
## Examples
```swift ```swift
import Path import PathKit
// convenient static members // convenient static members
let home = Path.home let home = Path.home
@@ -31,38 +39,28 @@ print(bar.isFile) // => true
let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir()) let foo = try Path.root.join("foo").copy(into: Path.root.join("bar").mkdir())
print(foo) // => /bar/foo print(foo) // => /bar/foo
print(foo.isFile) // => true 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.
// we support dynamic-member-syntax when joining named static members, eg: // we support dynamic-member-syntax when joining named static members, eg:
let prefs = Path.home.Library.Preferences // => /Users/mxcl/Library/Preferences let prefs = Path.home.Library.Preferences // => /Users/someuser/Library/Preferences
// a practical example: installing a helper executable // a practical example: installing a helper executable
try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500) try Bundle.resources.helper.copy(into: Path.root.usr.local.bin).chmod(0o500)
``` ```
We emphasize safety and correctness, just like Swift, and also (again like This repository emphasizes safety and correctness, just like Swift, and also (again like
Swift), we provide a thoughtful and comprehensive (yet concise) API. Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl
Hi, Im Max Howell and I have written a lot of open source software, and
probably you already use some of it (Homebrew anyone?). I work full-time on
open source and its hard; currently I earn *less* than minimum wage. Please
help me continue my work, I appreciate it x
<a href="https://www.patreon.com/mxcl">
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
</a>
[Other donation/tipping options](http://mxcl.dev/#donate)
# Handbook # Handbook
Our [online API documentation][docs] covers 100% of our public API and is The [online API documentation][docs] covers 100% of the public API and is
automatically updated for new releases. automatically updated for new releases.
## Codable ## Codable
We support `Codable` as you would expect: `Path` conforms to `Codable`:
```swift ```swift
try JSONEncoder().encode([Path.home, Path.home/"foo"]) try JSONEncoder().encode([Path.home, Path.home/"foo"])
@@ -70,48 +68,70 @@ try JSONEncoder().encode([Path.home, Path.home/"foo"])
```json ```json
[ [
"/Users/mxcl", "/Users/someuser",
"/Users/mxcl/foo", "/Users/someuser/foo",
] ]
``` ```
However, often you want to encode relative paths: `Paths` can be encoded as *relative* paths:
```swift ```swift
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = Path.home encoder.userInfo[.relativePath] = Path.home
encoder.encode([Path.home, Path.home/"foo"]) encoder.encode([Path.home, Path.home/"foo", Path.home/"../baz"])
``` ```
```json ```json
[ [
"", "",
"foo", "foo",
"../baz"
] ]
``` ```
**Note** make sure you decode with this key set *also*, otherwise we `fatal` **Note** if you encode with this key set you *must* decode with the key
(unless the paths are absolute obv.) set also:
```swift ```swift
let decoder = JSONDecoder() let decoder = JSONDecoder()
decoder.userInfo[.relativePath] = Path.home 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 ## Dynamic members
We support `@dynamicMemberLookup`: `Path` supports `@dynamicMemberLookup`:
```swift ```swift
let ls = Path.root.usr.bin.ls // => /usr/bin/ls 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 provided for “starting” functions only, eg. `Path.home` or `Bundle.path`.
This is because we found in practice it was easy to write incorrect code, since Allowing arbitrary property access only in special cases is a precaution.
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 ### Pathish
much less dangerous.
`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 ## Initializing from user-input
@@ -126,9 +146,31 @@ This is explicit, not hiding anything that code-review may miss and preventing
common bugs like accidentally creating `Path` objects from strings you did not common bugs like accidentally creating `Path` objects from strings you did not
expect to be relative. expect to be relative.
Our initializer is nameless to be consistent with the equivalent operation for The initializer is nameless to be consistent with the equivalent operation for
converting strings to `Int`, `Float` etc. in the standard library. 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 ## Extensions
We have some extensions to Apple APIs: We have some extensions to Apple APIs:
@@ -146,8 +188,7 @@ try Bundle.main.resources.join("foo").copy(to: .home)
## Directory listings ## Directory listings
We provide `ls()`, called because it behaves like the Terminal `ls` function, `Path` provides `ls()` to list files. Like `ls` it is not recursive and doesnt
the name thus implies its behavior, ie. that it is not recursive and doesnt
list hidden files. list hidden files.
```swift ```swift
@@ -170,38 +211,46 @@ let files = Path.home.ls().files
// ^^ files that both *exist* and are *not* directories // ^^ files that both *exist* and are *not* directories
let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" } let swiftFiles = Path.home.ls().files.filter{ $0.extension == "swift" }
let includingHiddenFiles = Path.home.ls(.a)
``` ```
We provide `find()` for recursive listing: **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.
`Path` provides `find()` for recursive listing:
```swift ```swift
Path.home.find().execute { path in 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) {
// //
} }
``` ```
Which is configurable: It can be controlled with a closure syntax:
```swift ```swift
Path.home.find().maxDepth(1).extension("swift").kind(.file) { path in 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 }
// //
}
```
And can be controlled:
```swift
Path.home.find().execute { path in
guard foo else { return .skip }
guard bar else { return .abort }
return .continue return .continue
} }
``` ```
Or just get all paths at once: Or get everything at once as an array:
```swift ```swift
let paths = Path.home.find().execute() let paths = Path.home.find().map(\.self)
``` ```
# `Path.swift` is robust # `Path.swift` is robust
@@ -210,50 +259,53 @@ Some parts of `FileManager` are not exactly idiomatic. For example
`isExecutableFile` returns `true` even if there is no file there, it is instead `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 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 check the POSIX permissions of the file first, before returning the result of
`isExecutableFile`. `Path.swift` has done the leg-work for you so you can get on `isExecutableFile`. `Path.swift` has done the leg-work for you so you can just
with your work without worries. get on with it and not have to worry.
There is also some magic going on in Foundations filesystem APIs, which we look There is also some magic going on in Foundations filesystem APIs, which we look
for and ensure our API is deterministic, eg. [this test]. 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 [this test]: https://github.com/astzweig/swiftpm-pathkit/blob/master/Tests/PathTests/PathTests.swift#L539-L554
# `Path.swift` is properly cross-platform # `PathKit` is properly cross-platform
`FileManager` on Linux is full of holes. We have found the holes and worked `FileManager` on Linux has a lot of pitfalls, which this library works around on.
round them where necessary.
# Rules & Caveats # 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 ```swift
Path.home/"b" // => /Users/mxcl/b Path.home/"b" // => /Users/someuser/b
// joining multiple strings works as youd expect // joining multiple strings works as youd expect
Path.home/"b"/"c" // => /Users/mxcl/b/c Path.home/"b"/"c" // => /Users/someuser/b/c
// joining multiple parts at a time is fine // joining multiple parts simultaneously is fine
Path.home/"b/c" // => /Users/mxcl/b/c Path.home/"b/c" // => /Users/someuser/b/c
// joining with absolute paths omits prefixed slash // joining with absolute paths omits prefixed slash
Path.home/"/b" // => /Users/mxcl/b Path.home/"/b" // => /Users/someuser/b
// joining with .. or . works as expected // joining with .. or . works as expected
Path.home.foo.bar.join("..") // => /Users/mxcl/foo Path.home.foo.bar.join("..") // => /Users/someuser/foo
Path.home.foo.bar.join(".") // => /Users/mxcl/foo/bar Path.home.foo.bar.join(".") // => /Users/someuser/foo/bar
// though note that we provide `.parent`:
Path.home.foo.bar.parent // => /Users/someuser/foo
// of course, feel free to join variables: // of course, feel free to join variables:
let b = "b" let b = "b"
let c = "c" let c = "c"
Path.home/b/c // => /Users/mxcl/b/c Path.home/b/c // => /Users/someuser/b/c
// tilde is not special here // tilde is not special here
Path.root/"~b" // => /~b Path.root/"~b" // => /~b
Path.root/"~/b" // => /~/b Path.root/"~/b" // => /~/b
// but is here // but is here
Path("~/foo")! // => /Users/mxcl/foo Path("~/foo")! // => /Users/someuser/foo
// this works provided the user `Guest` exists // this works provided the user `Guest` exists
Path("~Guest") // => /Users/Guest Path("~Guest") // => /Users/Guest
@@ -266,24 +318,24 @@ Path("/foo/bar/../baz") // => /foo/baz
// symlinks are not resolved // symlinks are not resolved
Path.root.bar.symlink(as: "foo") Path.root.bar.symlink(as: "foo")
Path("foo") // => /foo Path("/foo") // => /foo
Path.foo // => /foo Path.root.foo // => /foo
// unless you do it explicitly // unless you do it explicitly
try Path.foo.readlink() // => /bar try Path.root.foo.readlink() // => /bar
// `readlink` only resolves the *final* path component, // `readlink` only resolves the *final* path component,
// thus use `realpath` if there are multiple symlinks // thus use `realpath` if there are multiple symlinks
``` ```
*Path.swift* has the general policy that if the desired end result preexists, *PathKit* has the general policy that if the desired end result preexists,
then its a noop: then its a noop:
* If you try to delete a file, but the file doesn't exist, we do nothing. * If you try to delete a file, but the file doesn't exist, nothing happens.
* If you try to make a directory and it already exists, we do nothing. * If you try to make a directory and it already exists, nothing happens.
* If you call `readlink` on a non-symlink, we return `self` * If you call `readlink` on a non-symlink, we return `self`
However notably if you try to copy or move a file with specifying `overwrite` 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 and the file already exists at the destination and is identical, *PathKit* doesn't check
for that as the check was deemed too expensive to be worthwhile. for that as the check was deemed too expensive to be worthwhile.
## Symbolic links ## Symbolic links
@@ -293,23 +345,27 @@ for that as the check was deemed too expensive to be worthwhile.
equality check is required. equality check is required.
* There are several symlink paths on Mac that are typically automatically * There are several symlink paths on Mac that are typically automatically
resolved by Foundation, eg. `/private`, we attempt to do the same for 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 functions that you would expect it (notably `realpath`), we *do* the same
`Path.init`, but *do not* if you are joining a path that ends up being one of for `Path.init`, but *do not* if you are joining a path that ends up being
these paths, (eg. `Path.root.join("var/private')`). 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` 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 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 meant to be an abstraction for filesystems. To instead verify that there is
no filesystem entry there at all check if `kind` is `nil`. no filesystem entry there at all check if `type` is `nil`.
## We do not provide change directory functionality ## There is no change directory functionality
Changing directory is dangerous, you should *always* try to avoid it and thus 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 we dont even provide the method. If you are executing a sub-process then
use `Process.currentDirectoryURL`. use `Process.currentDirectoryURL` to change *its* working directory when it
executes.
If you must then use `FileManager.changeCurrentDirectory`. 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? # I thought I should only use `URL`s?
@@ -318,8 +374,8 @@ Apple recommend this because they provide a magic translation for
file:///.file/id=6571367.15106761 file:///.file/id=6571367.15106761
Therefore, if you are not using this feature you are fine. If you have URLs the correct Therefore, if you are not using this feature you are fine. If you have URLs the
way to get a `Path` is: correct way to get a `Path` is:
```swift ```swift
if let path = Path(url: url) { if let path = Path(url: url) {
@@ -332,32 +388,32 @@ 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 [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 # Installation
SwiftPM: SwiftPM:
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift.git", from: "0.13.0")) package.append(
.package(url: "https://github.com/astzweig/swiftpm-pathkit.git", from: "1.0.0")
)
package.targets.append(
.target(name: "Foo", dependencies: [
.product(name: "Path", package: "swiftpm-pathkit")
])
)
``` ```
CocoaPods: # Naming Conflicts with `SwiftUI.Path`, etc.
```ruby We have a typealias of `PathStruct` you can use instead.
pod 'Path.swift', '~> 0.13'
```
Carthage:
> Waiting on: [@Carthage#1945](https://github.com/Carthage/Carthage/pull/1945).
## Pre1.0 status
We are pre 1.0, thus we can change the API as we like, and we will (to the
pursuit of getting it *right*)! We will tag 1.0 as soon as possible.
### Get push notifications for new releases
https://mxcl.dev/canopy/
# Alternatives # Alternatives
@@ -368,12 +424,5 @@ https://mxcl.dev/canopy/
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg [badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg
[badge-languages]: https://img.shields.io/badge/swift-4.2%20%7C%205.0-orange.svg [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 [docs]: https://mxcl.dev/Path.swift/Structs/Path.html
[badge-jazzy]: https://raw.githubusercontent.com/mxcl/Path.swift/gh-pages/badge.svg?sanitize=true
[badge-codecov]: https://codecov.io/gh/mxcl/Path.swift/branch/master/graph/badge.svg
[badge-ci]: https://travis-ci.com/mxcl/Path.swift.svg
[travis]: https://travis-ci.com/mxcl/Path.swift
[codecov]: https://codecov.io/gh/mxcl/Path.swift
[badge-version]: https://img.shields.io/cocoapods/v/Path.swift.svg?label=version
[cocoapods]: https://cocoapods.org/pods/Path.swift

View File

@@ -1,21 +1,33 @@
import Foundation import Foundation
/// The `extension` that provides static properties that are common directories. /// The `extension` that provides static properties that are common directories.
extension Path { private enum Foo {
//MARK: Common Directories //MARK: Common Directories
/// Returns a `Path` containing `FileManager.default.currentDirectoryPath`. /// Returns a `Path` containing `FileManager.default.currentDirectoryPath`.
public static var cwd: DynamicPath { static var cwd: DynamicPath {
return .init(string: FileManager.default.currentDirectoryPath) return .init(string: FileManager.default.currentDirectoryPath)
} }
/// Returns a `Path` representing the root path. /// Returns a `Path` representing the root path.
public static var root: DynamicPath { static var root: DynamicPath {
return .init(string: "/") 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 /// Returns a `Path` representing the users home directory
public static var home: DynamicPath { static var home: DynamicPath {
let string: String let string: String
#if os(macOS) #if os(macOS)
if #available(OSX 10.12, *) { if #available(OSX 10.12, *) {
@@ -58,7 +70,7 @@ extension Path {
- Note: There is no standard location for documents on Linux, thus we return `~/Documents`. - Note: There is no standard location for documents on Linux, thus we return `~/Documents`.
- Note: You should create a subdirectory before creating any files. - Note: You should create a subdirectory before creating any files.
*/ */
public static var documents: DynamicPath { static var documents: DynamicPath {
return path(for: .documentDirectory) return path(for: .documentDirectory)
} }
@@ -67,7 +79,7 @@ extension Path {
- Note: On Linux this is `XDG_CACHE_HOME`. - Note: On Linux this is `XDG_CACHE_HOME`.
- Note: You should create a subdirectory before creating any files. - Note: You should create a subdirectory before creating any files.
*/ */
public static var caches: DynamicPath { static var caches: DynamicPath {
return path(for: .cachesDirectory) return path(for: .cachesDirectory)
} }
@@ -76,7 +88,7 @@ extension Path {
- Note: On Linux is `XDG_DATA_HOME`. - Note: On Linux is `XDG_DATA_HOME`.
- Note: You should create a subdirectory before creating any files. - Note: You should create a subdirectory before creating any files.
*/ */
public static var applicationSupport: DynamicPath { static var applicationSupport: DynamicPath {
return path(for: .applicationSupportDirectory) return path(for: .applicationSupportDirectory)
} }
} }
@@ -96,3 +108,35 @@ func defaultUrl(for searchPath: FileManager.SearchPathDirectory) -> DynamicPath
} }
#endif #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

@@ -29,8 +29,9 @@ public extension Pathish {
if overwrite, let tokind = to.type, tokind != .directory, type != .directory { if overwrite, let tokind = to.type, tokind != .directory, type != .directory {
try FileManager.default.removeItem(at: to.url) try FileManager.default.removeItem(at: to.url)
} }
#if os(Linux) && !swift(>=5.2) // check if fixed #if os(Linux)
if !overwrite, to.kind != nil { //NOTE doing manually due to inconsistency in Linux Foundation behavior
if !overwrite, to.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -69,8 +70,9 @@ public extension Pathish {
if overwrite, let kind = rv.type, kind != .directory { if overwrite, let kind = rv.type, kind != .directory {
try FileManager.default.removeItem(at: rv.url) try FileManager.default.removeItem(at: rv.url)
} }
#if os(Linux) && !swift(>=5.2) // check if fixed #if os(Linux)
if !overwrite, rv.kind != nil { //NOTE doing manually due to inconsistency in Linux Foundation behavior
if !overwrite, rv.type != nil {
throw CocoaError.error(.fileWriteFileExists) throw CocoaError.error(.fileWriteFileExists)
} }
#endif #endif
@@ -204,7 +206,7 @@ public extension Pathish {
} }
/** /**
Renames the file at path. Renames the file (basename only) at path.
Path.root.foo.bar.rename(to: "baz") // => /foo/baz Path.root.foo.bar.rename(to: "baz") // => /foo/baz

View File

@@ -13,8 +13,14 @@ public extension Path {
private let enumerator: FileManager.DirectoryEnumerator! private let enumerator: FileManager.DirectoryEnumerator!
/// The range of directory depths for which the find operation will return entries.b /// The range of directory depths for which the find operation will return entries.
private(set) public var depth: ClosedRange<Int> = 1...Int.max 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. /// The kinds of filesystem entries find operations will return.
public var types: Set<EntryType> { public var types: Set<EntryType> {
@@ -25,6 +31,9 @@ public extension Path {
/// The file extensions find operations will return. Files *and* directories unless you filter for `kinds`. /// The file extensions find operations will return. Files *and* directories unless you filter for `kinds`.
private(set) public var extensions: Set<String>? private(set) public var extensions: Set<String>?
/// Whether to return hidden files
public var hidden:Bool = true
} }
} }
@@ -42,14 +51,14 @@ extension Path.Finder: Sequence, IteratorProtocol {
continue continue
} }
if enumerator.level < depth.lowerBound { if enumerator.level < depth.lowerBound {
if path == self.path, depth.lowerBound == 0 { continue
return path }
} else {
continue if !hidden, path.basename().hasPrefix(".") {
} enumerator.skipDescendants()
continue
} }
#endif #endif
if let type = path.type, !types.contains(type) { continue } if let type = path.type, !types.contains(type) { continue }
if let exts = extensions, !exts.contains(path.extension) { continue } if let exts = extensions, !exts.contains(path.extension) { continue }
return path return path
@@ -112,6 +121,15 @@ public extension Path.Finder {
extensions!.insert(ext) extensions!.insert(ext)
return self 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` /// The return type for `Path.Finder`
enum ControlFlow { enum ControlFlow {
@@ -128,7 +146,11 @@ public extension Path.Finder {
while let path = next() { while let path = next() {
switch try closure(path) { switch try closure(path) {
case .skip: case .skip:
#if !os(Linux) || swift(>=5.0)
enumerator.skipDescendants() enumerator.skipDescendants()
#else
fputs("warning: skip is not implemented for Swift < 5.0\n", stderr)
#endif
case .abort: case .abort:
return return
case .continue: case .continue:
@@ -158,7 +180,7 @@ public extension Pathish {
if options != .a, path.basename().hasPrefix(".") { return nil } 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` // ^^ we dont use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls`
return path return path
} }.sorted()
} }
/// Recursively find files under this path. If the path is a file, no files will be found. /// Recursively find files under this path. If the path is a file, no files will be found.
@@ -192,6 +214,6 @@ public extension Array where Element == Path {
/// Options for `Path.ls(_:)` /// Options for `Path.ls(_:)`
public enum ListDirectoryOptions { public enum ListDirectoryOptions {
/// Creates intermediary directories; works the same as `mkdir -p`. /// Lists hidden files also
case a case a
} }

View File

@@ -7,6 +7,8 @@ import func Glibc.realpath
let _realpath = Glibc.realpath let _realpath = Glibc.realpath
#endif #endif
public typealias PathStruct = Path
/** /**
A `Path` represents an absolute path on a filesystem. A `Path` represents an absolute path on a filesystem.
@@ -168,6 +170,7 @@ public extension Pathish {
*/ */
var parent: Path { var parent: Path {
let index = string.lastIndex(of: "/")! let index = string.lastIndex(of: "/")!
guard index != string.indices.startIndex else { return Path(string: "/") }
let substr = string[string.indices.startIndex..<index] let substr = string[string.indices.startIndex..<index]
return Path(string: String(substr)) return Path(string: String(substr))
} }
@@ -176,7 +179,7 @@ public extension Pathish {
Returns the filename extension of this path. Returns the filename extension of this path.
- Remark: If there is no extension returns "". - Remark: If there is no extension returns "".
- Remark: If the filename ends with any number of ".", returns "". - Remark: If the filename ends with any number of ".", returns "".
- Note: We special case eg. `foo.tar.gz`. - Note: We special case eg. `foo.tar.gz`there are a limited number of these specializations, feel free to PR more.
*/ */
@inlinable @inlinable
var `extension`: String { var `extension`: String {

View File

@@ -11,7 +11,8 @@ public extension Pathish {
/** /**
- Returns: `true` if the path represents an actual filesystem entry. - Returns: `true` if the path represents an actual filesystem entry.
- Note: If `self` is a symlink the return value represents the destination. - 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 { var exists: Bool {
return FileManager.default.fileExists(atPath: string) return FileManager.default.fileExists(atPath: string)

View File

@@ -1,5 +1,5 @@
import XCTest import XCTest
import Path import PathKit
extension PathTests { extension PathTests {
func testFindMaxDepth1() throws { func testFindMaxDepth1() throws {
@@ -11,16 +11,16 @@ extension PathTests {
do { do {
let finder = tmpdir.find().depth(max: 1) let finder = tmpdir.find().depth(max: 1)
XCTAssertEqual(finder.depth, 1...1) XCTAssertEqual(finder.depth, 1...1)
#if !os(Linux) || swift(>=5) #if !os(Linux) || swift(>=5)
XCTAssertEqual(Set(finder), Set([tmpdir.a, tmpdir.b, tmpdir.c].map(Path.init))) XCTAssertEqual(Set(finder), Set([tmpdir.a, tmpdir.b, tmpdir.c].map(Path.init)))
#endif #endif
} }
do { do {
let finder = tmpdir.find().depth(max: 0) let finder = tmpdir.find().depth(max: 0)
XCTAssertEqual(finder.depth, 0...0) XCTAssertEqual(finder.depth, 0...0)
#if !os(Linux) || swift(>=5) #if !os(Linux) || swift(>=5)
XCTAssertEqual(Set(finder), Set()) XCTAssertEqual(Set(finder), Set())
#endif #endif
} }
} }
} }
@@ -34,20 +34,130 @@ extension PathTests {
do { do {
let finder = tmpdir.find().depth(max: 2) let finder = tmpdir.find().depth(max: 2)
XCTAssertEqual(finder.depth, 1...2) XCTAssertEqual(finder.depth, 1...2)
#if !os(Linux) || swift(>=5)
XCTAssertEqual( XCTAssertEqual(
Set(finder), Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c].map(Path.init))) Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c].map(Path.init)))
#endif
} }
do { do {
let finder = tmpdir.find().depth(max: 3) let finder = tmpdir.find().depth(max: 3)
XCTAssertEqual(finder.depth, 1...3) XCTAssertEqual(finder.depth, 1...3)
#if !os(Linux) || swift(>=5)
XCTAssertEqual( XCTAssertEqual(
Set(finder), Set(finder),
Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c, tmpdir.b.d.e].map(Path.init))) 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 { func testFindExtension() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
try tmpdir.join("foo.json").touch() try tmpdir.join("foo.json").touch()
@@ -62,7 +172,55 @@ extension PathTests {
} }
} }
func testFindKinds() throws { //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 Path.mktemp { tmpdir in
try tmpdir.foo.mkdir() try tmpdir.foo.mkdir()
try tmpdir.bar.touch() try tmpdir.bar.touch()
@@ -78,4 +236,16 @@ extension PathTests {
Set(["foo", "bar"].map(tmpdir.join))) 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,9 +1,22 @@
@testable import Path @testable import PathKit
import func XCTest.XCTAssertEqual import func XCTest.XCTAssertEqual
import Foundation import Foundation
import XCTest import XCTest
extension PathStruct {
var foo: Int { fatalError()}
}
class PathTests: XCTestCase { class PathTests: XCTestCase {
func testNewStuff() {
#if swift(>=5.5)
func foo<P: Pathish>(_ path: P) {}
foo(.home)
foo(.root)
#endif
}
func testConcatenation() { func testConcatenation() {
XCTAssertEqual((Path.root/"bar").string, "/bar") XCTAssertEqual((Path.root/"bar").string, "/bar")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath) XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
@@ -41,7 +54,7 @@ class PathTests: XCTestCase {
XCTAssertEqual(["b.swift"], Set(lsrv.files.filter{ $0.extension == "swift" }.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(["c"], Set(lsrv.files.filter{ $0.extension == "" }.map{ $0.relative(to: tmpdir) }))
XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"]) XCTAssertEqual(paths, ["a", "b.swift", "c", ".d"])
} }
func testEnumerationSkippingHiddenFiles() throws { func testEnumerationSkippingHiddenFiles() throws {
@@ -51,7 +64,7 @@ class PathTests: XCTestCase {
try tmpdir.join("b").touch() try tmpdir.join("b").touch()
try tmpdir.join("c").touch() try tmpdir.join("c").touch()
try tmpdir.join(".d").mkdir().join("e").touch() try tmpdir.join(".d").mkdir().join("e").touch()
var paths = Set<String>() var paths = Set<String>()
var dirs = 0 var dirs = 0
for path in tmpdir.ls() { for path in tmpdir.ls() {
@@ -146,15 +159,21 @@ class PathTests: XCTestCase {
].map(Path.init) ].map(Path.init)
let encoder = JSONEncoder() let encoder = JSONEncoder()
encoder.userInfo[.relativePath] = root
let data = try encoder.encode(input)
XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"]) func test<P: Pathish>(relativePath: P, line: UInt = #line) throws {
encoder.userInfo[.relativePath] = relativePath
let data = try encoder.encode(input)
let decoder = JSONDecoder() XCTAssertEqual(try JSONSerialization.jsonObject(with: data) as? [String], ["..", "", "bar"], line: line)
XCTAssertThrowsError(try decoder.decode([Path].self, from: data))
decoder.userInfo[.relativePath] = root let decoder = JSONDecoder()
XCTAssertEqual(try decoder.decode([Path].self, from: data), input) 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() { func testJoin() {
@@ -186,6 +205,14 @@ class PathTests: XCTestCase {
XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar") XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar")
} }
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() { func testDynamicMember() {
XCTAssertEqual(Path.root.Documents, Path.root/"Documents") XCTAssertEqual(Path.root.Documents, Path.root/"Documents")
@@ -193,7 +220,7 @@ class PathTests: XCTestCase {
XCTAssertEqual(a.Documents, Path.home/"foo/Documents") XCTAssertEqual(a.Documents, Path.home/"foo/Documents")
// verify use of the dynamic-member-subscript works according to our rules // verify use of the dynamic-member-subscript works according to our rules
XCTAssertEqual(Path.home[dynamicMember: "../~foo"].string, "\(Path.home.parent.string)/~foo") XCTAssertEqual(Path.home[dynamicMember: "../~foo"].string, Path(Path.home).parent.join("~foo").string)
} }
func testCopyTo() throws { func testCopyTo() throws {
@@ -299,6 +326,13 @@ class PathTests: XCTestCase {
XCTAssertEqual(Path.root.string, "/") XCTAssertEqual(Path.root.string, "/")
XCTAssertEqual(Path.home.string, NSHomeDirectory()) XCTAssertEqual(Path.home.string, NSHomeDirectory())
XCTAssertEqual(Path.documents.string, NSHomeDirectory() + "/Documents") 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) #if !os(Linux)
XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches") XCTAssertEqual(Path.caches.string, NSHomeDirectory() + "/Library/Caches")
XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath) XCTAssertEqual(Path.cwd.string, FileManager.default.currentDirectoryPath)
@@ -313,6 +347,8 @@ class PathTests: XCTestCase {
func testStringConvertibles() { func testStringConvertibles() {
XCTAssertEqual(Path.root.description, "/") XCTAssertEqual(Path.root.description, "/")
XCTAssertEqual(Path.root.debugDescription, "Path(/)") XCTAssertEqual(Path.root.debugDescription, "Path(/)")
XCTAssertEqual(Path(Path.root).description, "/")
XCTAssertEqual(Path(Path.root).debugDescription, "Path(/)")
} }
func testFilesystemAttributes() throws { func testFilesystemAttributes() throws {
@@ -365,17 +401,13 @@ class PathTests: XCTestCase {
func testTimes() throws { func testTimes() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
let foo = try tmpdir.foo.touch()
let now1 = Date().timeIntervalSince1970.rounded(.down) let now1 = Date().timeIntervalSince1970.rounded(.down)
#if !os(Linux)
XCTAssertEqual(foo.ctime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
#endif
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now1) //FIXME flakey
sleep(1) sleep(1)
try foo.touch() let foo = try tmpdir.foo.touch()
let now2 = Date().timeIntervalSince1970.rounded(.down) #if !os(Linux)
XCTAssertNotEqual(now1, now2) XCTAssertGreaterThan(foo.ctime?.timeIntervalSince1970.rounded(.down) ?? 0, now1) //FIXME flakey
XCTAssertEqual(foo.mtime?.timeIntervalSince1970.rounded(.down), now2) //FIXME flakey #endif
XCTAssertGreaterThan(foo.mtime?.timeIntervalSince1970.rounded(.down) ?? 0, now1) //FIXME flakey
XCTAssertNil(tmpdir.void.mtime) XCTAssertNil(tmpdir.void.mtime)
XCTAssertNil(tmpdir.void.ctime) XCTAssertNil(tmpdir.void.ctime)
@@ -405,6 +437,9 @@ class PathTests: XCTestCase {
XCTAssertNoThrow(try bar7.delete()) XCTAssertNoThrow(try bar7.delete())
XCTAssertEqual(bar7.type, nil) XCTAssertEqual(bar7.type, nil)
XCTAssertEqual(tmpdir.bar6.type, .file) XCTAssertEqual(tmpdir.bar6.type, .file)
// for code-coverage
XCTAssertEqual(tmpdir.bar6.kind, .file)
} }
} }
@@ -436,7 +471,7 @@ class PathTests: XCTestCase {
#if os(macOS) #if os(macOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks) XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Contents.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.Contents.Resources) XCTAssertEqual(bndl.defaultResourcesPath, tmpdir.Contents.Resources)
#elseif os(tvOS) || os(iOS) #elseif os(tvOS) || os(iOS) || os(watchOS)
XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Frameworks) XCTAssertEqual(bndl.defaultSharedFrameworksPath, tmpdir.Frameworks)
XCTAssertEqual(bndl.defaultResourcesPath, tmpdir) XCTAssertEqual(bndl.defaultResourcesPath, tmpdir)
#else #else
@@ -488,8 +523,19 @@ class PathTests: XCTestCase {
func testTouchThrowsIfCannotWrite() throws { func testTouchThrowsIfCannotWrite() throws {
try Path.mktemp { tmpdir in try Path.mktemp { tmpdir in
print(try FileManager.default.attributesOfItem(atPath: tmpdir.string)[.posixPermissions])
//FIXME fails in Docker image (only)
try tmpdir.chmod(0o000) 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()) XCTAssertThrowsError(try tmpdir.bar.touch())
XCTAssertFalse(tmpdir.bar.exists)
} }
} }
@@ -642,12 +688,10 @@ class PathTests: XCTestCase {
XCTAssertEqual(bar.type, .symlink) XCTAssertEqual(bar.type, .symlink)
} }
} }
}
private func XCTAssertEqual<P: Pathish, Q: Pathish>(_ p: P, _ q: Q, file: StaticString = #file, line: UInt = #line) { func testOptionalInitializer() throws {
XCTAssertEqual(p.string, q.string, file: file, line: line) XCTAssertNil(Path(""))
} XCTAssertNil(Path("./foo"))
XCTAssertEqual(Path("/foo"), Path.root.foo)
private 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)
} }

View File

@@ -1,4 +1,4 @@
@testable import Path @testable import PathKit
import Foundation import Foundation
class TemporaryDirectory { class TemporaryDirectory {

View File

@@ -24,21 +24,32 @@ extension PathTests {
("testFileHandleExtensions", testFileHandleExtensions), ("testFileHandleExtensions", testFileHandleExtensions),
("testFileReference", testFileReference), ("testFileReference", testFileReference),
("testFilesystemAttributes", testFilesystemAttributes), ("testFilesystemAttributes", testFilesystemAttributes),
("testFindCallingExecuteTwice", testFindCallingExecuteTwice),
("testFindDepth0", testFindDepth0),
("testFindDepthRange", testFindDepthRange),
("testFindExecute", testFindExecute),
("testFindExtension", testFindExtension), ("testFindExtension", testFindExtension),
("testFindKinds", testFindKinds), ("testFindHidden", testFindHidden),
("testFindMaxDepth0", testFindMaxDepth0),
("testFindMaxDepth1", testFindMaxDepth1), ("testFindMaxDepth1", testFindMaxDepth1),
("testFindMaxDepth2", testFindMaxDepth2),
("testFindMinDepth", testFindMinDepth),
("testFindOnNonexistentDirectoryHasNoContent", testFindOnNonexistentDirectoryHasNoContent),
("testFindTypes", testFindTypes),
("testFlatMap", testFlatMap), ("testFlatMap", testFlatMap),
("testInitializerForRelativePath", testInitializerForRelativePath), ("testInitializerForRelativePath", testInitializerForRelativePath),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin), ("testJoin", testJoin),
("testKind", testKind), ("testKind", testKind),
("testLock", testLock), ("testLock", testLock),
("testLsOnNonexistentDirectoryReturnsEmptyArray", testLsOnNonexistentDirectoryReturnsEmptyArray),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testMoveInto", testMoveInto), ("testMoveInto", testMoveInto),
("testMoveTo", testMoveTo), ("testMoveTo", testMoveTo),
("testNewStuff", testNewStuff),
("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution), ("testNoUndesiredSymlinkResolution", testNoUndesiredSymlinkResolution),
("testOptionalInitializer", testOptionalInitializer),
("testParent", testParent),
("testPathComponents", testPathComponents), ("testPathComponents", testPathComponents),
("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf), ("testReadlinkOnFileReturnsSelf", testReadlinkOnFileReturnsSelf),
("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows), ("testReadlinkOnNonExistantFileThrows", testReadlinkOnNonExistantFileThrows),

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

@@ -0,0 +1,43 @@
import XCTest
import PathKit
#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