146 lines
3.9 KiB
Dart
146 lines
3.9 KiB
Dart
import "dart:io";
|
|
|
|
import "base_tool.dart";
|
|
|
|
|
|
class GlobTool extends BaseTool {
|
|
@override
|
|
final String name = "Glob";
|
|
|
|
@override
|
|
final String description =
|
|
"Fast file pattern matching. Supports glob patterns like \"**/*.js\". "
|
|
"Returns matching file paths sorted by modification time.";
|
|
|
|
@override
|
|
Future<String> execute(Map<String, dynamic> input) async {
|
|
final pattern = requireString(input, "pattern");
|
|
final pathArg = optionalString(input, "path");
|
|
|
|
final searchDir = pathArg != null ? Directory(pathArg) : Directory.current;
|
|
|
|
if (!await searchDir.exists()) {
|
|
return "Error: Directory does not exist: ${searchDir.path}";
|
|
}
|
|
|
|
final start = DateTime.now();
|
|
|
|
final matched = <FileSystemEntity>[];
|
|
|
|
await for (final entity in searchDir.list(recursive: true, followLinks: false)) {
|
|
if (entity is File) {
|
|
final rel = entity.path.substring(searchDir.path.length);
|
|
// strip leading slash
|
|
final relClean = rel.startsWith("/") ? rel.substring(1) : rel;
|
|
if (_matchGlob(pattern, relClean)) {
|
|
matched.add(entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
const maxResults = 100;
|
|
final truncated = matched.length > maxResults;
|
|
final limited = truncated ? matched.sublist(0, maxResults) : matched;
|
|
|
|
// sort by mtime desc (same as original)
|
|
final withStat = <MapEntry<FileSystemEntity, DateTime>>[];
|
|
for (final f in limited) {
|
|
try {
|
|
final st = await f.stat();
|
|
withStat.add(MapEntry(f, st.modified));
|
|
} catch (_) {
|
|
withStat.add(MapEntry(f, DateTime.fromMillisecondsSinceEpoch(0)));
|
|
}
|
|
}
|
|
|
|
withStat.sort((a, b) => b.value.compareTo(a.value));
|
|
|
|
final filenames = withStat.map((e) {
|
|
final rel = e.key.path.substring(searchDir.path.length);
|
|
return rel.startsWith("/") ? rel.substring(1) : rel;
|
|
}).toList();
|
|
|
|
final durationMs = DateTime.now().difference(start).inMilliseconds;
|
|
|
|
if (filenames.isEmpty) {
|
|
return "No files found";
|
|
}
|
|
|
|
final buf = StringBuffer();
|
|
for (final f in filenames) {
|
|
buf.writeln(f);
|
|
}
|
|
|
|
if (truncated) {
|
|
buf.writeln("(Results are truncated. Consider using a more specific path or pattern.)");
|
|
}
|
|
|
|
// small summary footer
|
|
buf.write("Found ${filenames.length} file${filenames.length == 1 ? "" : "s"} in ${durationMs}ms");
|
|
|
|
return buf.toString();
|
|
}
|
|
|
|
// basic glob matching - handles ** and *
|
|
bool _matchGlob(String pattern, String path) {
|
|
return _globMatch(pattern, path);
|
|
}
|
|
|
|
bool _globMatch(String pattern, String text) {
|
|
// convert glob to regex-ish check
|
|
final regexStr = _globToRegex(pattern);
|
|
final regex = RegExp(regexStr);
|
|
return regex.hasMatch(text);
|
|
}
|
|
|
|
String _globToRegex(String pattern) {
|
|
final buf = StringBuffer("^");
|
|
|
|
int i = 0;
|
|
while (i < pattern.length) {
|
|
final c = pattern[i];
|
|
|
|
if (c == "*") {
|
|
if (i + 1 < pattern.length && pattern[i + 1] == "*") {
|
|
// ** matches anything including slashes
|
|
buf.write(".*");
|
|
i += 2;
|
|
// skip optional trailing slash after **
|
|
if (i < pattern.length && pattern[i] == "/") i++;
|
|
} else {
|
|
// * matches anything except slash
|
|
buf.write("[^/]*");
|
|
i++;
|
|
}
|
|
} else if (c == "?") {
|
|
buf.write("[^/]");
|
|
i++;
|
|
} else if (c == ".") {
|
|
buf.write("\\.");
|
|
i++;
|
|
} else if (c == "{") {
|
|
// {a,b,c} style alternation
|
|
final close = pattern.indexOf("}", i);
|
|
if (close == -1) {
|
|
buf.write("\\{");
|
|
i++;
|
|
} else {
|
|
final inner = pattern.substring(i + 1, close);
|
|
final parts = inner.split(",").map(_globToRegex).join("|");
|
|
buf.write("(?:$parts)");
|
|
i = close + 1;
|
|
}
|
|
} else if ("[]()|^".contains(c)) {
|
|
buf.write("\\$c");
|
|
i++;
|
|
} else {
|
|
buf.write(c);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
buf.write(r"$");
|
|
return buf.toString();
|
|
}
|
|
}
|