Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Updated
- Improved performance from parser internals rework
- Updated to .NET 8.0
- Improved path traversal performance using `System.IO.Enumeration`: a single recursive
`FileSystemEnumerable` now walks the directory tree natively with glob matching pushed into
the enumeration predicates. Subtrees that cannot match the pattern are pruned before any
`FileSystemInfo` objects are allocated. Sparse patterns like `**/*.csproj` are ~56% faster
with ~70% fewer allocations; directory patterns like `**/bin` are ~22% faster with ~46%
fewer allocations.

### Changed
- Matching full path is now the default for Glob expressions, GlobOptions.MatchFilenameOnly
Expand Down
9 changes: 6 additions & 3 deletions src/Glob/Matcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ namespace GlobExpressions;
internal static class Matcher
{
public static bool MatchesSegment(this DirectorySegment segment, string pathSegment, bool caseSensitive) =>
MatchesSegment(segment, pathSegment.AsSpan(), caseSensitive);

public static bool MatchesSegment(this DirectorySegment segment, ReadOnlySpan<char> pathSegment, bool caseSensitive) =>
MatchesSubSegment(segment.SubSegments, 0, -1, pathSegment, 0, caseSensitive);

private static bool MatchesSubSegment(SubSegment[] segments, int segmentIndex, int literalSetIndex, string pathSegment, int pathIndex, bool caseSensitive)
private static bool MatchesSubSegment(SubSegment[] segments, int segmentIndex, int literalSetIndex, ReadOnlySpan<char> pathSegment, int pathIndex, bool caseSensitive)
{
var nextSegment = segmentIndex + 1;
if (nextSegment > segments.Length)
Expand Down Expand Up @@ -67,6 +70,6 @@ private static bool MatchesSubSegment(SubSegment[] segments, int segmentIndex, i
}
}

private static bool SubstringEquals(string segment, int segmentIndex, string search, bool caseSensitive) =>
string.Compare(segment, segmentIndex, search, 0, search.Length, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) == 0;
private static bool SubstringEquals(ReadOnlySpan<char> segment, int segmentIndex, string search, bool caseSensitive) =>
segment.Slice(segmentIndex, search.Length).Equals(search.AsSpan(), caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
}
112 changes: 112 additions & 0 deletions src/Glob/PathMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using GlobExpressions.AST;

namespace GlobExpressions;

// Span-based glob matching used by the recursive FileSystemEnumerable traversal.
// Matches an entry's path (relative to the traversal root) against the parsed
// segment array without splitting the path into a string[].
internal static class PathMatcher
{
private static readonly char[] Separators = { '/', '\\' };

// Full match: does the relative path match the whole segment pattern?
public static bool IsFullMatch(Segment[] segments, ReadOnlySpan<char> relativePath, bool caseSensitive) =>
Eval(segments, 0, relativePath, caseSensitive);

// Prefix match: could any descendant of the directory at relativeDir match?
// Used for ShouldRecursePredicate to prune subtrees that cannot match.
public static bool CanRecurse(Segment[] segments, ReadOnlySpan<char> relativeDir, bool caseSensitive)
{
var si = 0;
while (true)
{
// Pattern fully consumed with no trailing wildcard: a deeper path
// would be longer than the pattern, so nothing below can match.
if (si == segments.Length)
return false;

// Prefix matched everything so far and segments remain, so a
// descendant may complete the match.
if (relativeDir.IsEmpty)
return true;

SplitHead(relativeDir, out var head, out var rest);

switch (segments[si])
{
case DirectoryWildcard _:
// ** can absorb arbitrary depth, so always worth recursing.
return true;

case DirectorySegment dir:
if (!dir.MatchesSegment(head, caseSensitive))
return false;
si++;
relativeDir = rest;
continue;

default:
return false;
}
}
}

private static bool Eval(Segment[] segments, int si, ReadOnlySpan<char> path, bool caseSensitive)
{
while (true)
{
if (si == segments.Length)
return path.IsEmpty;

// Segments remain but the path is exhausted: no match.
if (path.IsEmpty)
return false;

SplitHead(path, out var head, out var rest);

switch (segments[si])
{
case DirectoryWildcard _:
var isLastSegment = si == segments.Length - 1;

// ** as the final segment matches everything remaining.
if (isLastSegment)
return true;

// Match zero path components against **.
if (Eval(segments, si + 1, path, caseSensitive))
return true;

// Match one component and keep the ** active.
path = rest;
continue;

case DirectorySegment dir:
if (!dir.MatchesSegment(head, caseSensitive))
return false;
si++;
path = rest;
continue;

default:
return false;
}
}
}

private static void SplitHead(ReadOnlySpan<char> path, out ReadOnlySpan<char> head, out ReadOnlySpan<char> rest)
{
var sep = path.IndexOfAny(Separators[0], Separators[1]);
if (sep < 0)
{
head = path;
rest = ReadOnlySpan<char>.Empty;
}
else
{
head = path.Slice(0, sep);
rest = path.Slice(sep + 1);
}
}
}
19 changes: 7 additions & 12 deletions src/Glob/PathTraverser.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using GlobExpressions.AST;

namespace GlobExpressions;
Expand All @@ -11,17 +9,14 @@ internal static class PathTraverser
{
public static IEnumerable<FileSystemInfo> Traverse(this DirectoryInfo root, string pattern, bool caseSensitive, bool emitFiles, bool emitDirectories)
{
var parser = new Parser(pattern);
var segments = parser.ParseTree().Segments;
var parser = new Parser(pattern);
var segments = parser.ParseTree().Segments;

var cache = new TraverseOptions(caseSensitive, emitFiles, emitDirectories);
var options = new TraverseOptions(caseSensitive, emitFiles, emitDirectories);

return segments.Length == 0 ? Array.Empty<FileSystemInfo>() : Traverse(root, segments, cache);
}
return segments.Length == 0 ? Array.Empty<FileSystemInfo>() : Traverse(root, segments, options);
}

internal static IEnumerable<FileSystemInfo> Traverse(DirectoryInfo root, Segment[] segments, TraverseOptions options) =>
Traverse(new List<DirectoryInfo> { root }, segments, options);

private static IEnumerable<FileSystemInfo> Traverse(List<DirectoryInfo> roots, Segment[] segments, TraverseOptions options) =>
new PathTraverserEnumerable(roots, segments, options);
}
new RecursiveGlobEnumerable(root, segments, options);
}
136 changes: 0 additions & 136 deletions src/Glob/PathTraverserEnumerable.cs

This file was deleted.

Loading
Loading