Searching Files
Read One Unicode Line at a Time
The file-search method opens a decoded text stream and reads one logical line at a time. It normalizes malformed units through Core’s deterministic replacement handling, then removes only trailing CR and LF characters. All other whitespace is preserved for matching and display.
auto ElGrepApp::searchFile(
const el::Path &path, const el::String &displayPath, const el::re::RegEx &expression) const -> FileResult {
auto result = FileResult{.displayPath = displayPath};
const auto input = path.content().openTextInputStream();
const auto lineEndCharacters = el::CharSet{"\r\n"_el};
auto lineNumber = std::size_t{1};
while (true) {
const auto readResult = input->readLine();
if (readResult.isFinished()) {
break;
}
if (readResult.isTimeout()) {
throw el::ApplicationError{"Reading a searched file timed out."_el};
}
auto line = readResult.data().trimmed(lineEndCharacters, el::StringSide::Back);
const auto matches = expression.collectAll(line);
if (!matches.empty()) {
auto matchingLine = MatchingLine{.number = lineNumber, .text = std::move(line)};
matchingLine.ranges.reserve(matches.size());
for (const auto &match : matches) {
matchingLine.ranges.push_back(match->range());
}
result.matchCount += matches.size();
result.lines.push_back(std::move(matchingLine));
}
++lineNumber;
}
return result;
}
The regular-expression engine reports UTF-8 byte ranges.
These ranges can safely slice the same read-only String without splitting a Unicode
code point.
Only the ranges and matching lines are retained; unmatched lines are released immediately.
collectAll() returns non-overlapping matches in input order.
Applying it to one line at a time gives elgrep conventional line-oriented behavior and keeps memory use bounded by
the matching output rather than the complete input file.
Walk a Directory Deterministically
When --recursive is present, PathWalker visits regular files in a stable
order.
The configuration skips symbolic links so the tutorial cannot accidentally leave the selected tree or enter a cycle.
void ElGrepApp::searchDirectory(const el::Path &path, const el::re::RegEx &expression) const {
auto walkOptions = el::PathWalkOptions{};
walkOptions.setTypes(el::PathType::RegularFile).setSymlinkMode(el::SymlinkMode::Skip);
path.walker().walkOrThrow(
[&](const el::Path &filePath) -> el::PathWalkStatus {
const auto displayPath = filePath.toRelativeOrThrow(path).toString();
printResult(searchFile(filePath, displayPath, expression));
return el::PathWalkStatus::Continue;
},
walkOptions);
}
Each displayed path is relative to the selected directory.
A filesystem error from walkOrThrow() propagates to Application::run() just like an error from reading a file.
Note
This tutorial treats every regular file as UTF-8 text and stops at the first read or traversal error. A production search tool could add binary-file detection, ignore rules, and a policy for continuing after selected errors.