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.

ElGrepApp.cpp — search one file
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.

ElGrepApp.cpp — recursive traversal
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.

Render Safe Unicode Output →