Compare commits

...

16 Commits
0.4.0 ... 0.4.4

Author SHA1 Message Date
Max Howell
58d026c8a9 Fix copy(into:) overwrite mode
Seems like Linux Foundation has a bug

I checked, seems fixed in Swift 5. But added a swift version check se we can verify and if not report the bug.
2019-01-22 15:26:04 -05:00
Max Howell
43d3e0a745 Improve docs 2019-01-21 14:30:12 -05:00
Max Howell
21fb03b9d9 Test .. paths work with join 2019-01-21 14:26:56 -05:00
Max Howell
3333c731d3 Fix Travis 2019-01-21 12:57:13 -05:00
Max Howell
e15173cfbc Merge pull request #9 from LucianoPAlmeida/is-path-conveniece-add
Adding convenience extensions on Path->Bool
2019-01-20 19:28:05 -05:00
Luciano Almeida
7be264a38e Adding convenience extensions on Path->Bool 2019-01-20 22:13:13 -02:00
Max Howell
aac81b85a4 Merge pull request #8 from mxcl/better-deploy
Better deploy
2019-01-20 18:53:11 -05:00
Max Howell
3644124a36 Fix testing on tvOS/iOS 2019-01-20 18:44:24 -05:00
Max Howell
ca4ac3ec8f Fix stage types in .travis.yml 2019-01-20 17:33:56 -05:00
Max Howell
920f007660 Fix Linux testEnumerationSkippingHiddenFiles() 2019-01-20 17:32:39 -05:00
Max Howell
751b855a26 Pretest that fails if Linux tests aren't current 2019-01-20 17:25:46 -05:00
Max Howell
c0e5023632 Better deployment of Jazzy and Pods 2019-01-20 17:13:03 -05:00
Max Howell
e0c62108e8 Update Linux tests 2019-01-20 16:30:02 -05:00
Max Howell
5cc2fcbf30 Tag 0.4.1 2019-01-20 16:26:22 -05:00
Max Howell
7595c601e8 Attempt custom Jazzy index/contents 2019-01-20 16:24:06 -05:00
Luciano Almeida
d8ea357459 Adding ls -a like functionality to Path.ls() 2019-01-20 16:23:40 -05:00
12 changed files with 183 additions and 62 deletions

View File

@@ -2,6 +2,7 @@
if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/ if: type != push OR branch = master OR branch =~ /^\d+\.\d+(\.\d+)?(-\S*)?$/
stages: stages:
- name: pretest
- name: test - name: test
- name: deploy - name: deploy
if: branch =~ ^\d+\.\d+\.\d+$ if: branch =~ ^\d+\.\d+\.\d+$
@@ -34,7 +35,6 @@ jobs:
-destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \ -destination 'platform=watchOS Simulator,OS=latest,name=Apple Watch Series 4 - 40mm' \
build | xcpretty build | xcpretty
- env: SWIFT_VERSION=4.2.1 - env: SWIFT_VERSION=4.2.1
os: linux os: linux
name: Linux name: Linux
@@ -44,20 +44,34 @@ jobs:
install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" install: eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
script: swift test script: swift test
- <<: *xcodebuild - stage: pretest
stage: deploy name: Check if Linux tests are up-to-date
install: swift test --generate-linuxmain
script: git diff --exit-code
- stage: deploy
name: Jazzy 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
install: gem install jazzy install: gem install jazzy
script: | before_script: swift package generate-xcodeproj
jazzy \ script: jazzy
--no-hide-documentation-coverage \
--theme fullwidth \
--output output \
--readme README.md \
--root-url https://mxcl.github.io/Path.swift/ \
--github_url https://github.com/mxcl/Path.swift \
--module Path \
--module-version $TRAVIS_TAG
deploy: deploy:
provider: pages provider: pages
skip-cleanup: true skip-cleanup: true
@@ -66,7 +80,27 @@ jobs:
on: on:
tags: true tags: true
- stage: deploy - name: CocoaPods
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 install: gem install cocoapods --pre
script: pod trunk push --allow-warnings script: pod trunk push

View File

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

View File

@@ -15,21 +15,29 @@ let docs = Path.home/"Documents"
// paths are *always* absolute thus avoiding common bugs // paths are *always* absolute thus avoiding common bugs
let path = Path(userInput) ?? Path.cwd/userInput let path = Path(userInput) ?? Path.cwd/userInput
// chainable syntax so you have less boilerplate // elegant, chainable syntax
try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555) try Path.home.join("foo").mkdir().join("bar").touch().chmod(0o555)
// easy file-management // sensible considerations
try Path.root.join("foo").copy(to: Path.root/"bar") try Path.home.join("bar").mkdir()
try Path.home.join("bar").mkdir() // doesnt throw we already have the desired result
// careful API to avoid common bugs // easy file-management
try Path.root.join("foo").copy(into: Path.root.mkdir("bar")) let bar = try Path.root.join("foo").copy(to: Path.root/"bar")
// ^^ other libraries would make the above `to:` form handle both these cases print(bar) // => /bar
// but that can easily lead to bugs where you accidentally write files that print(bar.isFile) // => true
// were meant to be directory destinations
// careful API considerations so as to avoid common bugs
let foo = try Path.root.join("foo").copy(into: Path.root.mkdir("bar"))
print(foo) // => /bar/foo
print(foo.isFile) // => true
// A practical example: installing a helper executable
try Bundle.resources.join("helper").copy(into: Path.home.join(".local/bin").mkpath()).chmod(0o500)
``` ```
We emphasize safety and correctness, just like Swift, and also just We emphasize safety and correctness, just like Swift, and also (again like
like Swift, we provide a thoughtful and comprehensive (yet concise) API. Swift), we provide a thoughtful and comprehensive (yet concise) API.
# Support mxcl # Support mxcl
@@ -183,13 +191,13 @@ Path("~foo") // => nil
SwiftPM: SwiftPM:
```swift ```swift
package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.4.0")) package.append(.package(url: "https://github.com/mxcl/Path.swift", from: "0.4.1"))
``` ```
CocoaPods: CocoaPods:
```ruby ```ruby
pod 'Path.swift' ~> 0.4.0 pod 'Path.swift' ~> '0.4.1'
``` ```
Please note! We are pre 1.0, thus we can change the API as we like! We will tag Please note! We are pre 1.0, thus we can change the API as we like! We will tag
@@ -208,4 +216,4 @@ https://codebasesaga.com/canopy/
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20iOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg [badge-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 [badge-languages]: https://img.shields.io/badge/swift-4.2-orange.svg
[online API documentation]: https://mxcl.github.io/Path.swift/ [online API documentation]: https://mxcl.github.io/Path.swift/Structs/Path.html

View File

@@ -1,5 +1,6 @@
import Foundation import Foundation
/// Extensions on Foundations `Bundle` so you get `Path` rather than `String` or `URL`.
public extension Bundle { public extension Bundle {
/// Returns the path for requested resource in this bundle. /// Returns the path for requested resource in this bundle.
func path(forResource: String, ofType: String?) -> Path? { func path(forResource: String, ofType: String?) -> Path? {
@@ -24,6 +25,7 @@ public extension Bundle {
} }
} }
/// Extensions on `String` that work with `Path` rather than `String` or `URL`
public extension String { public extension String {
/// Initializes this `String` with the contents of the provided path. /// Initializes this `String` with the contents of the provided path.
@inlinable @inlinable
@@ -40,6 +42,7 @@ public extension String {
} }
} }
/// Extensions on `Data` that work with `Path` rather than `String` or `URL`
public extension Data { public extension Data {
/// Initializes this `Data` with the contents of the provided path. /// Initializes this `Data` with the contents of the provided path.
@inlinable @inlinable

View File

@@ -1,10 +1,12 @@
import Foundation import Foundation
/// Provided for relative-path coding. See the instructions in our `README`.
public extension CodingUserInfoKey { public extension CodingUserInfoKey {
/// If set paths are encoded as relative to this path. /// If set paths are encoded as relative to this path.
static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")! static let relativePath = CodingUserInfoKey(rawValue: "dev.mxcl.Path.relative")!
} }
/// Provided for relative-path coding. See the instructions in our `README`.
extension Path: Codable { extension Path: Codable {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer().decode(String.self) let value = try decoder.singleValueContainer().decode(String.self)

View File

@@ -7,7 +7,7 @@ public extension Path {
- Parameter to: Destination filename. - Parameter to: Destination filename.
- Parameter overwrite: If true overwrites any file that already exists at `to`. - Parameter overwrite: If true overwrites any file that already exists at `to`.
- Returns: `to` to allow chaining - Returns: `to` to allow chaining
- SeeAlso: copy(into:overwrite:) - SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
public func copy(to: Path, overwrite: Bool = false) throws -> Path { public func copy(to: Path, overwrite: Bool = false) throws -> Path {
@@ -23,20 +23,33 @@ public extension Path {
If the destination does not exist, this function creates the directory first. 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)
- Note: `throws` if `into` is a file. - Note: `throws` if `into` is a file.
- Parameter into: Destination directory - Parameter into: Destination directory
- Parameter overwrite: If true overwrites any file that already exists at `into`. - Parameter overwrite: If true overwrites any file that already exists at `into`.
- Returns: The `Path` of the newly copied file. - Returns: The `Path` of the newly copied file.
- SeeAlso: copy(into:overwrite:) - SeeAlso: `copy(into:overwrite:)`
*/ */
@discardableResult @discardableResult
public func copy(into: Path, overwrite: Bool = false) throws -> Path { public func copy(into: Path, overwrite: Bool = false) throws -> Path {
if !into.exists { if !into.exists {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
} else if overwrite, !into.isDirectory {
try into.delete()
} }
let rv = into/basename() let rv = into/basename()
if overwrite, rv.isFile {
try rv.delete()
}
#if os(Linux)
#if swift(>=5)
// check if fixed
#else
if !overwrite, rv.isFile {
throw CocoaError.error(.fileWriteFileExists)
}
#endif
#endif
try FileManager.default.copyItem(at: url, to: rv.url) try FileManager.default.copyItem(at: url, to: rv.url)
return rv return rv
} }

View File

@@ -1,14 +1,24 @@
import Foundation import Foundation
public extension Path { public extension Path {
/// same as the `ls` command is shallow /**
func ls() throws -> [Entry] { Same as the `ls -a` command is shallow
let relativePaths = try FileManager.default.contentsOfDirectory(atPath: string) - Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`.
func convert(relativePath: String) -> Entry { - Important: `includeHiddenFiles` does not work on Linux
let path = self/relativePath */
func ls(includeHiddenFiles: Bool = true) throws -> [Entry] {
var opts = FileManager.DirectoryEnumerationOptions()
#if !os(Linux)
if !includeHiddenFiles {
opts.insert(.skipsHiddenFiles)
}
#endif
let paths = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: opts)
func convert(url: URL) -> Entry? {
guard let path = Path(url.path) else { return nil }
return Entry(kind: path.isDirectory ? .directory : .file, path: path) return Entry(kind: path.isDirectory ? .directory : .file, path: path)
} }
return relativePaths.map(convert) return paths.compactMap(convert)
} }
} }

View File

@@ -6,6 +6,16 @@ public extension Path {
return FileManager.default.isWritableFile(atPath: string) 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)
}
/// Returns true if the path represents an actual directory. /// Returns true if the path represents an actual directory.
var isDirectory: Bool { var isDirectory: Bool {
var isDir: ObjCBool = false var isDir: ObjCBool = false

View File

@@ -10,7 +10,7 @@ import Foundation
let p3 = Path.cwd/relativePathString let p3 = Path.cwd/relativePathString
let p4 = Path(userInput) ?? Path.cwd/userInput let p4 = Path(userInput) ?? Path.cwd/userInput
- Note: There may not be an actual filename at the path. - Note: There may not be an actual filesystem entry at the path.
*/ */
public struct Path: Equatable, Hashable, Comparable { public struct Path: Equatable, Hashable, Comparable {
/// The underlying filesystem path /// The underlying filesystem path
@@ -113,7 +113,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter pathComponent: The string to join with this path. - Parameter pathComponent: The string to join with this path.
- Returns: A new joined path. - Returns: A new joined path.
- SeeAlso: /(:Path,:String) - SeeAlso: `/(_:, _:)`
*/ */
public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol { public func join<S>(_ pathComponent: S) -> Path where S: StringProtocol {
//TODO standardizingPath does more than we want really (eg tilde expansion) //TODO standardizingPath does more than we want really (eg tilde expansion)
@@ -154,7 +154,7 @@ public struct Path: Equatable, Hashable, Comparable {
- Parameter lhs: The base path to join with `rhs`. - Parameter lhs: The base path to join with `rhs`.
- Parameter rhs: The string to join with this `lhs`. - Parameter rhs: The string to join with this `lhs`.
- Returns: A new joined path. - Returns: A new joined path.
- SeeAlso: Path.join(_:) - SeeAlso: `Path.join(_:)`
*/ */
@inlinable @inlinable
public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol { public func /<S>(lhs: Path, rhs: S) -> Path where S: StringProtocol {

View File

@@ -17,6 +17,7 @@ class PathTests: XCTestCase {
try tmpdir.join("a").mkdir().join("c").touch() try tmpdir.join("a").mkdir().join("c").touch()
try tmpdir.join("b").touch() try tmpdir.join("b").touch()
try tmpdir.join("c").touch() try tmpdir.join("c").touch()
try tmpdir.join(".d").mkdir().join("e").touch()
var paths = Set<String>() var paths = Set<String>()
var dirs = 0 var dirs = 0
@@ -26,8 +27,31 @@ class PathTests: XCTestCase {
} }
paths.insert(entry.path.basename()) paths.insert(entry.path.basename())
} }
XCTAssertEqual(dirs, 2)
XCTAssertEqual(paths, ["a", "b", "c", ".d"])
}
func testEnumerationSkippingHiddenFiles() throws {
#if !os(Linux)
let tmpdir_ = try TemporaryDirectory()
let tmpdir = tmpdir_.path
try tmpdir.join("a").mkdir().join("c").touch()
try tmpdir.join("b").touch()
try tmpdir.join("c").touch()
try tmpdir.join(".d").mkdir().join("e").touch()
var paths = Set<String>()
var dirs = 0
for entry in try tmpdir.ls(includeHiddenFiles: false) {
if entry.kind == .directory {
dirs += 1
}
paths.insert(entry.path.basename())
}
XCTAssertEqual(dirs, 1) XCTAssertEqual(dirs, 1)
XCTAssertEqual(paths, ["a", "b", "c"]) XCTAssertEqual(paths, ["a", "b", "c"])
#endif
} }
func testRelativeTo() { func testRelativeTo() {
@@ -108,5 +132,21 @@ class PathTests: XCTestCase {
XCTAssertEqual(Path.root/"~/b", Path("/~/b")) XCTAssertEqual(Path.root/"~/b", Path("/~/b"))
XCTAssertEqual(Path("~/foo"), Path.home/"foo") XCTAssertEqual(Path("~/foo"), Path.home/"foo")
XCTAssertNil(Path("~foo")) XCTAssertNil(Path("~foo"))
XCTAssertEqual(Path.root/"a/foo"/"../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"/../bar", Path.root/"a/bar")
XCTAssertEqual(Path.root/"a/foo"/"../../bar", Path.root/"bar")
XCTAssertEqual(Path.root/"a/foo"/"../../../bar", Path.root/"bar")
}
func testCopyInto() 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)
}
}
} }
} }

View File

@@ -52,3 +52,20 @@ extension Path {
return try body(tmp.path) return try body(tmp.path)
} }
} }
#if !os(macOS) && !os(Linux)
import XCTest
// SwiftPM generates code that is improperly escaped thus we require this to
// compile on iOS & tvOS.
public typealias XCTestCaseEntry = (testCaseClass: XCTestCase.Type, allTests: [(String, (XCTestCase) throws -> Void)])
public func testCase<T: XCTestCase>(_ allTests: [(String, (T) -> () throws -> Void)]) -> XCTestCaseEntry {
fatalError()
}
public func testCase<T: XCTestCase>(_ allTests: [(String, (T) -> () -> Void)]) -> XCTestCaseEntry {
fatalError()
}
#endif

View File

@@ -5,9 +5,12 @@ extension PathTests {
("testBasename", testBasename), ("testBasename", testBasename),
("testCodable", testCodable), ("testCodable", testCodable),
("testConcatenation", testConcatenation), ("testConcatenation", testConcatenation),
("testCopyInto", testCopyInto),
("testEnumeration", testEnumeration), ("testEnumeration", testEnumeration),
("testEnumerationSkippingHiddenFiles", testEnumerationSkippingHiddenFiles),
("testExists", testExists), ("testExists", testExists),
("testIsDirectory", testIsDirectory), ("testIsDirectory", testIsDirectory),
("testJoin", testJoin),
("testMkpathIfExists", testMkpathIfExists), ("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp), ("testMktemp", testMktemp),
("testRelativePathCodable", testRelativePathCodable), ("testRelativePathCodable", testRelativePathCodable),
@@ -15,7 +18,7 @@ extension PathTests {
] ]
} }
#if os(Linux) #if !os(macOS)
public func __allTests() -> [XCTestCaseEntry] { public func __allTests() -> [XCTestCaseEntry] {
return [ return [
testCase(PathTests.__allTests), testCase(PathTests.__allTests),