Skip to content

fix(fcontent): output improvements — exit code + path deduplication#5

Merged
lliWcWill merged 2 commits intolliWcWill:masterfrom
Cwilliams333:fix/fcontent-output-improvements
Feb 11, 2026
Merged

fix(fcontent): output improvements — exit code + path deduplication#5
lliWcWill merged 2 commits intolliWcWill:masterfrom
Cwilliams333:fix/fcontent-output-improvements

Conversation

@Cwilliams333
Copy link
Contributor

Summary

Two fixes for fcontent output handling:

  • Exit code fix: Add missing exit 0 in pretty output mode — script was falling through without explicit exit
  • Path deduplication: When multiple matches occur in the same file, only show full path for first match. Subsequent matches show basename:line:content only

Why

The path deduplication reduces token usage when AI agents consume fcontent output. Same file with many hits no longer repeats the full path on every line.

Before:

/very/long/path/to/file.py:10:match1
/very/long/path/to/file.py:15:match2  
/very/long/path/to/file.py:20:match3

After:

/very/long/path/to/file.py:10:match1
  file.py:15:match2
  file.py:20:match3

Test plan

  • Verified exit code is 0 on successful search
  • Tested deduplication with multi-match files
  • Quiet mode (-q) also dedupes correctly

Pretty mode fell off the end of the script without an explicit exit,
causing the last arithmetic test (( _METACHAR_WARNING == 1 )) to set
the exit code to 1 when no warning was needed. JSON and paths modes
already had exit 0; pretty was the only mode missing it.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The changes enhance output formatting for ripgrep matches in fcontent by introducing intelligent path printing. When processing filename:line:content formatted lines, the code now prints full paths for new files and baselines for repeated files, with per-line formatting tied to a QUIET flag and a fallback mechanism for unmatched formats.

Changes

Cohort / File(s) Summary
Output Formatting Logic
fcontent
Added parsing branch for filename:line:content format recognition. Implements per-file path display logic (full path for new files, basename for repeats) with _PREV_FILE state tracking. Includes QUIET-dependent line formatting and fallback to original output for non-matching lines.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the two main changes: exit code fix and path deduplication. It's specific and reflects the core improvements in the changeset.
Description check ✅ Passed The description clearly explains both fixes with examples, rationale, and test coverage. It relates directly to the changes and provides context for the modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@fcontent`:
- Around line 475-478: The regex ^(.+):([0-9]+):(.*) is greedy and mis-parses
lines (corrupting _cur_file/_cur_line/_cur_text), so replace the re-parsing here
with a lookup into the data you already parsed earlier (the pass that sets those
values around lines 393-398); build an associative array (e.g., declare -A
parsed_lines) during that first pass keyed by the original rg line (or its
unique id) to store filename and line number, and here populate
_cur_file/_cur_line/_cur_text from that map instead of re-running the greedy
regex — keep the same variable names (_cur_file, _cur_line, _cur_text) so
downstream deduplication and basename logic continue working.
- Around line 473-504: The deduplication logic using _PREV_FILE fails when rg
output is not grouped by file (e.g., when users pass --rg-args sorting), so
replace the consecutive-only tracker with a persistent set: declare -A
_SEEN_FILES and mark files as seen when you first print the full-path line; then
for each MATCH_LINES entry (same loop using variables _cur_file, _cur_line,
_cur_text and QUIET), check _SEEN_FILES["$_cur_file"] to decide whether to print
the full path (and set _SEEN_FILES["$_cur_file"]=1) or print the basename-only
line; keep the existing fallback branch for non-matching lines unchanged.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bee9acd and b48ad93.

📒 Files selected for processing (1)
  • fcontent
🧰 Additional context used
📓 Path-based instructions (1)
fcontent

⚙️ CodeRabbit configuration file

fcontent: Focus on: rg invocation safety (args handling), stdin file list handling,
truncation accounting (SHOWN_MATCHES/TOTAL_FILES), and JSON/pretty output
correctness. Avoid changes that could leak private paths in examples.

Files:

  • fcontent
🔇 Additional comments (2)
fcontent (2)

507-507: Exit code fix is correct and necessary.

Without this exit 0, when _METACHAR_WARNING is 0, line 506's (( 0 )) && ... evaluates to exit code 1. While set -e won't trigger (failure is in a && list), the EXIT trap on line 324 captures $?, so the script previously exited with code 1 on successful searches that had no metachar warning. This correctly resolves that.


482-486: Quiet vs non-quiet indentation is inconsistent for visual alignment.

In non-quiet mode, new-file lines get 5-space indent (" ", line 485) while same-file lines get 7-space indent (" ", line 493). The 2-space difference visually groups repeated matches under their file, which seems intentional. Just confirming this is deliberate, since it's not documented.

Also applies to: 490-494

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

fcontent Outdated
Comment on lines 473 to 504
_PREV_FILE=""
for line in "${MATCH_LINES[@]}"; do
if (( QUIET == 1 )); then
echo "$line"
if [[ "$line" =~ ^(.+):([0-9]+):(.*) ]]; then
_cur_file="${BASH_REMATCH[1]}"
_cur_line="${BASH_REMATCH[2]}"
_cur_text="${BASH_REMATCH[3]}"
if [[ "$_cur_file" != "$_PREV_FILE" ]]; then
# New file — show full path
_PREV_FILE="$_cur_file"
if (( QUIET == 1 )); then
echo "$line"
else
echo " ${_cur_file}:${_cur_line}:${_cur_text}"
fi
else
# Same file — show only basename:line:content
_basename="${_cur_file##*/}"
if (( QUIET == 1 )); then
echo "${_basename}:${_cur_line}:${_cur_text}"
else
echo " ${_basename}:${_cur_line}:${_cur_text}"
fi
fi
else
echo " $line"
# Fallback: line doesn't match expected format
if (( QUIET == 1 )); then
echo "$line"
else
echo " $line"
fi
fi
done
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Deduplication assumes rg output is grouped by file — breaks with --rg-args "--sort path" or --rg-args "--sort-files".

The _PREV_FILE tracker only deduplicates consecutive matches from the same file. Default rg groups matches by file, so this works for normal usage. However, if a user passes sorting or other reordering flags via --rg-args, matches from the same file could become non-contiguous, causing basename-only lines to appear without a preceding full-path line for context.

This is a minor edge case since --rg-args is an advanced option, but worth noting. An associative array (declare -A _SEEN_FILES) would handle arbitrary ordering if you want robustness:

Sketch using associative array
-_PREV_FILE=""
+declare -A _SEEN_FILES
 for line in "${MATCH_LINES[@]}"; do
   if [[ "$line" =~ ^(.+):([0-9]+):(.*) ]]; then
     _cur_file="${BASH_REMATCH[1]}"
     _cur_line="${BASH_REMATCH[2]}"
     _cur_text="${BASH_REMATCH[3]}"
-    if [[ "$_cur_file" != "$_PREV_FILE" ]]; then
-      # New file — show full path
-      _PREV_FILE="$_cur_file"
+    if [[ -z "${_SEEN_FILES[$_cur_file]+x}" ]]; then
+      _SEEN_FILES[$_cur_file]=1
       if (( QUIET == 1 )); then
         echo "$line"
       else
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_PREV_FILE=""
for line in "${MATCH_LINES[@]}"; do
if (( QUIET == 1 )); then
echo "$line"
if [[ "$line" =~ ^(.+):([0-9]+):(.*) ]]; then
_cur_file="${BASH_REMATCH[1]}"
_cur_line="${BASH_REMATCH[2]}"
_cur_text="${BASH_REMATCH[3]}"
if [[ "$_cur_file" != "$_PREV_FILE" ]]; then
# New file — show full path
_PREV_FILE="$_cur_file"
if (( QUIET == 1 )); then
echo "$line"
else
echo " ${_cur_file}:${_cur_line}:${_cur_text}"
fi
else
# Same file — show only basename:line:content
_basename="${_cur_file##*/}"
if (( QUIET == 1 )); then
echo "${_basename}:${_cur_line}:${_cur_text}"
else
echo " ${_basename}:${_cur_line}:${_cur_text}"
fi
fi
else
echo " $line"
# Fallback: line doesn't match expected format
if (( QUIET == 1 )); then
echo "$line"
else
echo " $line"
fi
fi
done
_SEEN_FILES=()
declare -A _SEEN_FILES
for line in "${MATCH_LINES[@]}"; do
if [[ "$line" =~ ^(.+):([0-9]+):(.*) ]]; then
_cur_file="${BASH_REMATCH[1]}"
_cur_line="${BASH_REMATCH[2]}"
_cur_text="${BASH_REMATCH[3]}"
if [[ -z "${_SEEN_FILES[$_cur_file]+x}" ]]; then
# New file — show full path
_SEEN_FILES[$_cur_file]=1
if (( QUIET == 1 )); then
echo "$line"
else
echo " ${_cur_file}:${_cur_line}:${_cur_text}"
fi
else
# Same file — show only basename:line:content
_basename="${_cur_file##*/}"
if (( QUIET == 1 )); then
echo "${_basename}:${_cur_line}:${_cur_text}"
else
echo " ${_basename}:${_cur_line}:${_cur_text}"
fi
fi
else
# Fallback: line doesn't match expected format
if (( QUIET == 1 )); then
echo "$line"
else
echo " $line"
fi
fi
done
🤖 Prompt for AI Agents
In `@fcontent` around lines 473 - 504, The deduplication logic using _PREV_FILE
fails when rg output is not grouped by file (e.g., when users pass --rg-args
sorting), so replace the consecutive-only tracker with a persistent set: declare
-A _SEEN_FILES and mark files as seen when you first print the full-path line;
then for each MATCH_LINES entry (same loop using variables _cur_file, _cur_line,
_cur_text and QUIET), check _SEEN_FILES["$_cur_file"] to decide whether to print
the full path (and set _SEEN_FILES["$_cur_file"]=1) or print the basename-only
line; keep the existing fallback branch for non-matching lines unchanged.

fcontent Outdated
Comment on lines 475 to 478
if [[ "$line" =~ ^(.+):([0-9]+):(.*) ]]; then
_cur_file="${BASH_REMATCH[1]}"
_cur_line="${BASH_REMATCH[2]}"
_cur_text="${BASH_REMATCH[3]}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Greedy .+ regex can mismatch when file content starts with digits:.

^(.+):([0-9]+):(.*) is greedy, so for rg output like /path/file:10:42:actual content, the regex backtracks and captures group 1 as /path/file:10, group 2 as 42, group 3 as actual content — wrong file and line number.

This is a pre-existing pattern (same regex on line 395), so not introduced here. But since this PR adds deduplication that depends on _cur_file being correct, the mismatch would now cause visible bugs: the wrong "file" string means deduplication groups incorrectly and basenames are derived from a corrupted path.

A non-greedy or anchored approach is hard in bash regex. One pragmatic fix: reuse the filename extracted on line 393-398 (which already parsed the same lines) by building an associative map during that pass instead of re-parsing here. Or accept the known limitation with a comment.

🤖 Prompt for AI Agents
In `@fcontent` around lines 475 - 478, The regex ^(.+):([0-9]+):(.*) is greedy and
mis-parses lines (corrupting _cur_file/_cur_line/_cur_text), so replace the
re-parsing here with a lookup into the data you already parsed earlier (the pass
that sets those values around lines 393-398); build an associative array (e.g.,
declare -A parsed_lines) during that first pass keyed by the original rg line
(or its unique id) to store filename and line number, and here populate
_cur_file/_cur_line/_cur_text from that map instead of re-running the greedy
regex — keep the same variable names (_cur_file, _cur_line, _cur_text) so
downstream deduplication and basename logic continue working.

@Cwilliams333 Cwilliams333 force-pushed the fix/fcontent-output-improvements branch from b48ad93 to 7a39524 Compare February 11, 2026 07:16
@lliWcWill
Copy link
Owner

Hey — nice work on the path dedup and the exit 0 fix. Both are solid. The [^:]+ regex update is good too.

One change we'd like before merging: swap _PREV_FILE for declare -A _SEEN_FILES (associative array).

Why: The consecutive-only tracker breaks if someone passes --rg-args "--sort path" or any other reordering flag. Same file can appear non-contiguously, and then basename-only lines show up without a preceding full-path line for context. The associative array handles arbitrary ordering.

fmap already uses declare -A (line 699), so this is an established pattern in the suite. Bash 4+ required, which we're well past.

Here's the sketch:

-_PREV_FILE=""
+declare -A _SEEN_FILES
 for line in "${MATCH_LINES[@]}"; do
   if [[ "$line" =~ ^([^:]+):([0-9]+):(.*) ]]; then
     _cur_file="${BASH_REMATCH[1]}"
     _cur_line="${BASH_REMATCH[2]}"
     _cur_text="${BASH_REMATCH[3]}"
-    if [[ "$_cur_file" != "$_PREV_FILE" ]]; then
-      _PREV_FILE="$_cur_file"
+    if [[ -z "${_SEEN_FILES[$_cur_file]+x}" ]]; then
+      _SEEN_FILES[$_cur_file]=1

Everything else stays the same. Should be a quick swap.

When multiple matches occur in the same file, only show the full path
for the first match. Subsequent matches show basename:line:content only.

This reduces token usage when the same file has many hits, making
output more compact for AI agent consumption.

Before:
  /very/long/path/to/file.py:10:match1
  /very/long/path/to/file.py:15:match2
  /very/long/path/to/file.py:20:match3

After:
  /very/long/path/to/file.py:10:match1
    file.py:15:match2
    file.py:20:match3
@Cwilliams333 Cwilliams333 force-pushed the fix/fcontent-output-improvements branch from 7a39524 to 4623d40 Compare February 11, 2026 07:20
@Cwilliams333
Copy link
Contributor Author

Done — swapped _PREV_FILE for declare -A _SEEN_FILES as requested.

Now handles non-contiguous file ordering (e.g., --rg-args "--sort path"). Same file appearing later in results will still get basename-only treatment since we track all seen files, not just the previous one.

-_PREV_FILE=""
+declare -A _SEEN_FILES
 for line in "${MATCH_LINES[@]}"; do
   if [[ "$line" =~ ^([^:]+):([0-9]+):(.*) ]]; then
     _cur_file="${BASH_REMATCH[1]}"
-    if [[ "$_cur_file" != "$_PREV_FILE" ]]; then
-      _PREV_FILE="$_cur_file"
+    if [[ -z "${_SEEN_FILES[$_cur_file]+x}" ]]; then
+      _SEEN_FILES[$_cur_file]=1

Ready for re-review.

@lliWcWill lliWcWill merged commit 6423d56 into lliWcWill:master Feb 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants