├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── FileScanKit │ ├── FileExtension.swift │ ├── FileScanner.swift │ ├── Option.swift │ └── Recursion.swift └── Tests ├── FileScanKitTests ├── FileScanKitTests.swift ├── Utilities │ └── Project.swift └── XCTestManifests.swift └── LinuxMain.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | macos: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Build 15 | run: swift build 16 | - name: Run tests 17 | run: swift test 18 | linux: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Build 23 | run: swift build 24 | - name: Run tests 25 | run: swift test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yutaro Muta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "PathKit", 6 | "repositoryURL": "https://github.com/kylef/PathKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", 10 | "version": "1.0.0" 11 | } 12 | }, 13 | { 14 | "package": "Spectre", 15 | "repositoryURL": "https://github.com/kylef/Spectre.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 19 | "version": "0.9.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "FileScanKit", 8 | products: [ 9 | .library( 10 | name: "FileScanKit", 11 | targets: ["FileScanKit"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.0"), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "FileScanKit", 19 | dependencies: ["PathKit"]), 20 | .testTarget( 21 | name: "FileScanKitTests", 22 | dependencies: ["FileScanKit"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileScanKit 2 | 3 |

4 | Swift 5.3 5 | Swift Package Manager 6 | Lincense 7 |

8 | 9 | ## Overview 10 | 11 | Scanning file path library for Swift. 12 | 13 | Main use case is to be used with [SwiftSyntax](https://github.com/apple/swift-syntax). 14 | 15 | ### Support 16 | 17 | * Recursion 18 | * all 19 | * depth limit 20 | * FIle extension 21 | * Ignore paths 22 | 23 | > Note: FileScanKit is still in development, and the API is not guaranteed to be stable. It's subject to change without warning. 24 | 25 | ## Requirements 26 | 27 | * Swift 5.3+ 28 | * Xcode 12.4+ 29 | 30 | ## Installation 31 | 32 | ### [Swift Package Manager](https://swift.org/package-manager/) 33 | 34 | ```swift 35 | // swift-tools-version:5.3 36 | // The swift-tools-version declares the minimum version of Swift required to build this package. 37 | 38 | import PackageDescription 39 | 40 | let package = Package( 41 | name: "test", 42 | dependencies: [ 43 | .package(url: "https://github.com/yutailang0119/FileScanKit.git", from: Version(0, 1, 0)), 44 | ], 45 | targets: [ 46 | .target(name: "targetName", dependencies: ["FileScanKit"]), 47 | ] 48 | ) 49 | 50 | ``` 51 | 52 | https://github.com/apple/swift-package-manager 53 | 54 | ## Usage 55 | 56 | ```swift 57 | import Foundation 58 | import FileScanKit 59 | 60 | let path: String = "target/path" 61 | guard let fileScanner = FileScanner(path: path)! 62 | 63 | let recursion: Recursion = .all 64 | let fileExtension: FileExtension = .swift 65 | let ignorePaths: [String] = ["ignore/path"] 66 | let option = Option( 67 | recursion: recursion, 68 | fileExtension: fileExtension, 69 | ignorePaths: ignorePaths 70 | ) 71 | 72 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 73 | 74 | switch result { 75 | case .success(let urls): 76 | // Do something 77 | case .failure(let error): 78 | // Handle error 79 | } 80 | ``` 81 | 82 | ## Author 83 | 84 | [Yutaro Muta](https://github.com/yutailang0119) 85 | * muta.yutaro@gmail.com 86 | * [@yutailang0119](https://twitter.com/yutailang0119) 87 | 88 | ## License 89 | 90 | FileScanKit is available under the MIT license. See [the LICENSE file](./LICENSE) for more info. 91 | This software includes the work that is distributed in the BSD License. 92 | -------------------------------------------------------------------------------- /Sources/FileScanKit/FileExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileExtension.swift 3 | // FileScanKit 4 | // 5 | // Created by Yutaro Muta on 2019/04/11. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum FileExtension { 11 | case swift 12 | case header 13 | case plist 14 | case custom(extension: String) 15 | 16 | var identifier: String { 17 | switch self { 18 | case .swift: 19 | return "swift" 20 | case .header: 21 | return "h" 22 | case .plist: 23 | return "plist" 24 | case .custom(let `extension`): 25 | return `extension` 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/FileScanKit/FileScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileScanner.swift 3 | // FileScanKit 4 | // 5 | // Created by Yutaro Muta on 2019/04/11. 6 | // 7 | 8 | import Foundation 9 | import PathKit 10 | 11 | public struct FileScanner { 12 | private let path: Path 13 | 14 | public init?(path: String?) { 15 | let path = path.flatMap(Path.init(_:)) ?? Path.current 16 | if !path.exists { 17 | return nil 18 | } 19 | self.path = path 20 | } 21 | 22 | public func scan(with option: Option) -> Result<[URL], Error> { 23 | if path.isFile { 24 | let url = scanFile() 25 | return .success([url]) 26 | } else if path.isDirectory { 27 | return scanDirecotry(with: option) 28 | } else { 29 | fatalError() 30 | } 31 | } 32 | } 33 | 34 | extension FileScanner { 35 | private func scanFile() -> URL { 36 | return path.absolute().url 37 | } 38 | 39 | private func scanDirecotry(with option: Option) -> Result<[URL], Error> { 40 | let paths: [Path] 41 | do { 42 | switch option.recursion { 43 | case .all: 44 | paths = try path.recursiveChildren() 45 | case .depth(let limit): 46 | paths = try descend(from: path, limit: limit) 47 | } 48 | } catch { 49 | return .failure(error) 50 | } 51 | let urls = paths.lazy 52 | .filter { $0.extension == option.fileExtension?.identifier } 53 | .filter { path in 54 | !option.ignorePaths 55 | .contains(where: { path.absolute().string.hasPrefix($0.absolute().string ) }) 56 | } 57 | .sorted() 58 | .map { $0.absolute().url } 59 | 60 | return .success(Array(urls)) 61 | } 62 | 63 | private func descend(from path: Path, limit: UInt) throws -> [Path] { 64 | func descend(from paths: [Path]) throws -> [Path] { 65 | return try paths 66 | .filter { $0.isDirectory } 67 | .flatMap { try $0.children() } 68 | } 69 | 70 | var paths: [Path] = [path] 71 | var cursors: [Path] = paths 72 | try (0.. = fileScanner.scan(with: option) 51 | switch result { 52 | case .success(let urls): 53 | let paths = urls.map { $0.absoluteString } 54 | XCTAssertEqual( 55 | paths, 56 | [ 57 | "file://\(rootPath)/Sources/FileScanKit/FileExtension.swift", 58 | "file://\(rootPath)/Sources/FileScanKit/FileScanner.swift", 59 | "file://\(rootPath)/Sources/FileScanKit/Option.swift", 60 | "file://\(rootPath)/Sources/FileScanKit/Recursion.swift", 61 | ] 62 | ) 63 | case .failure(let error): 64 | print(error) 65 | XCTFail() 66 | } 67 | } 68 | 69 | func testDepthScan() { 70 | let path: String = "\(rootPath)" 71 | guard let fileScanner = FileScanner(path: path) else { 72 | XCTFail("path is invalid") 73 | fatalError() 74 | } 75 | 76 | // Depth limit = 0 77 | do { 78 | let recursion: Recursion = .depth(limit: 0) 79 | let option = Option( 80 | recursion: recursion, 81 | fileExtension: .swift, 82 | ignorePaths: [String]() 83 | ) 84 | 85 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 86 | switch result { 87 | case .success(let urls): 88 | let paths = urls.map { $0.absoluteString } 89 | XCTAssertEqual( 90 | paths, 91 | [] 92 | ) 93 | case .failure(let error): 94 | print(error) 95 | XCTFail() 96 | } 97 | } 98 | 99 | // Depth limit = 1 100 | do { 101 | let recursion: Recursion = .depth(limit: 1) 102 | let option = Option( 103 | recursion: recursion, 104 | fileExtension: .swift, 105 | ignorePaths: [String]() 106 | ) 107 | 108 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 109 | switch result { 110 | case .success(let urls): 111 | let paths = urls.map { $0.absoluteString } 112 | XCTAssertEqual( 113 | paths, 114 | [ 115 | "file://\(rootPath)/Package.swift", 116 | ] 117 | ) 118 | case .failure(let error): 119 | print(error) 120 | XCTFail() 121 | } 122 | } 123 | 124 | // Depth limit = 2 125 | do { 126 | let recursion: Recursion = .depth(limit: 2) 127 | let option = Option( 128 | recursion: recursion, 129 | fileExtension: .swift, 130 | ignorePaths: [String]() 131 | ) 132 | 133 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 134 | switch result { 135 | case .success(let urls): 136 | let paths = urls.map { $0.absoluteString } 137 | XCTAssertEqual( 138 | paths, 139 | [ 140 | "file://\(rootPath)/Package.swift", 141 | "file://\(rootPath)/Tests/LinuxMain.swift", 142 | ] 143 | ) 144 | case .failure(let error): 145 | print(error) 146 | XCTFail() 147 | } 148 | } 149 | 150 | // Depth limit = 4 151 | do { 152 | let recursion: Recursion = .depth(limit: 4) 153 | let ignorePaths: [String] = ["\(rootPath)/.build"] 154 | let option = Option( 155 | recursion: recursion, 156 | fileExtension: .swift, 157 | ignorePaths: ignorePaths 158 | ) 159 | 160 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 161 | switch result { 162 | case .success(let urls): 163 | let paths = urls.map { $0.absoluteString } 164 | XCTAssertEqual( 165 | paths, 166 | [ 167 | "file://\(rootPath)/Package.swift", 168 | "file://\(rootPath)/Sources/FileScanKit/FileExtension.swift", 169 | "file://\(rootPath)/Sources/FileScanKit/FileScanner.swift", 170 | "file://\(rootPath)/Sources/FileScanKit/Option.swift", 171 | "file://\(rootPath)/Sources/FileScanKit/Recursion.swift", 172 | "file://\(rootPath)/Tests/FileScanKitTests/FileScanKitTests.swift", 173 | "file://\(rootPath)/Tests/FileScanKitTests/Utilities/Project.swift", 174 | "file://\(rootPath)/Tests/FileScanKitTests/XCTestManifests.swift", 175 | "file://\(rootPath)/Tests/LinuxMain.swift" 176 | ] 177 | ) 178 | case .failure(let error): 179 | print(error) 180 | XCTFail() 181 | } 182 | } 183 | 184 | // Depth limit = 10 185 | do { 186 | let recursion: Recursion = .depth(limit: 10) 187 | let ignorePaths: [String] = ["\(rootPath)/.build"] 188 | let option = Option( 189 | recursion: recursion, 190 | fileExtension: .swift, 191 | ignorePaths: ignorePaths 192 | ) 193 | 194 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 195 | switch result { 196 | case .success(let urls): 197 | let paths = urls.map { $0.absoluteString } 198 | XCTAssertEqual( 199 | paths, 200 | [ 201 | "file://\(rootPath)/Package.swift", 202 | "file://\(rootPath)/Sources/FileScanKit/FileExtension.swift", 203 | "file://\(rootPath)/Sources/FileScanKit/FileScanner.swift", 204 | "file://\(rootPath)/Sources/FileScanKit/Option.swift", 205 | "file://\(rootPath)/Sources/FileScanKit/Recursion.swift", 206 | "file://\(rootPath)/Tests/FileScanKitTests/FileScanKitTests.swift", 207 | "file://\(rootPath)/Tests/FileScanKitTests/Utilities/Project.swift", 208 | "file://\(rootPath)/Tests/FileScanKitTests/XCTestManifests.swift", 209 | "file://\(rootPath)/Tests/LinuxMain.swift" 210 | ] 211 | ) 212 | case .failure(let error): 213 | print(error) 214 | XCTFail() 215 | } 216 | } 217 | 218 | } 219 | 220 | func testFileExtensionScan() { 221 | let path: String = "\(rootPath)/" 222 | guard let fileScanner = FileScanner(path: path) else { 223 | XCTFail("path is invalid") 224 | fatalError() 225 | } 226 | 227 | let fileExtension: FileExtension = .custom(extension: "resolved") 228 | let option = Option( 229 | recursion: .all, 230 | fileExtension: fileExtension, 231 | ignorePaths: [String]() 232 | ) 233 | 234 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 235 | switch result { 236 | case .success(let urls): 237 | let paths = urls.map { $0.absoluteString } 238 | XCTAssertEqual( 239 | paths, 240 | [ 241 | "file://\(rootPath)/Package.resolved", 242 | ] 243 | ) 244 | case .failure(let error): 245 | print(error) 246 | XCTFail() 247 | } 248 | 249 | } 250 | 251 | func testIgnoreTest() { 252 | // Ignore directories 253 | do { 254 | let path: String = "\(rootPath)/" 255 | guard let fileScanner = FileScanner(path: path) else { 256 | XCTFail("path is invalid") 257 | fatalError() 258 | } 259 | 260 | let ignorePaths: [String] = [ 261 | "\(rootPath)/.build", 262 | "\(rootPath)/Tests", 263 | ] 264 | let option = Option( 265 | recursion: .all, 266 | fileExtension: .swift, 267 | ignorePaths: ignorePaths 268 | ) 269 | 270 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 271 | switch result { 272 | case .success(let urls): 273 | let paths = urls.map { $0.absoluteString } 274 | XCTAssertEqual( 275 | paths, 276 | [ 277 | "file://\(rootPath)/Package.swift", 278 | "file://\(rootPath)/Sources/FileScanKit/FileExtension.swift", 279 | "file://\(rootPath)/Sources/FileScanKit/FileScanner.swift", 280 | "file://\(rootPath)/Sources/FileScanKit/Option.swift", 281 | "file://\(rootPath)/Sources/FileScanKit/Recursion.swift", 282 | ] 283 | ) 284 | case .failure(let error): 285 | print(error) 286 | XCTFail() 287 | } 288 | } 289 | 290 | // Ignore files 291 | do { 292 | let path: String = "\(rootPath)/Sources" 293 | guard let fileScanner = FileScanner(path: path) else { 294 | XCTFail("path is invalid") 295 | fatalError() 296 | } 297 | 298 | let ignorePaths: [String] = [ 299 | "\(rootPath)/Sources/FileScanKit/Option.swift", 300 | "\(rootPath)/Sources/FileScanKit/Recursion.swift", 301 | ] 302 | let option = Option( 303 | recursion: .all, 304 | fileExtension: .swift, 305 | ignorePaths: ignorePaths 306 | ) 307 | 308 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 309 | switch result { 310 | case .success(let urls): 311 | let paths = urls.map { $0.absoluteString } 312 | XCTAssertEqual( 313 | paths, 314 | [ 315 | "file://\(rootPath)/Sources/FileScanKit/FileExtension.swift", 316 | "file://\(rootPath)/Sources/FileScanKit/FileScanner.swift", 317 | ] 318 | ) 319 | case .failure(let error): 320 | print(error) 321 | XCTFail() 322 | } 323 | } 324 | 325 | // Invalid ignore paths 326 | do { 327 | let path: String = "\(rootPath)/Sources/" 328 | guard let fileScanner = FileScanner(path: path) else { 329 | XCTFail("path is invalid") 330 | fatalError() 331 | } 332 | 333 | do { 334 | let invalidIgnorePaths: [String] = [ 335 | "\(rootPath)/Sources/FileScanKi", 336 | ] 337 | let option = Option( 338 | recursion: .all, 339 | fileExtension: .swift, 340 | ignorePaths: invalidIgnorePaths 341 | ) 342 | 343 | let result: Result<[URL], Error> = fileScanner.scan(with: option) 344 | switch result { 345 | case .success(let urls): 346 | let paths = urls.map { $0.absoluteString } 347 | XCTAssertEqual( 348 | paths, 349 | [ 350 | "file://\(rootPath)/Sources/FileScanKit/FileExtension.swift", 351 | "file://\(rootPath)/Sources/FileScanKit/FileScanner.swift", 352 | "file://\(rootPath)/Sources/FileScanKit/Option.swift", 353 | "file://\(rootPath)/Sources/FileScanKit/Recursion.swift", 354 | ] 355 | ) 356 | case .failure(let error): 357 | print(error) 358 | XCTFail() 359 | } 360 | } 361 | } 362 | } 363 | 364 | } 365 | -------------------------------------------------------------------------------- /Tests/FileScanKitTests/Utilities/Project.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project.swift 3 | // FileScanKitTests 4 | // 5 | // Created by Yutaro Muta on 2019/04/12. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Project { 11 | var rootPath: String { 12 | let filePath = #file // ParentPath/FileScanKit/Tests/FileScanKitTests/Utilities/ProjectPath.swift 13 | return filePath 14 | .components(separatedBy: "/") 15 | .dropLast(4) 16 | .joined(separator: "/") // ParentPath/FileScanKit 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/FileScanKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(FileScanKitTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import FileScanKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += FileScanKitTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------