Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ on:
push:
branches:
- master
paths:
- '**/*.swift'
- .github/workflows/checks.yml
jobs:
smoke:
runs-on: ubuntu-latest
Expand Down
52 changes: 25 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,47 +1,34 @@
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

# we want to test more macOS versions but GitHub make that difficult without
# constant maintenance because runners are deprecated and removed
apple:
runs-on: ${{ matrix.os }}
runs-on: macos-latest
strategy:
matrix:
os:
- macos-10.15
- macos-11
platform:
- iOS
- tvOS
- macOS
- watchOS
steps:
- uses: actions/checkout@v2
- uses: mxcl/xcodebuild@v1
- uses: actions/checkout@v6
- uses: mxcl/xcodebuild@latest
with:
platform: ${{ matrix.platform }}
code-coverage: true
warnings-as-errors: true
- uses: codecov/codecov-action@v1
- uses: codecov/codecov-action@v5

linux:
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
swift:
Expand All @@ -51,26 +38,37 @@ jobs:
- swift:5.2
- swift:5.3
- swift:5.4
- swiftlang/swift:nightly-5.5
- swift:5.5
- swift:5.6
- swift:5.7
- swift:5.8
- swift:5.9
- swift:5.10
# - swift:6.0 strangely fails
- swift:6.1
- swift:6.2
container:
image: ${{ matrix.swift }}
steps:
- uses: mxcl/get-swift-version@v1
id: swift
- uses: actions/checkout@v1

- uses: actions/checkout@v2
- name: Get Swift version
id: swift
run: |
ver=$(swift --version | head -1 | sed 's/.*Swift version \([0-9]*\).*/\1/')
echo "marketing-version=$ver" >> $GITHUB_OUTPUT

- 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 }}
if: ${{ steps.swift.outputs.marketing-version > 6.1 }}

- run: su mxcl -c "swift test --parallel $ARGS"

- name: Generate `.lcov`
if: ${{ steps.swift.outputs.marketing-version > 5 }}
if: ${{ steps.swift.outputs.marketing-version > 6.1 }}
run: |
apt-get -qq update && apt-get -qq install curl
b=$(swift build --show-bin-path)
Expand All @@ -82,6 +80,6 @@ jobs:
> info.lcov

- uses: codecov/codecov-action@v1
if: ${{ steps.swift.outputs.marketing-version > 5 }}
if: ${{ steps.swift.outputs.marketing-version > 6.1 }}
with:
file: ./info.lcov
11 changes: 11 additions & 0 deletions Sources/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,22 @@ public extension Bundle {
/// Extensions on `String` that work with `Path` rather than `String` or `URL`
public extension String {
/// Initializes this `String` with the contents of the provided path.
/// - SeeAlso: `init(contentsOf:encoding:)`
@available(macOS, deprecated: 15, message: "Use `init(contentsOf:encoding:)` instead")
@available(iOS, deprecated: 18, message: "Use `init(contentsOf:encoding:)` instead")
@available(tvOS, deprecated: 18, message: "Use `init(contentsOf:encoding:)` instead")
@available(watchOS, deprecated: 11, message: "Use `init(contentsOf:encoding:)` instead")
@inlinable
init<P: Pathish>(contentsOf path: P) throws {
try self.init(contentsOfFile: path.string)
}

/// Initializes this `String` with the contents of the provided path interpreted using a given encoding.
@inlinable
init<P: Pathish>(contentsOf path: P, encoding: String.Encoding) throws {
try self.init(contentsOfFile: path.string, encoding: encoding)
}

/// - Returns: `to` to allow chaining
@inlinable
@discardableResult
Expand Down
46 changes: 39 additions & 7 deletions Sources/Path+ls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,23 +165,47 @@ public extension Pathish {
//MARK: Directory Listing

/**
Same as the `ls` command ∴ output is ”shallow” and unsorted.
Same as the `ls` command ∴ output is "shallow".

- 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`
- WARNING: we actually sort the output :( sorry. Will fix in a major version bump.
- WARNING: ⚠️ **PERFORMANCE**: By default, output is sorted using locale-specific sorting which can be **VERY EXPENSIVE**
for large directories (0.5+ seconds). For better performance, use `.unsorted` or `.aUnsorted` options.
- Note: Sorting will be removed by default in the next major version bump.
*/
func ls(_ options: ListDirectoryOptions? = nil) -> [Path] {
guard let urls = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else {
fputs("warning: could not list: \(self)\n", stderr)
return []
}
return urls.compactMap { url in

let shouldSort: Bool
let includeHidden: Bool

switch options {
case .some(.a):
shouldSort = true
includeHidden = true
case .some(.aUnsorted):
shouldSort = false
includeHidden = true
case .some(.unsorted):
shouldSort = false
includeHidden = false
case .none:
shouldSort = true
includeHidden = false
}

let paths = urls.compactMap { url -> Path? in
guard let path = Path(url.path) else { 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`
if !includeHidden, path.basename().hasPrefix(".") { return nil }
// ^^ we don't use the Foundation `skipHiddenFiles` because it considers weird things hidden and we are mirroring `ls`
return path
}.sorted()
}

return shouldSort ? paths.sorted() : paths
}

/// Recursively find files under this path. If the path is a file, no files will be found.
Expand Down Expand Up @@ -214,7 +238,15 @@ public extension Array where Element == Path {
}

/// Options for `Path.ls(_:)`
///
/// - WARNING: Sorting is locale-specific and can be expensive for large directories (0.5+ seconds).
/// Use `.unsorted` or `.aUnsorted` when you don't need sorted output and performance is critical.
/// - Note: In the next major version, sorting will be removed by default.
public enum ListDirectoryOptions {
/// Lists hidden files also
/// Lists hidden files also (sorted)
case a
/// Lists hidden files also (unsorted for better performance)
case aUnsorted
/// Disables sorting for better performance
case unsorted
}
53 changes: 53 additions & 0 deletions Tests/PathTests/PathTests+ls().swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,57 @@ extension PathTests {
XCTAssertNil(tmpdir.a.find().next())
}
}

func testLsUnsortedOption() throws {
try Path.mktemp { tmpdir in
// Create files with names that would be sorted differently
try tmpdir.join("zebra.txt").touch()
try tmpdir.join("apple.txt").touch()
try tmpdir.join("banana.txt").touch()

// Test default (sorted) behavior
let sortedResults = tmpdir.ls()
XCTAssertEqual(sortedResults.count, 3)
XCTAssertEqual(sortedResults[0].basename(), "apple.txt")
XCTAssertEqual(sortedResults[1].basename(), "banana.txt")
XCTAssertEqual(sortedResults[2].basename(), "zebra.txt")

// Test unsorted behavior - just verify we get all files, order doesn't matter
let unsortedResults = tmpdir.ls(.unsorted)
XCTAssertEqual(unsortedResults.count, 3)
XCTAssertTrue(unsortedResults.contains(tmpdir.join("apple.txt")))
XCTAssertTrue(unsortedResults.contains(tmpdir.join("banana.txt")))
XCTAssertTrue(unsortedResults.contains(tmpdir.join("zebra.txt")))
}
}

func testLsUnsortedWithHidden() throws {
try Path.mktemp { tmpdir in
// Create regular and hidden files
try tmpdir.join("visible.txt").touch()
try tmpdir.join(".hidden.txt").touch()
try tmpdir.join("another.txt").touch()

// Test .a (sorted with hidden)
let sortedWithHidden = tmpdir.ls(.a)
XCTAssertEqual(sortedWithHidden.count, 3)
XCTAssertEqual(sortedWithHidden[0].basename(), ".hidden.txt")
XCTAssertEqual(sortedWithHidden[1].basename(), "another.txt")
XCTAssertEqual(sortedWithHidden[2].basename(), "visible.txt")

// Test .aUnsorted (unsorted with hidden)
let unsortedWithHidden = tmpdir.ls(.aUnsorted)
XCTAssertEqual(unsortedWithHidden.count, 3)
XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join("visible.txt")))
XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join(".hidden.txt")))
XCTAssertTrue(unsortedWithHidden.contains(tmpdir.join("another.txt")))

// Test .unsorted (unsorted without hidden)
let unsortedNoHidden = tmpdir.ls(.unsorted)
XCTAssertEqual(unsortedNoHidden.count, 2)
XCTAssertTrue(unsortedNoHidden.contains(tmpdir.join("visible.txt")))
XCTAssertTrue(unsortedNoHidden.contains(tmpdir.join("another.txt")))
XCTAssertFalse(unsortedNoHidden.contains(tmpdir.join(".hidden.txt")))
}
}
}
7 changes: 7 additions & 0 deletions Tests/PathTests/PathTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,13 @@ class PathTests: XCTestCase {
}
}

func testStringEncodingExtensions() throws {
let string = try String(contentsOf: Path(#file)!, encoding: .utf8)
try Path.mktemp { tmpdir in
_ = try string.write(to: tmpdir.foo, encoding: .utf8)
}
}

func testFileHandleExtensions() throws {
_ = try FileHandle(forReadingAt: Path(#file)!)
_ = try FileHandle(forWritingAt: Path(#file)!)
Expand Down
2 changes: 2 additions & 0 deletions Tests/PathTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ extension PathTests {
("testKind", testKind),
("testLock", testLock),
("testLsOnNonexistentDirectoryReturnsEmptyArray", testLsOnNonexistentDirectoryReturnsEmptyArray),
("testLsUnsortedOption", testLsUnsortedOption),
("testLsUnsortedWithHidden", testLsUnsortedWithHidden),
("testMkpathIfExists", testMkpathIfExists),
("testMktemp", testMktemp),
("testMoveInto", testMoveInto),
Expand Down
Loading