Description
Background and Motivation
The current Microsoft.Extensions.FileSystemGlobbing.Matcher
does not evaluate include and exclude patterns in the order they are added, which leads to limitations when trying to achieve more complex file-matching behavior. Specifically, includes added after an exclude do not override the exclude, resulting in unexpected or undesired results.
In scenarios where finer control over the inclusion and exclusion of files is needed, the inability to specify an ordered evaluation can prevent the desired matches. Adding an option to allow ordered evaluation of patterns would offer more intuitive control, empowering developers to achieve more precise and flexible file selection.
Originally posted by @Benjin #21362 (comment)
Current Behavior
Consider the following setup in Matcher
:
Matcher m = new();
m.AddInclude("**/*");
m.AddExclude("ExcludeMe/**/*");
m.AddInclude("ExcludeMe/ButActuallyIncludeMe/**/*");
Given this directory structure:
root/
- helloWorld.txt
- ExcludeMe/
- notIncluded.txt
- ButActuallyIncludeMe/
- hiEarth.txt
The output is currently:
root/helloWorld.txt
- (Does NOT include
root/ExcludeMe/ButActuallyIncludeMe/hiEarth.txt
)
Expected Behavior
With ordered evaluation, the output would instead be:
root/helloWorld.txt
root/ExcludeMe/ButActuallyIncludeMe/hiEarth.txt
Revised Proposal
The matcher is just a builder that calls into MatcherContext
internally to do the work. That class takes in the include and exclude patterns separately so change should be made at that level as well.
The ordering is a configuration of the Matcher, so logically it should be passed into the Matcher (either in the constructor or as a property/field) instead of as an argument to Execute
. This is the API I would suggest:
Proposed API
namespace Microsoft.Extensions.FileSystemGlobbing;
public class Matcher
{
public Matcher() { }
public Matcher(StringComparison comparisonType) { }
+ public Matcher(bool ordered) { }
+ public Matcher(StringComparison comparisonType, bool ordered) { }
}
namespace Microsoft.Extensions.FileSystemGlobbing.Internal;
/// <summary>
/// This API supports infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class MatcherContext
+ : MatcherContextBase
{
public MatcherContext(
IEnumerable<IPattern> includePatterns,
IEnumerable<IPattern> excludePatterns,
DirectoryInfoBase directoryInfo,
StringComparison comparison) { }
}
namespace Microsoft.Extensions.FileSystemGlobbing.Internal;
public class OrderedMatcherContext
: MatcherContextBase
{
public OrderedMatcherContext(
IEnumerable<SelectionPattern> patterns,
DirectoryInfoBase directoryInfo,
StringComparison comparison) { }
}
public abstract class MatcherContextBase
{
protected abstract PatternTestResult MatchPatternContexts<TFileInfoBase>(
TFileInfoBase fileinfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
}
public struct SelectionPattern
{
public IPattern pattern;
public SelectionType selectionType;
}
public enum SelectionType
{
Include,
Exclude
}
Usage
var m = new Matcher(true);
m.AddInclude("**/*");
m.AddExclude("ExcludeMe/**/*");
m.AddInclude("ExcludeMe/ButActuallyIncludeMe/**/*");
var result = m.Execute(new DirectoryInfoWrapper(new DirectoryInfo("root")));
foreach (var file in result.Files)
{
Console.WriteLine(file.Path);
}
Original Proposal
API Proposal
To introduce ordered evaluation without altering existing behavior by default, we propose adding an optional ordered
parameter to the Execute
method. This would allow users to explicitly enable ordered evaluation when required.
Proposed API Changes
public virtual Microsoft.Extensions.FileSystemGlobbing.PatternMatchingResult Execute(
- DirectoryInfoBase directoryInfo);
+ DirectoryInfoBase directoryInfo, bool ordered = false);
- Execute Method Parameter: Adds an
ordered
parameter to theExecute
method, allowing ordered evaluation to be enabled per execution while keeping current behavior as the default.
API Usage
// Execute with ordered evaluation
Matcher m = new();
m.AddInclude("**/*");
m.AddExclude("ExcludeMe/**/*");
m.AddInclude("ExcludeMe/ButActuallyIncludeMe/**/*");
var result = m.Execute(new DirectoryInfoWrapper(new DirectoryInfo("root")), ordered: true);
foreach (var file in result.Files)
{
Console.WriteLine(file.Path);
}
Expected output:
root/helloWorld.txt
root/ExcludeMe/ButActuallyIncludeMe/hiEarth.txt
Alternative Designs
-
Matcher Constructor with Ordered Parameter:
Another approach could be adding anordered
parameter to theMatcher
constructor to set the evaluation order for the entire lifecycle of theMatcher
instance, rather than on a per-execution basis:public Matcher(bool ordered = false); public Matcher(StringComparison comparisonType, bool ordered = false);
This approach makes
ordered
a property of theMatcher
instance, reducing the need to specify it with each execution. However, this would make it less flexible for scenarios where a developer may want to toggle ordered evaluation for different executions. -
Separate
ExecuteInOrder
Method:
Another alternative is to create a separate method,ExecuteInOrder
, which explicitly indicates ordered evaluation:public virtual PatternMatchingResult ExecuteInOrder(DirectoryInfoBase directoryInfo);
This approach would keep
Execute
unchanged and provide a clear distinction in the API. However, it would increase the API surface.
Risks
Enabling ordered evaluation may introduce a minor performance cost due to tracking evaluation order. However, this impact is expected to be minimal and only relevant when ordered
is set to true
.