├── .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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------