Skip to content

Commit

Permalink
Fix handling of closing bracket as character
Browse files Browse the repository at this point in the history
  • Loading branch information
davbeck committed Jul 8, 2024
1 parent 13458b7 commit 16e647d
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 30 deletions.
24 changes: 17 additions & 7 deletions Sources/Glob/Pattern+Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,18 @@ public extension Pattern {
/// How wildcards are interpreted
public var wildcardBehavior: WildcardBehavior

/// When false, an error will be thrown if an empty range (`[]`) is found.
public var allowsEmptyRanges: Bool
/// How empty ranges (`[]`) are treated
public enum EmptyRangeBehavior: Sendable {
/// Treat an empty range as matching nothing, equivalent to nothing at all
case allow
/// Throw an error when an empty range is used
case error
/// When a range starts with a closing range character, treat the closing bracket as a character and continue the range
case treatClosingBracketAsCharacter
}

/// How are empty ranges handled.
public var emptyRangeBehavior: EmptyRangeBehavior

/// The character used to specify when a range matches characters that aren't in the range.
public var rangeNegationCharacter: Character = "!"
Expand All @@ -36,20 +46,20 @@ public extension Pattern {
/// Default options for parsing and matching patterns.
public static let `default`: Self = .init(
wildcardBehavior: .doubleStarMatchesFullPath,
allowsEmptyRanges: false
emptyRangeBehavior: .error
)

/// Attempts to match the behavior of [VSCode](https://code.visualstudio.com/docs/editor/glob-patterns).
public static let vscode: Self = Options(
wildcardBehavior: .doubleStarMatchesFullPath,
allowsEmptyRanges: false,
emptyRangeBehavior: .error,
rangeNegationCharacter: "^"
)

/// Attempts to match the behavior of [`filepath.Match` in go](https://pkg.go.dev/path/filepath#Match).
public static let go: Self = Options(
wildcardBehavior: .pathComponentsOnly,
allowsEmptyRanges: false,
emptyRangeBehavior: .error,
rangeNegationCharacter: "^"
)

Expand All @@ -58,7 +68,7 @@ public extension Pattern {
public static func posix() -> Self {
Options(
wildcardBehavior: .pathComponentsOnly,
allowsEmptyRanges: true
emptyRangeBehavior: .allow
)
}

Expand All @@ -68,7 +78,7 @@ public extension Pattern {
public static func fnmatch(usePathnameBehavior: Bool = false) -> Self {
Options(
wildcardBehavior: usePathnameBehavior ? .pathComponentsOnly : .singleStarMatchesFullPath,
allowsEmptyRanges: true
emptyRangeBehavior: .treatClosingBracketAsCharacter
)
}

Expand Down
14 changes: 11 additions & 3 deletions Sources/Glob/Pattern.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ public struct Pattern: Sendable {

var ranges: [ClosedRange<Character>] = []

if options.emptyRangeBehavior == .treatClosingBracketAsCharacter && pattern.first == "]" {
// https://man7.org/linux/man-pages/man7/glob.7.html
// The string enclosed by the brackets cannot be empty; therefore ']' can be allowed between the brackets, provided that it is the first character.

pattern = pattern.dropFirst()
ranges.append("]" ... "]")
}

while pattern.first != "]" {
guard pattern.first != "-" else { throw PatternParsingError.rangeMissingBounds }
guard let lower = try getNext() else { break }
Expand All @@ -126,10 +134,10 @@ public struct Pattern: Sendable {
pattern = pattern.dropFirst()

guard !ranges.isEmpty else {
if options.allowsEmptyRanges {
break
} else {
if options.emptyRangeBehavior == .error {
throw PatternParsingError.rangeIsEmpty
} else {
break
}
}

Expand Down
28 changes: 8 additions & 20 deletions Tests/GlobTests/Pattern+fnmatchTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,7 @@ final class PatternFNMatchTests: XCTestCase {
// C "a" "[][abc]" 0
XCTAssertMatchesFNMatch("a", pattern: "[][abc]", flags: 0, result: 0)
// C "a]" "[]a]]" 0
XCTExpectFailure {
XCTAssertMatchesFNMatch("a]", pattern: "[]a]]", flags: 0, result: 0)
}
XCTAssertMatchesFNMatch("a]", pattern: "[]a]]", flags: 0, result: 0)
}

// B.6 010(C)
Expand All @@ -194,17 +192,13 @@ final class PatternFNMatchTests: XCTestCase {
// B.6 011(C)
func test_b_6_011_c() throws {
// C "]" "[][abc]" 0
XCTExpectFailure {
XCTAssertMatchesFNMatch("]", pattern: "[][abc]", flags: 0, result: 0)
}
XCTAssertMatchesFNMatch("]", pattern: "[][abc]", flags: 0, result: 0)
// C "abc]" "[][abc]" NOMATCH
XCTAssertMatchesFNMatch("abc]", pattern: "[][abc]", flags: 0, result: NOMATCH)
// C "[]abc" "[][]abc" NOMATCH
XCTAssertMatchesFNMatch("[]abc", pattern: "[][]abc", flags: 0, result: NOMATCH)
// C "]" "[!]]" NOMATCH
XCTExpectFailure {
XCTAssertMatchesFNMatch("]", pattern: "[!]]", flags: 0, result: NOMATCH)
}
XCTAssertMatchesFNMatch("]", pattern: "[!]]", flags: 0, result: NOMATCH)
// C "aa]" "[!]a]" NOMATCH
XCTAssertMatchesFNMatch("aa]", pattern: "[!]a]", flags: 0, result: NOMATCH)
// C "]" "[!a]" 0
Expand Down Expand Up @@ -991,9 +985,7 @@ final class PatternFNMatchTests: XCTestCase {
// C.UTF-8 "a" "[][abc]" 0
XCTAssertMatchesFNMatch("a", pattern: "[][abc]", flags: 0, result: 0)
// C.UTF-8 "a]" "[]a]]" 0
XCTExpectFailure {
XCTAssertMatchesFNMatch("a]", pattern: "[]a]]", flags: 0, result: 0)
}
XCTAssertMatchesFNMatch("a]", pattern: "[]a]]", flags: 0, result: 0)
}

// B.6 010(C) utf8
Expand All @@ -1008,18 +1000,14 @@ final class PatternFNMatchTests: XCTestCase {

// B.6 011(C) utf8
func test_b_6_011_c_utf8() throws {
XCTExpectFailure {
// C.UTF-8 "]" "[][abc]" 0
XCTAssertMatchesFNMatch("]", pattern: "[][abc]", flags: 0, result: 0)
}
// C.UTF-8 "]" "[][abc]" 0
XCTAssertMatchesFNMatch("]", pattern: "[][abc]", flags: 0, result: 0)
// C.UTF-8 "abc]" "[][abc]" NOMATCH
XCTAssertMatchesFNMatch("abc]", pattern: "[][abc]", flags: 0, result: NOMATCH)
// C.UTF-8 "[]abc" "[][]abc" NOMATCH
XCTAssertMatchesFNMatch("[]abc", pattern: "[][]abc", flags: 0, result: NOMATCH)
XCTExpectFailure {
// C.UTF-8 "]" "[!]]" NOMATCH
XCTAssertMatchesFNMatch("]", pattern: "[!]]", flags: 0, result: NOMATCH)
}
// C.UTF-8 "]" "[!]]" NOMATCH
XCTAssertMatchesFNMatch("]", pattern: "[!]]", flags: 0, result: NOMATCH)
// C.UTF-8 "aa]" "[!]a]" NOMATCH
XCTAssertMatchesFNMatch("aa]", pattern: "[!]a]", flags: 0, result: NOMATCH)
// C.UTF-8 "]" "[!a]" 0
Expand Down

0 comments on commit 16e647d

Please sign in to comment.