Files
Cpp2Beef/CxxBuilder/src/FileMatcher.bf
2026-03-05 17:02:04 +01:00

157 lines
3.5 KiB
Beef

using System;
using System.IO;
using System.Collections;
using System.Diagnostics;
namespace CxxBuilder;
static class FileMatcher
{
public enum Error
{
ExpectedEscapeSequence,
CharacterClassNotClosed,
}
public static Result<void, Error> HandleMatches(Span<StringView> patterns, StringView directory, delegate void(StringView match) callback)
{
Runtime.Assert(Directory.Exists(directory), scope $"No such directory {directory}");
void Dir(StringView dir)
{
dir: for (let element in Directory.Enumerate(dir))
{
let path = element.GetFilePath(..scope .());
path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
if (element.IsDirectory)
{
Dir(path);
continue;
}
let relpath = Path.GetRelativePath(path, directory, ..scope .());
for (let pattern in patterns)
{
if (!pattern.StartsWith('^') && !pattern.StartsWith('!')) continue;
if (IsMatch(pattern[1...], relpath))
continue dir;
}
for (let pattern in patterns)
{
if (pattern.StartsWith('^') || pattern.StartsWith('!')) continue;
if (!IsMatch(pattern, relpath)) continue;
callback(relpath);
break;
}
}
}
Dir(directory);
return .Ok;
}
public static Result<bool, Error> IsMatch(StringView pattern, StringView path)
{
var pattern, path;
Result<bool, Error> Matches(StringView pattern, char8 c)
{
if (pattern.IsEmpty) return false;
switch (pattern[0])
{
case '*':
if (pattern.Length > 1 && pattern[1] == '*')
return true;
return !Path.IsDirectorySeparatorChar(c);
case '?': return true;
case '\\':
if (pattern.Length <= 1) return .Err(.ExpectedEscapeSequence);
return c == pattern[1];
case '/': return Path.IsDirectorySeparatorChar(c);
case '[':
int i = 0;
char8 Next()
{
if (pattern.Length <= ++i)
return (.)7;
return pattern[i];
}
bool negated = false;
char8 current;
switch (Next())
{
case (.)7: return .Err(.CharacterClassNotClosed);
case '^', '!': negated = true; fallthrough;
case '\\': current = Next();
case ']': return false;
default: current = _;
}
while (true)
{
if (c == current) return !negated;
switch (Next())
{
case ']': return negated;
case '-':
let end = Next();
if (c >= current && c <= end)
return !negated;
current = Next();
case '\\': current = Next();
default: current = _;
}
}
default:
return c == _;
}
}
reduce: while (true)
{
if (path.IsEmpty) return true;
if (pattern.IsEmpty) return false;
switch (Matches(pattern, path[0]))
{
case .Err: return _;
case .Ok(false): return false;
case .Ok:
}
switch (pattern[0])
{
case '*':
StringView lazy;
if (pattern.Length > 1)
{
lazy = pattern;
lazy.RemoveFromStart(pattern[1] == '*' ? 2 : 1);
}
else lazy = .();
defer { pattern = lazy; }
while (true)
{
if (Try!(IsMatch(lazy, path))) continue reduce;
if (!Try!(Matches(pattern, path[0]))) continue reduce;
path.RemoveFromStart(1);
if (path.IsEmpty) return lazy.IsEmpty;
}
case '[':
bool escaped = false;
while (true)
{
let c = pattern[0];
pattern.RemoveFromStart(1);
if (c == ']' && !escaped)
{
path.RemoveFromStart(1);
continue reduce;
}
escaped = c == '\\';
}
case '\\':
pattern.RemoveFromStart(2);
path.RemoveFromStart(1);
default:
pattern.RemoveFromStart(1);
path.RemoveFromStart(1);
}
}
}
}