Path.find()
This commit is contained in:
35
README.md
35
README.md
@@ -146,7 +146,8 @@ try Bundle.main.resources.join("foo").copy(to: .home)
|
||||
## Directory listings
|
||||
|
||||
We provide `ls()`, called because it behaves like the Terminal `ls` function,
|
||||
the name thus implies its behavior, ie. that it is not recursive.
|
||||
the name thus implies its behavior, ie. that it is not recursive and doesn’t
|
||||
list hidden files.
|
||||
|
||||
```swift
|
||||
for entry in Path.home.ls() {
|
||||
@@ -169,6 +170,38 @@ let files = Path.home.ls().files
|
||||
let swiftFiles = Path.home.ls().files(withExtension: "swift")
|
||||
```
|
||||
|
||||
We provide `find()` for recursive listing:
|
||||
|
||||
```swift
|
||||
Path.home.find().execute { path in
|
||||
//…
|
||||
}
|
||||
```
|
||||
|
||||
Which is configurable:
|
||||
|
||||
```swift
|
||||
Path.home.find().maxDepth(1).extension("swift").kind(.file) { path in
|
||||
//…
|
||||
}
|
||||
```
|
||||
|
||||
And can be controlled:
|
||||
|
||||
```swift
|
||||
Path.home.find().execute { path in
|
||||
guard foo else { return .skip }
|
||||
guard bar else { return .abort }
|
||||
return .continue
|
||||
}
|
||||
```
|
||||
|
||||
Or just get all paths at once:
|
||||
|
||||
```swift
|
||||
let paths = Path.home.find().execute()
|
||||
```
|
||||
|
||||
# `Path.swift` is robust
|
||||
|
||||
Some parts of `FileManager` are not exactly idiomatic. For example
|
||||
|
||||
@@ -1,49 +1,135 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A file entry from a directory listing.
|
||||
- SeeAlso: `ls()`
|
||||
*/
|
||||
public struct Entry {
|
||||
/// The kind of this directory entry.
|
||||
public enum Kind {
|
||||
/// The path is a file.
|
||||
case file
|
||||
/// The path is a directory.
|
||||
case directory
|
||||
public extension Path {
|
||||
/**
|
||||
A file entry from a directory listing.
|
||||
- SeeAlso: `ls()`
|
||||
*/
|
||||
struct Entry {
|
||||
/// The kind of this directory entry.
|
||||
public enum Kind {
|
||||
/// The path is a file.
|
||||
case file
|
||||
/// The path is a directory.
|
||||
case directory
|
||||
}
|
||||
/// The kind of this entry.
|
||||
public let kind: Kind
|
||||
/// The path of this entry.
|
||||
public let path: Path
|
||||
}
|
||||
|
||||
class Finder {
|
||||
fileprivate init(path: Path) {
|
||||
self.path = path
|
||||
}
|
||||
|
||||
public let path: Path
|
||||
fileprivate(set) public var maxDepth: Int? = nil
|
||||
fileprivate(set) public var kinds: Set<Path.Kind>?
|
||||
fileprivate(set) public var extensions: Set<String>?
|
||||
}
|
||||
/// The kind of this entry.
|
||||
public let kind: Kind
|
||||
/// The path of this entry.
|
||||
public let path: Path
|
||||
}
|
||||
|
||||
public extension Path {
|
||||
public extension Path.Finder {
|
||||
/// Multiple calls will configure the Finder for the final depth call only.
|
||||
func maxDepth(_ maxDepth: Int) -> Path.Finder {
|
||||
#if os(Linux) && !swift(>=5.0)
|
||||
fputs("warning: maxDepth not implemented for Swift < 5\n", stderr)
|
||||
#endif
|
||||
self.maxDepth = maxDepth
|
||||
return self
|
||||
}
|
||||
|
||||
/// Multiple calls will configure the Finder with multiple kinds.
|
||||
func kind(_ kind: Path.Kind) -> Path.Finder {
|
||||
kinds = kinds ?? []
|
||||
kinds!.insert(kind)
|
||||
return self
|
||||
}
|
||||
|
||||
/// Multiple calls will configure the Finder with for multiple extensions
|
||||
func `extension`(_ ext: String) -> Path.Finder {
|
||||
extensions = extensions ?? []
|
||||
extensions!.insert(ext)
|
||||
return self
|
||||
}
|
||||
|
||||
/// Enumerate and return all results, note that this may take a while since we are recursive.
|
||||
func execute() -> [Path] {
|
||||
var rv: [Path] = []
|
||||
execute{ rv.append($0); return .continue }
|
||||
return rv
|
||||
}
|
||||
|
||||
/// The return type for `Path.Finder`
|
||||
enum ControlFlow {
|
||||
/// Stop enumerating this directory, return to the parent.
|
||||
case skip
|
||||
/// Stop enumerating all together.
|
||||
case abort
|
||||
/// Keep going.
|
||||
case `continue`
|
||||
}
|
||||
|
||||
/// Enumerate, one file at a time.
|
||||
func execute(_ closure: (Path) throws -> ControlFlow) rethrows {
|
||||
guard let finder = FileManager.default.enumerator(atPath: path.string) else {
|
||||
fputs("warning: could not enumerate: \(path)\n", stderr)
|
||||
return
|
||||
}
|
||||
while let relativePath = finder.nextObject() as? String {
|
||||
#if !os(Linux) || swift(>=5.0)
|
||||
if let maxDepth = maxDepth, finder.level > maxDepth {
|
||||
finder.skipDescendants()
|
||||
}
|
||||
#endif
|
||||
let path = self.path/relativePath
|
||||
if path == self.path { continue }
|
||||
if let kinds = kinds, let kind = path.kind, !kinds.contains(kind) { continue }
|
||||
if let exts = extensions, !exts.contains(path.extension) { continue }
|
||||
|
||||
switch try closure(path) {
|
||||
case .skip:
|
||||
finder.skipDescendants()
|
||||
case .abort:
|
||||
return
|
||||
case .continue:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Pathish {
|
||||
//MARK: Directory Listings
|
||||
|
||||
/**
|
||||
Same as the `ls -a` command ∴ output is ”shallow” and unsorted.
|
||||
- Parameter includeHiddenFiles: If `true`, hidden files are included in the results. Defaults to `true`.
|
||||
- Important: `includeHiddenFiles` does not work on Linux
|
||||
Same as the `ls` command ∴ output is ”shallow” and unsorted.
|
||||
- Note: as per `ls`, by default we do *not* return hidden files. Specify `.a` for hidden files.
|
||||
- Parameter options: Configure the listing.
|
||||
- Important: On Linux the listing is always `ls -a`
|
||||
*/
|
||||
func ls(includeHiddenFiles: Bool = true) throws -> [Entry] {
|
||||
var opts = FileManager.DirectoryEnumerationOptions()
|
||||
#if !os(Linux)
|
||||
if !includeHiddenFiles {
|
||||
opts.insert(.skipsHiddenFiles)
|
||||
func ls(_ options: ListDirectoryOptions? = nil) -> [Path.Entry] {
|
||||
guard let urls = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else {
|
||||
fputs("warning: could not list: \(self)\n", stderr)
|
||||
return []
|
||||
}
|
||||
#endif
|
||||
let paths = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: opts)
|
||||
func convert(url: URL) -> Entry? {
|
||||
return urls.compactMap { url in
|
||||
guard let path = Path(url.path) else { return nil }
|
||||
return Entry(kind: path.isDirectory ? .directory : .file, path: path)
|
||||
if options != .a, path.basename().hasPrefix(".") { return nil }
|
||||
// ^^ we don’t use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls`
|
||||
return .init(kind: path.isDirectory ? .directory : .file, path: path)
|
||||
}
|
||||
return paths.compactMap(convert)
|
||||
}
|
||||
|
||||
func find() -> Path.Finder {
|
||||
return .init(path: Path(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience functions for the array return value of `Path.ls()`
|
||||
public extension Array where Element == Entry {
|
||||
public extension Array where Element == Path.Entry {
|
||||
/// Filters the list of entries to be a list of Paths that are directories.
|
||||
var directories: [Path] {
|
||||
return compactMap {
|
||||
@@ -65,3 +151,9 @@ public extension Array where Element == Entry {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for `Path.mkdir(_:)`
|
||||
public enum ListDirectoryOptions {
|
||||
/// Creates intermediary directories; works the same as `mkdir -p`.
|
||||
case a
|
||||
}
|
||||
|
||||
70
Tests/PathTests/PathTests+ls().swift
Normal file
70
Tests/PathTests/PathTests+ls().swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import XCTest
|
||||
import Path
|
||||
|
||||
extension PathTests {
|
||||
func testFindMaxDepth0() throws {
|
||||
#if !os(Linux) || swift(>=5)
|
||||
try Path.mktemp { tmpdir in
|
||||
try tmpdir.a.touch()
|
||||
try tmpdir.b.touch()
|
||||
try tmpdir.c.mkdir().join("e").touch()
|
||||
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().maxDepth(0).execute()),
|
||||
Set([tmpdir.a, tmpdir.b, tmpdir.c].map(Path.init)))
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func testFindMaxDepth1() throws {
|
||||
#if !os(Linux) || swift(>=5)
|
||||
try Path.mktemp { tmpdir in
|
||||
try tmpdir.a.touch()
|
||||
try tmpdir.b.mkdir().join("c").touch()
|
||||
try tmpdir.b.d.mkdir().join("e").touch()
|
||||
|
||||
#if !os(Linux)
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().maxDepth(1).execute()),
|
||||
Set([tmpdir.a, tmpdir.b, tmpdir.b.c].map(Path.init)))
|
||||
#else
|
||||
// Linux behavior is different :-/
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().maxDepth(1).execute()),
|
||||
Set([tmpdir.a, tmpdir.b, tmpdir.b.d, tmpdir.b.c].map(Path.init)))
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func testFindExtension() throws {
|
||||
try Path.mktemp { tmpdir in
|
||||
try tmpdir.join("foo.json").touch()
|
||||
try tmpdir.join("bar.txt").touch()
|
||||
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().extension("json").execute()),
|
||||
[tmpdir.join("foo.json")])
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().extension("txt").extension("json").execute()),
|
||||
[tmpdir.join("foo.json"), tmpdir.join("bar.txt")])
|
||||
}
|
||||
}
|
||||
|
||||
func testFindKinds() throws {
|
||||
try Path.mktemp { tmpdir in
|
||||
try tmpdir.foo.mkdir()
|
||||
try tmpdir.bar.touch()
|
||||
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().kind(.file).execute()),
|
||||
[tmpdir.join("bar")])
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().kind(.directory).execute()),
|
||||
[tmpdir.join("foo")])
|
||||
XCTAssertEqual(
|
||||
Set(tmpdir.find().kind(.file).kind(.directory).execute()),
|
||||
Set(["foo", "bar"].map(tmpdir.join)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ extension PathTests {
|
||||
("testFileHandleExtensions", testFileHandleExtensions),
|
||||
("testFileReference", testFileReference),
|
||||
("testFilesystemAttributes", testFilesystemAttributes),
|
||||
("testFindExtension", testFindExtension),
|
||||
("testFindKinds", testFindKinds),
|
||||
("testFindMaxDepth0", testFindMaxDepth0),
|
||||
("testFindMaxDepth1", testFindMaxDepth1),
|
||||
("testFlatMap", testFlatMap),
|
||||
("testInitializerForRelativePath", testInitializerForRelativePath),
|
||||
("testIsDirectory", testIsDirectory),
|
||||
|
||||
Reference in New Issue
Block a user