Skip to content

Commit 4b700ca

Browse files
committed
Support glob syntax for input paths
1 parent e5d7aea commit 4b700ca

5 files changed

Lines changed: 90 additions & 58 deletions

File tree

Sources/Arguments.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func parseRules(_ rules: String) throws -> [String] {
235235
}
236236
}
237237

238-
// Parse single file path
238+
// Parse single file path, disallowing globs or commas
239239
func parsePath(_ path: String, for argument: String, in directory: String) throws -> URL {
240240
let expandedPath = expandPath(path, in: directory)
241241
if !FileManager.default.fileExists(atPath: expandedPath.path) {
@@ -249,11 +249,9 @@ func parsePath(_ path: String, for argument: String, in directory: String) throw
249249
return expandedPath
250250
}
251251

252-
// Parse one or more comma-delimited file paths
253-
func parsePaths(_ paths: String, for argument: String, in directory: String) throws -> [URL] {
254-
return try parseCommaDelimitedList(paths).map {
255-
try parsePath($0, for: argument, in: directory)
256-
}
252+
// Parse one or more comma-delimited file paths, expanding globs as required
253+
func parsePaths(_ paths: String, in directory: String) throws -> [URL] {
254+
return try matchGlobs(expandGlobs(paths, in: directory), in: directory)
257255
}
258256

259257
// Merge two dictionaries of arguments

Sources/CommandLine.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -398,16 +398,18 @@ func processArguments(_ args: [String], in directory: String) -> ExitCode {
398398
let fileListURL = try parsePath(fileListPath, for: "filelist", in: directory)
399399
do {
400400
let source = try String(contentsOf: fileListURL)
401-
inputURLs += parseFileList(source, in: fileListURL.deletingLastPathComponent().path)
401+
inputURLs += try parseFileList(source, in: fileListURL.deletingLastPathComponent().path)
402402
} catch {
403403
throw FormatError.options("Failed to read file list at \(fileListPath)")
404404
}
405405
}
406406
var useStdin = false
407407
while let inputPath = args[String(inputURLs.count + 1)] {
408-
inputURLs += try parsePaths(inputPath, for: "input", in: directory)
409408
if inputPath.lowercased() == "stdin" {
410409
useStdin = true
410+
inputURLs.append(URL(string: "stdin")!)
411+
} else {
412+
inputURLs += try parsePaths(inputPath, in: directory)
411413
}
412414
}
413415
if useStdin {
@@ -447,7 +449,7 @@ func processArguments(_ args: [String], in directory: String) -> ExitCode {
447449
if arg.lowercased() == "stdin" {
448450
useStdin = true
449451
} else {
450-
inputURLs += try parsePaths(arg, for: "input", in: directory)
452+
inputURLs += try parsePaths(arg, in: directory)
451453
}
452454
}
453455
try addInputPaths(for: "quiet")
@@ -715,12 +717,11 @@ func processArguments(_ args: [String], in directory: String) -> ExitCode {
715717
}
716718
}
717719

718-
func parseFileList(_ source: String, in directory: String) -> [URL] {
719-
return source
720+
func parseFileList(_ source: String, in directory: String) throws -> [URL] {
721+
return try source
720722
.components(separatedBy: .newlines)
721723
.map { $0.components(separatedBy: "#")[0].trimmingCharacters(in: .whitespaces) }
722-
.filter { !$0.isEmpty }
723-
.map { expandPath($0, in: directory) }
724+
.flatMap { try parsePaths($0, in: directory) }
724725
}
725726

726727
func printResult(_ dryrun: Bool, _ lint: Bool, _ lenient: Bool, _ flags: OutputFlags) -> ExitCode {

Sources/Globs.swift

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ func pathContainsGlobSyntax(_ path: String) -> Bool {
1515
/// Glob type represents either an exact path or wildcard
1616
public enum Glob: CustomStringConvertible {
1717
case path(String)
18-
case regex(NSRegularExpression)
18+
case regex(String, NSRegularExpression)
1919

2020
public func matches(_ path: String) -> Bool {
2121
switch self {
2222
case let .path(_path):
2323
return _path == path
24-
case let .regex(regex):
25-
let range = NSRange(location: 0, length: path.utf16.count)
24+
case let .regex(prefix, regex):
25+
guard path.hasPrefix(prefix) else {
26+
return false
27+
}
28+
let count = prefix.utf16.count
29+
let range = NSRange(location: count, length: path.utf16.count - count)
2630
return regex.firstMatch(in: path, options: [], range: range) != nil
2731
}
2832
}
@@ -31,7 +35,7 @@ public enum Glob: CustomStringConvertible {
3135
switch self {
3236
case let .path(path):
3337
return path
34-
case let .regex(regex):
38+
case let .regex(prefix, regex):
3539
var result = regex.pattern.dropFirst().dropLast()
3640
.replacingOccurrences(of: "([^/]+)?", with: "*")
3741
.replacingOccurrences(of: "(.+/)?", with: "**/")
@@ -42,7 +46,7 @@ public enum Glob: CustomStringConvertible {
4246
let options = result[range].dropFirst().dropLast().components(separatedBy: "|")
4347
result.replaceSubrange(range, with: "{\(options.joined(separator: ","))}")
4448
}
45-
return result
49+
return prefix + result
4650
}
4751
}
4852
}
@@ -70,7 +74,16 @@ public func expandGlobs(_ paths: String, in directory: String) -> [Glob] {
7074
// TODO: should we also handle cases where path includes tokens?
7175
return .path(path)
7276
}
73-
var regex = "^\(path)$"
77+
var prefix = "", regex = ""
78+
let parts = path.components(separatedBy: "/")
79+
for (i, part) in parts.enumerated() {
80+
if pathContainsGlobSyntax(part) || part.contains("<<<") {
81+
regex = parts[i...].joined(separator: "/")
82+
break
83+
}
84+
prefix += "\(part)/"
85+
}
86+
regex = "^\(regex)$"
7487
.replacingOccurrences(of: "[.+(){\\\\|]", with: "\\\\$0", options: .regularExpression)
7588
.replacingOccurrences(of: "?", with: "[^/]")
7689
.replacingOccurrences(of: "**/", with: "(.+/)?")
@@ -79,16 +92,15 @@ public func expandGlobs(_ paths: String, in directory: String) -> [Glob] {
7992
for (token, replacement) in tokens {
8093
regex = regex.replacingOccurrences(of: token, with: replacement)
8194
}
82-
return try! .regex(NSRegularExpression(pattern: regex, options: []))
95+
return try! .regex(prefix, NSRegularExpression(pattern: regex, options: []))
8396
}
8497
}
8598

86-
// NOTE: currently only used for testing
87-
func matchGlobs(_ globs: [Glob], in directory: String) -> [URL] {
99+
func matchGlobs(_ globs: [Glob], in directory: String) throws -> [URL] {
88100
var urls = [URL]()
89101
let keys: [URLResourceKey] = [.isDirectoryKey]
90102
let manager = FileManager.default
91-
func enumerate(_ directory: URL) {
103+
func enumerate(_ directory: URL, with glob: Glob) {
92104
guard let files = try? manager.contentsOfDirectory(
93105
at: directory, includingPropertiesForKeys: keys, options: []
94106
) else {
@@ -97,15 +109,34 @@ func matchGlobs(_ globs: [Glob], in directory: String) -> [URL] {
97109
for url in files {
98110
let path = url.path
99111
var isDirectory: ObjCBool = false
100-
if globs.contains(where: { $0.matches(path) }) {
112+
if glob.matches(path) {
101113
urls.append(url)
102114
} else if manager.fileExists(atPath: path, isDirectory: &isDirectory),
103115
isDirectory.boolValue
104116
{
105-
enumerate(url)
117+
enumerate(url, with: glob)
118+
}
119+
}
120+
}
121+
for glob in globs {
122+
switch glob {
123+
case let .path(path):
124+
if manager.fileExists(atPath: path) {
125+
urls.append(URL(fileURLWithPath: path))
126+
} else {
127+
throw FormatError.options("File not found at \(glob)")
128+
}
129+
case let .regex(path, _):
130+
let count = urls.count
131+
if directory.hasPrefix(path) {
132+
enumerate(URL(fileURLWithPath: directory).standardized, with: glob)
133+
} else if path.hasPrefix(directory) {
134+
enumerate(URL(fileURLWithPath: path).standardized, with: glob)
135+
}
136+
if count == urls.count {
137+
throw FormatError.options("Glob did not match any files at \(glob)")
106138
}
107139
}
108140
}
109-
enumerate(URL(fileURLWithPath: directory).standardized)
110141
return urls
111142
}

Tests/CommandLineTests.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,16 @@ class CommandLineTests: XCTestCase {
211211
func testParseFileList() {
212212
let source = """
213213
#foo
214-
foo.swift #bar
214+
Package.swift #bar
215215
216216
#baz
217-
bar/baz.swift
217+
Sources/Rules.swift
218+
CommandLineTool/*.swift
218219
"""
219-
XCTAssertEqual(parseFileList(source, in: projectDirectory.path), [
220-
URL(fileURLWithPath: "\(projectDirectory.path)/foo.swift"),
221-
URL(fileURLWithPath: "\(projectDirectory.path)/bar/baz.swift"),
220+
XCTAssertEqual(try parseFileList(source, in: projectDirectory.path), [
221+
URL(fileURLWithPath: "\(projectDirectory.path)/Package.swift"),
222+
URL(fileURLWithPath: "\(projectDirectory.path)/Sources/Rules.swift"),
223+
URL(fileURLWithPath: "\(projectDirectory.path)/CommandLineTool/main.swift"),
222224
])
223225
}
224226

0 commit comments

Comments
 (0)