mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
166 lines
6.5 KiB
Swift
166 lines
6.5 KiB
Swift
//
|
|
// Globs.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 31/12/2018.
|
|
// Copyright 2018 Nick Lockwood
|
|
//
|
|
// Distributed under the permissive MIT license
|
|
// Get the latest version from here:
|
|
//
|
|
// https://github.com/nicklockwood/SwiftFormat
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
func pathContainsGlobSyntax(_ path: String) -> Bool {
|
|
"*?[{".contains(where: { path.contains($0) })
|
|
}
|
|
|
|
/// Glob type represents either an exact path or wildcard
|
|
public enum Glob: CustomStringConvertible, Hashable {
|
|
case path(String)
|
|
case regex(String, NSRegularExpression)
|
|
|
|
public func matches(_ path: String) -> Bool {
|
|
switch self {
|
|
case let .path(_path):
|
|
return _path == path
|
|
case let .regex(prefix, regex):
|
|
guard path.hasPrefix(prefix) else {
|
|
return false
|
|
}
|
|
let count = prefix.utf16.count
|
|
let range = NSRange(location: count, length: path.utf16.count - count)
|
|
return regex.firstMatch(in: path, options: [], range: range) != nil
|
|
}
|
|
}
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case let .path(path):
|
|
return path
|
|
case let .regex(prefix, regex):
|
|
var result = regex.pattern.dropFirst().dropLast()
|
|
.replacingOccurrences(of: "([^/]+)?", with: "*")
|
|
.replacingOccurrences(of: "(.+/)?", with: "**/")
|
|
.replacingOccurrences(of: ".+", with: "**")
|
|
.replacingOccurrences(of: "[^/]", with: "?")
|
|
.replacingOccurrences(of: "\\", with: "")
|
|
while let range = result.range(of: "\\([^)]+\\)", options: .regularExpression) {
|
|
let options = result[range].dropFirst().dropLast().components(separatedBy: "|")
|
|
result.replaceSubrange(range, with: "{\(options.joined(separator: ","))}")
|
|
}
|
|
return prefix + result
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Expand one or more comma-delimited file paths using glob syntax
|
|
public func expandGlobs(_ paths: String, in directory: String) -> [Glob] {
|
|
guard pathContainsGlobSyntax(paths) else {
|
|
return parseCommaDelimitedList(paths).map {
|
|
.path(expandPath($0, in: directory).path)
|
|
}
|
|
}
|
|
var paths = paths
|
|
var tokens = [String: String]()
|
|
while let range = paths.range(of: "\\{[^}]+\\}", options: .regularExpression) {
|
|
let options = paths[range].dropFirst().dropLast()
|
|
.replacingOccurrences(of: "[.+(){\\\\|]", with: "\\\\$0", options: .regularExpression)
|
|
.components(separatedBy: ",")
|
|
let token = "<<<\(tokens.count)>>>"
|
|
tokens[token] = "(\(options.joined(separator: "|")))"
|
|
paths.replaceSubrange(range, with: token)
|
|
}
|
|
return parseCommaDelimitedList(paths).map { path -> Glob in
|
|
let path = expandPath(path, in: directory).path
|
|
if FileManager.default.fileExists(atPath: path) {
|
|
// TODO: should we also handle cases where path includes tokens?
|
|
return .path(path)
|
|
}
|
|
var prefix = "", regex = ""
|
|
let parts = path.components(separatedBy: "/")
|
|
for (i, part) in parts.enumerated() {
|
|
if pathContainsGlobSyntax(part) || part.contains("<<<") {
|
|
regex = parts[i...].joined(separator: "/")
|
|
break
|
|
}
|
|
prefix += "\(part)/"
|
|
}
|
|
regex = "^\(regex)$"
|
|
.replacingOccurrences(of: "[.+(){\\\\|]", with: "\\\\$0", options: .regularExpression)
|
|
.replacingOccurrences(of: "?", with: "[^/]")
|
|
.replacingOccurrences(of: "**/", with: "(.+/)?")
|
|
.replacingOccurrences(of: "**", with: ".+")
|
|
.replacingOccurrences(of: "*", with: "([^/]+)?")
|
|
for (token, replacement) in tokens {
|
|
regex = regex.replacingOccurrences(of: token, with: replacement)
|
|
}
|
|
return try! .regex(prefix, NSRegularExpression(pattern: regex, options: []))
|
|
}
|
|
}
|
|
|
|
func matchGlobs(_ globs: [Glob], in directory: String) throws -> [URL] {
|
|
var urls = [URL]()
|
|
let keys: [URLResourceKey] = [.isDirectoryKey]
|
|
let manager = FileManager.default
|
|
func enumerate(_ directory: URL, with glob: Glob) {
|
|
guard let files = try? manager.contentsOfDirectory(
|
|
at: directory, includingPropertiesForKeys: keys, options: []
|
|
) else {
|
|
return
|
|
}
|
|
for url in files {
|
|
let path = url.path
|
|
var isDirectory: ObjCBool = false
|
|
if glob.matches(path) {
|
|
urls.append(url)
|
|
} else if manager.fileExists(atPath: path, isDirectory: &isDirectory),
|
|
isDirectory.boolValue
|
|
{
|
|
enumerate(url, with: glob)
|
|
}
|
|
}
|
|
}
|
|
for glob in globs {
|
|
switch glob {
|
|
case let .path(path):
|
|
if manager.fileExists(atPath: path) {
|
|
urls.append(URL(fileURLWithPath: path))
|
|
} else {
|
|
throw FormatError.options("File not found at \(glob)")
|
|
}
|
|
case let .regex(path, _):
|
|
let count = urls.count
|
|
if directory.hasPrefix(path) {
|
|
enumerate(URL(fileURLWithPath: directory).standardized, with: glob)
|
|
} else if path.hasPrefix(directory) {
|
|
enumerate(URL(fileURLWithPath: path).standardized, with: glob)
|
|
}
|
|
if count == urls.count {
|
|
throw FormatError.options("Glob did not match any files at \(glob)")
|
|
}
|
|
}
|
|
}
|
|
return urls
|
|
}
|