Skip to content

Commit 0926de5

Browse files
committed
introduce find and remove commands + rename 'undo' flag to remove
1 parent 1f2377f commit 0926de5

10 files changed

+287
-40
lines changed

README.md

+92-17
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,29 @@ A command-line tool intended to insert a conditional compilation statement in mu
66

77
And generic enough being able to process multiple files and **insert *any* text at the top and the bottom of a file** :)
88

9-
## if os()
9+
## Find files with compiler directive (`#if ... #endif`) hugging the file content
1010

11-
Example:
11+
```bash
12+
swift run conditional-files find .
13+
```
14+
15+
## Find and remove hugging compilerdirective
1216

1317
```bash
14-
swift run conditional-files --ios test.swift
18+
swift run conditional-files find . | xargs swift run conditional-files remove
1519
```
1620

21+
## Add `#if os() ... #endif` to all files in (sub) directory
22+
23+
Set one or more respective flags, e.g. for iOS use `--ios`.
24+
25+
Pass a single dot (`.`) as argument to process all files in the current directory and subdirectories.
26+
27+
Example: `swift run conditional-files --ios .`
28+
1729
<table>
1830
<tr>
19-
<td> File (before) </td> <td> File (after) </td>
31+
<td> File (before) </td> <td> File (after)
2032
</tr>
2133
<tr>
2234
<td>
@@ -40,9 +52,41 @@ import CarKey
4052
</tr>
4153
</table>
4254

43-
You can process all files in the current directory and its sub-folders by specifying a single dot (`.`) as argument. Example: `swift run conditional-files --ios .`
55+
You can add multiple statements by adding multiple flags.
4456

45-
You can also remove an existing compiler directive.
57+
```bash
58+
swift run conditional-files --ios --watchos .
59+
```
60+
61+
<table>
62+
<tr>
63+
<td> File (before) </td> <td> File (after)
64+
</tr>
65+
<tr>
66+
<td>
67+
68+
```swift
69+
import CarKey
70+
// code
71+
```
72+
73+
</td>
74+
<td>
75+
76+
```swift
77+
#if os(iOS) || os(watchOS)
78+
import CarKey
79+
// code
80+
#endif
81+
```
82+
83+
</td>
84+
</tr>
85+
</table>
86+
87+
## Remove `#if os() ... #endif`
88+
89+
You can remove an existing compiler directive with flag `remove`.
4690

4791
```bash
4892
swift run conditional-files --ios --undo test.swift
@@ -74,17 +118,18 @@ import CarKey
74118
</tr>
75119
</table>
76120

77-
## if os() || os() ...
78121

79-
You can add multiple statements.
122+
## Add `#if os() ... #endif` to specific file(s)
123+
124+
Pass one or more file paths as arguments.
80125

81126
```bash
82-
swift run conditional-files --ios --watchos test.swift
127+
swift run conditional-files --ios test.swift
83128
```
84129

85130
<table>
86131
<tr>
87-
<td> File (before) </td> <td> File (after)
132+
<td> File (before) </td> <td> File (after) </td>
88133
</tr>
89134
<tr>
90135
<td>
@@ -98,7 +143,7 @@ import CarKey
98143
<td>
99144

100145
```swift
101-
#if os(iOS) || os(watchOS)
146+
#if os(iOS)
102147
import CarKey
103148
// code
104149
#endif
@@ -108,9 +153,9 @@ import CarKey
108153
</tr>
109154
</table>
110155

111-
## if !os()
156+
## Add `#if !os() ... #endif`
112157

113-
You can negate the #if(os) directive.
158+
You can negate the #if(os) directive with command `not-os`.
114159

115160
```bash
116161
swift run conditional-files not-os --ios --watchos test.swift
@@ -142,9 +187,7 @@ import CarKey
142187
</tr>
143188
</table>
144189

145-
## any (generic)
146-
147-
You can also add any top/bottom lines.
190+
## Add `#if DEBUG ... #endif`
148191

149192
```text
150193
swift run conditional-files generic --first-line '#if DEBUG' --last-line \#endif test.swift
@@ -174,4 +217,36 @@ import CarKey
174217

175218
</td>
176219
</tr>
177-
</table>
220+
</table>
221+
222+
## Add any (generic) top & bottom line
223+
224+
You can also add any top & bottom lines.
225+
226+
```text
227+
swift run conditional-files generic --first-line BEGIN --last-line END test.swift
228+
```
229+
230+
<table>
231+
<tr>
232+
<td> File (before) </td> <td> File (after)
233+
</tr>
234+
<tr>
235+
<td>
236+
237+
```text
238+
// text
239+
```
240+
241+
</td>
242+
<td>
243+
244+
```text
245+
BEGIN
246+
// text
247+
END
248+
```
249+
250+
</td>
251+
</tr>
252+
</table>

Sources/conditional-files/CommandProcessor.swift

+31
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@ struct CommandProcessor: Decodable {
2929
}
3030
}
3131
}
32+
33+
func findFilesWithCompilerDirective(in paths: [String]) -> [URL] {
34+
var conditionalFiles: [URL] = []
35+
36+
let files = fm.getFiles(for: paths, in: FileManager.default.currentDirectoryPath)
37+
38+
for fileURL in files {
39+
guard let fileContent = fm.content(for: fileURL) else { continue }
40+
var lines = fileContent.components(separatedBy: "\n")
41+
if lines.last == "" {
42+
lines.removeLast()
43+
}
44+
guard let first = lines.first, let last = lines.last else { continue }
45+
if first.trimmingCharacters(in: .whitespaces).starts(with: "#if") && last.trimmingCharacters(in: .whitespaces).starts(with: "#endif") {
46+
conditionalFiles.append(fileURL)
47+
}
48+
}
49+
50+
return conditionalFiles
51+
}
52+
53+
func removeCompilerDirective(in paths: [String]) {
54+
let files = findFilesWithCompilerDirective(in: paths)
55+
56+
for fileURL in files {
57+
guard let fileContent = fm.content(for: fileURL) else { continue }
58+
if let updatedFileContent = fileContent.deleteFirstAndLastLine() {
59+
fm.save(updatedFileContent, to: fileURL)
60+
}
61+
}
62+
}
3263
}
3364

3465
/// Abstraction to mock away the access to the filesystem in unit tests

Sources/conditional-files/Commands.swift

+47-8
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ struct OSCommand: ParsableCommand {
5454
var operatingSystems: [OperatingSystem] = []
5555

5656
@Flag(help: "Remove the compiler directive if it exists in the file(s)")
57-
var undo: Bool = false
57+
var remove: Bool = false
5858

5959
@OptionGroup var options: PathFileOptions
6060

@@ -65,11 +65,11 @@ struct OSCommand: ParsableCommand {
6565
throw ValidationError("At least one operating system has to be specified through an respective flag.")
6666
}
6767
}
68-
68+
6969
mutating func run() {
7070
let cd = CompilerDirective(type: .if_os(operatingSystems))
7171

72-
processor.execute(with: options.paths, firstLine: cd.topLine, lastLine: cd.bottomLine, undo: undo)
72+
processor.execute(with: options.paths, firstLine: cd.topLine, lastLine: cd.bottomLine, undo: remove)
7373
}
7474
}
7575

@@ -83,12 +83,12 @@ struct NotOSCommand: ParsableCommand {
8383
var operatingSystems: [OperatingSystem] = []
8484

8585
@Flag(help: "Remove the compiler directive if it exists in the file(s)")
86-
var undo: Bool = false
86+
var remove: Bool = false
8787

8888
@OptionGroup var options: PathFileOptions
8989

9090
var processor: CommandProcessor = .init()
91-
91+
9292
func validate() throws {
9393
guard !operatingSystems.isEmpty else {
9494
throw ValidationError("At least one operating system has to be specified through an respective flag.")
@@ -98,7 +98,7 @@ struct NotOSCommand: ParsableCommand {
9898
mutating func run() {
9999
let cd = CompilerDirective(type: .if_not_os(operatingSystems))
100100

101-
processor.execute(with: options.paths, firstLine: cd.topLine, lastLine: cd.bottomLine, undo: undo)
101+
processor.execute(with: options.paths, firstLine: cd.topLine, lastLine: cd.bottomLine, undo: remove)
102102
}
103103
}
104104

@@ -115,13 +115,52 @@ struct GenericCommand: ParsableCommand {
115115
var bottom: String
116116

117117
@Flag(help: "Remove the top and bottom lines if such exist in the file(s)")
118-
var undo: Bool = false
118+
var remove: Bool = false
119+
120+
@OptionGroup var options: PathFileOptions
121+
122+
var processor: CommandProcessor = .init()
123+
124+
mutating func run() {
125+
processor.execute(with: options.paths, firstLine: top, lastLine: bottom, undo: remove)
126+
}
127+
}
128+
129+
enum FindCommandError: Error {
130+
case notFound
131+
}
132+
133+
struct FindCommand: ParsableCommand {
134+
static var configuration = CommandConfiguration(
135+
commandName: "find",
136+
abstract: "Find conditional files starting with #if and ending with #endif and print out their paths."
137+
)
138+
139+
@OptionGroup var options: PathFileOptions
140+
141+
var processor: CommandProcessor = .init()
142+
143+
mutating func run() throws {
144+
let files = processor.findFilesWithCompilerDirective(in: options.paths)
145+
if files.isEmpty {
146+
throw FindCommandError.notFound
147+
} else {
148+
_ = files.map { print($0.absoluteString.deletingPrefix("file://")) }
149+
}
150+
}
151+
}
152+
153+
struct RemoveTopCompilerDirectiveCommand: ParsableCommand {
154+
static var configuration = CommandConfiguration(
155+
commandName: "remove",
156+
abstract: "Remove compiler directives from conditional files (i.e. starting with #if and ending with #endif)"
157+
)
119158

120159
@OptionGroup var options: PathFileOptions
121160

122161
var processor: CommandProcessor = .init()
123162

124163
mutating func run() {
125-
processor.execute(with: options.paths, firstLine: top, lastLine: bottom, undo: undo)
164+
processor.removeCompilerDirective(in: options.paths)
126165
}
127166
}

Sources/conditional-files/String+Extension.swift

+15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import Foundation
22

33
extension String {
4+
func deleteFirstAndLastLine() -> String? {
5+
var lines = components(separatedBy: "\n")
6+
if lines.last == "" {
7+
lines.removeLast()
8+
}
9+
lines.removeFirst()
10+
lines.removeLast()
11+
return lines.joined(separator: "\n")
12+
}
13+
414
func deleteIfExists(firstLine: String, lastLine: String) -> String? {
515
var lines = components(separatedBy: "\n")
616
if lines.last == "" {
@@ -36,4 +46,9 @@ extension String {
3646
func trimmingTrailingWhiteSpaces() -> String {
3747
return replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
3848
}
49+
50+
func deletingPrefix(_ prefix: String) -> String {
51+
guard hasPrefix(prefix) else { return self }
52+
return String(dropFirst(prefix.count))
53+
}
3954
}

Tests/conditional-files-tests/E2ETests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ final class E2ETest: XCTestCase {
2525
command.operatingSystems = [.ios]
2626
command.options = PathFileOptions()
2727
command.options.paths = [testFilePath]
28-
command.undo = false
28+
command.remove = false
2929

3030
command.run()
3131

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
@testable import conditional_files
2+
import XCTest
3+
4+
final class FindCommandTests: XCTestCase {
5+
func testFindSuccess() throws {
6+
let input = """
7+
#if DEBUG
8+
// code
9+
#endif
10+
"""
11+
12+
let stub = FileManagerMock()
13+
stub.originalFileContent = input
14+
15+
var command = FindCommand()
16+
command.processor = CommandProcessor(fileHandler: stub)
17+
command.options = PathFileOptions()
18+
command.options.paths = ["fakePath"]
19+
20+
try command.run()
21+
22+
XCTAssertEqual(command.processor.findFilesWithCompilerDirective(in: command.options.paths).count, 1)
23+
}
24+
25+
func testFindMiss() throws {
26+
let input = """
27+
// code
28+
"""
29+
30+
let stub = FileManagerMock()
31+
stub.originalFileContent = input
32+
33+
var command = FindCommand()
34+
command.processor = CommandProcessor(fileHandler: stub)
35+
command.options = PathFileOptions()
36+
command.options.paths = ["fakePath"]
37+
38+
XCTAssertThrowsError(try command.run())
39+
40+
XCTAssertEqual(command.processor.findFilesWithCompilerDirective(in: command.options.paths).count, 0)
41+
}
42+
}

Tests/conditional-files-tests/GenericCommandTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ final class GenericCommandTests: XCTestCase {
1919
command.bottom = "bottom"
2020
command.options = PathFileOptions()
2121
command.options.paths = ["fakePath"]
22-
command.undo = false
22+
command.remove = false
2323

2424
command.run()
2525

0 commit comments

Comments
 (0)