SwiftLint/Source/SwiftLintCore/Helpers/Glob.swift

105 lines
3.4 KiB
Swift

import Foundation
#if canImport(Darwin)
import Darwin
private let globFunction = Darwin.glob
#elseif canImport(Glibc)
import Glibc
private let globFunction = Glibc.glob
#else
#error("Unsupported platform")
#endif
// Adapted from https://gist.github.com/efirestone/ce01ae109e08772647eb061b3bb387c3
struct Glob {
static func resolveGlob(_ pattern: String) -> [String] {
let globCharset = CharacterSet(charactersIn: "*?[]")
guard pattern.rangeOfCharacter(from: globCharset) != nil else {
return [pattern]
}
return expandGlobstar(pattern: pattern)
.reduce(into: [String]()) { paths, pattern in
var globResult = glob_t()
defer { globfree(&globResult) }
if globFunction(pattern, GLOB_TILDE | GLOB_BRACE | GLOB_MARK, nil, &globResult) == 0 {
paths.append(contentsOf: populateFiles(globResult: globResult))
}
}
.unique
.sorted()
.map { $0.absolutePathStandardized() }
}
// MARK: Private
private static func expandGlobstar(pattern: String) -> [String] {
guard pattern.contains("**") else {
return [pattern]
}
var results = [String]()
var parts = pattern.components(separatedBy: "**")
let firstPart = parts.removeFirst()
var lastPart = parts.joined(separator: "**")
let fileManager = FileManager.default
var directories: [String]
let searchPath = firstPart.isEmpty ? fileManager.currentDirectoryPath : firstPart
do {
directories = try fileManager.subpathsOfDirectory(atPath: searchPath).compactMap { subpath in
let fullPath = firstPart.bridge().appendingPathComponent(subpath)
guard isDirectory(path: fullPath) else { return nil }
return fullPath
}
} catch {
directories = []
Issue.genericWarning("Error parsing file system item: \(error)").print()
}
// Check the base directory for the glob star as well.
directories.insert(firstPart, at: 0)
// Include the globstar root directory ("dir/") in a pattern like "dir/**" or "dir/**/"
if lastPart.isEmpty {
results.append(firstPart)
lastPart = "*"
}
for directory in directories {
let partiallyResolvedPattern: String
if directory.isEmpty {
partiallyResolvedPattern = lastPart.starts(with: "/") ? String(lastPart.dropFirst()) : lastPart
} else {
partiallyResolvedPattern = directory.bridge().appendingPathComponent(lastPart)
}
results.append(contentsOf: expandGlobstar(pattern: partiallyResolvedPattern))
}
return results
}
private static func isDirectory(path: String) -> Bool {
var isDirectoryBool = ObjCBool(false)
let isDirectory = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectoryBool)
return isDirectory && isDirectoryBool.boolValue
}
private static func populateFiles(globResult: glob_t) -> [String] {
#if os(Linux)
let matchCount = globResult.gl_pathc
#else
let matchCount = globResult.gl_matchc
#endif
return (0..<Int(matchCount)).compactMap { index in
globResult.gl_pathv[index].flatMap { String(validatingUTF8: $0) }
}
}
}