304a1ba133
Add periodic ctrlc_request checks during directory traversal and file processing in the grep_search tool. This allows the tool to respect user cancellation (Ctrl+C) during recursive directory scanning of large codebases. - Changed process_file to return bool indicating cancellation - Added ctrlc_request check in directory iteration loop - Added ctrlc_request check in file processing loop - Returns early with cancellation message when cancelled Closes issue #189
161 lines
6.5 KiB
C++
161 lines
6.5 KiB
C++
#include "grep_tool.hpp"
|
|
#include "json_types.hpp"
|
|
#include "utility.hpp"
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <vector>
|
|
|
|
// Helper to skip binary files by checking for null bytes
|
|
static bool is_text_file(const std::string& path) {
|
|
std::ifstream f(path, std::ios::binary);
|
|
if (!f.is_open()) return false;
|
|
char buffer[512];
|
|
f.read(buffer, sizeof(buffer));
|
|
for (std::streamsize i = 0; i < f.gcount(); ++i) {
|
|
if (buffer[i] == '\0') return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Tool make_grep_tool() {
|
|
return Tool{
|
|
.name = "grep_search",
|
|
.description = "Searches for a pattern in a file or recursively in a directory. "
|
|
"Displays matches with line numbers and optional context. Follows the path as a file or directory.",
|
|
.parameters_schema = {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"path", {
|
|
{"type", "string"},
|
|
{"description", "The file or directory path to search."}
|
|
}},
|
|
{"pattern", {
|
|
{"type", "string"},
|
|
{"description", "The pattern to search for (standard regex)."}
|
|
}},
|
|
{"context", {
|
|
{"type", "integer"},
|
|
{"description", "Number of lines to show before and after the match. Defaults to 0."}
|
|
}},
|
|
{"case_insensitive", {
|
|
{"type", "boolean"},
|
|
{"description", "Perform case-insensitive search. Defaults to false."}
|
|
}},
|
|
{"max_matches", {
|
|
{"type", "integer"},
|
|
{"description", "Max number of matches to display (default 5)"}
|
|
}},
|
|
}},
|
|
{"required", {"path", "pattern"}}
|
|
},
|
|
.execute = create_tool_execute<GrepToolParams>([](const GrepToolParams& params) -> std::string {
|
|
|
|
if (!std::filesystem::exists(params.path))
|
|
return "Error: path does not exist: " + params.path;
|
|
|
|
auto regex_flags = params.case_insensitive ? std::regex_constants::icase : std::regex_constants::ECMAScript;
|
|
auto re_result = make_regex_safely(params.pattern, regex_flags);
|
|
if (!re_result) {
|
|
return "Error: invalid regex pattern: " + re_result.error();
|
|
}
|
|
std::regex re = std::move(re_result.value());
|
|
|
|
std::string output;
|
|
size_t displayed_matches = 0;
|
|
|
|
auto process_file = [&](const std::string& file_path) -> bool {
|
|
if (!is_text_file(file_path)) return false;
|
|
|
|
std::ifstream file(file_path);
|
|
if (!file.is_open()) return false;
|
|
|
|
std::vector<std::string> lines;
|
|
std::string line;
|
|
while (std::getline(file, line)) {
|
|
lines.push_back(line);
|
|
}
|
|
|
|
int last_printed_line = -1;
|
|
for (size_t i = 0; i < lines.size(); ++i) {
|
|
if (ctrlc_request) {
|
|
return true;
|
|
}
|
|
std::smatch match;
|
|
if (std::regex_search(lines[i], match, re)) {
|
|
|
|
if (displayed_matches >= params.max_matches)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
displayed_matches++;
|
|
|
|
int start = std::max(0, static_cast<int>(i) - params.context);
|
|
int end = std::min(static_cast<int>(lines.size()) - 1, static_cast<int>(i) + params.context);
|
|
|
|
for (int j = start; j <= end; ++j) {
|
|
if (j > last_printed_line) {
|
|
bool is_match_line = (j == static_cast<int>(i));
|
|
output += file_path + ":" + std::to_string(j + 1) + ":";
|
|
if (is_match_line) output += "[MATCH] ";
|
|
output += lines[j] + "\n";
|
|
last_printed_line = j;
|
|
}
|
|
}
|
|
output += "=== " + file_path + " ===\n";
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
std::error_code ec;
|
|
if (std::filesystem::is_regular_file(params.path, ec)) {
|
|
if (ec) {
|
|
output += "Error checking path: " + ec.message() + "\n";
|
|
} else {
|
|
if (process_file(params.path)) {
|
|
return "[user requested halt of program]\n" + output;
|
|
}
|
|
}
|
|
} else if (std::filesystem::is_directory(params.path, ec)) {
|
|
if (ec) {
|
|
output += "Error checking path: " + ec.message() + "\n";
|
|
} else {
|
|
auto it = std::filesystem::recursive_directory_iterator(params.path, ec);
|
|
if (ec) {
|
|
output += "Error iterating path: " + ec.message() + "\n";
|
|
} else {
|
|
auto end = std::filesystem::recursive_directory_iterator();
|
|
while (it != end) {
|
|
if (ctrlc_request) {
|
|
return "[user requested halt of program]\n" + output;
|
|
}
|
|
if (std::filesystem::is_regular_file(it->path(), ec)) {
|
|
if (!ec) {
|
|
if (process_file(it->path().string())) {
|
|
return "[user requested halt of program]\n" + output;
|
|
}
|
|
}
|
|
}
|
|
it.increment(ec);
|
|
if (ec) {
|
|
output += "Error iterating path: " + ec.message() + "\n";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (ec) {
|
|
output += "Error checking path: " + ec.message() + "\n";
|
|
} else {
|
|
return "Error: path is neither a regular file nor a directory: " + params.path;
|
|
}
|
|
}
|
|
|
|
return output.empty() ? "(No matches found)" : output;
|
|
})
|
|
};
|
|
}
|