rg — Structured Output

JSON Structured Output

rg’s --json flag emits one JSON object per line — newline-delimited JSON (NDJSON). Each object has a type field: begin (file start), match (a hit), end (file done), summary (final stats). The match type contains data with path, lines, line_number, submatches, and byte offsets.


Basic --json output — understand the schema
cat <<'EOF' > /tmp/rg-json-test.adoc
= Test Page
:description: A test page
// rg/basics — Core search patterns
// Usage: include::partial$codex/rg/basics.adoc[]
// Last updated: 2026-04-11
// Status: POPULATED

== Pattern Search

.Basic pattern search against /etc/passwd
[source,bash]

rg 'root' /etc/passwd

Expected output:

[source,text]

1:root:x:0:0::/root:/bin/bash

`rg` prints line numbers by default, highlights matches in color. No flags needed for the common case.

---

.Case-insensitive search (-i) across docs/
[source,bash]

rg -i 'antora' ~/atelier/_bibliotheca/domus-captures/docs/

`-i` maps to `grep -i`. Searches recursively by default — no `-r` flag needed.

---

.Word match (-w) — prevent partial matches
[source,bash]

rg -w 'log' /etc/passwd

Without `-w`, `log` matches `syslog`, `dialogd`, `catalog`. With `-w`, only whole-word boundaries match. Equivalent to `grep -w` or wrapping the pattern in `\b...\b`.

---

.Fixed string / literal search (-F) — bypass regex engine
[source,bash]

rg -F '$$' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/

`-F` treats the pattern as a literal string. Essential when searching for regex metacharacters (`$`, `.`, `*`, `+`, `?`, `[`, `{`) without escaping each one.

---

.Invert match (-v) — show non-matching lines
[source,bash]

rg -v '^#' /etc/fstab

Prints every line that does NOT match the pattern. Here: all uncommented lines in fstab.

---

.Count matches per file (-c)
[source,bash]

rg -c 'include::' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/grep/

Expected output:

[source,text]

basics.adoc:3 index.adoc:0 gotchas.adoc:2 pcre.adoc:2

Reports count per file. Files with 0 matches are shown too (unlike `grep -c` which omits them by default with `-r`).

---

.First match only (-m 1) — early exit
[source,bash]

rg -m 1 'nologin' /etc/passwd

Stops after the first match in each file. Useful for existence checks in large trees.

---

.Multiline mode (-U) — match across line boundaries
[source,bash]

cat <<'EOF' > /tmp/rg-multiline-test.txt function setup() { echo "init" } EOF rg -U 'function.\{[\s\S]?\}' /tmp/rg-multiline-test.txt

`-U` enables multiline mode. `[\s\S]*?` matches anything including newlines (non-greedy). Without `-U`, `.` never matches `\n` and patterns cannot span lines.

---

.Quiet mode (-q) for scripting — exit code only
[source,bash]

if rg -q 'PermitRootLogin' /etc/ssh/sshd_config; then echo "PermitRootLogin directive found" else echo "PermitRootLogin directive missing" fi

`-q` suppresses all output. Exit code 0 = match found, 1 = no match, 2 = error. Same semantics as `grep -q`.

---

.Multiple patterns with -e — match any of several patterns
[source,bash]

rg -e 'TODO' -e 'FIXME' -e 'HACK' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/partials/

Each `-e` adds an alternative pattern. Lines matching ANY pattern are printed. Equivalent to `grep -e 'A' -e 'B'` or `rg 'TODO|FIXME|HACK'` — but `-e` is cleaner when patterns contain regex metacharacters.
EOF
rg --json 'include::' /tmp/rg-json-test.adoc | head -5

Each line is a self-contained JSON object. The match type is what you usually want — it contains path.text, lines.text, line_number, and submatches (array of match positions). Pipe to jq to extract specific fields.


Extract just filenames from --json output
rg --json 'include::partial' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/rg/ | jq -r 'select(.type == "match") | .data.path.text' | sort -u

select(.type == "match") filters to only match objects. .data.path.text extracts the file path. sort -u deduplicates since each match line emits a separate object.


Extract match text and line numbers
rg --json ':description:' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/rg/ | jq -r 'select(.type == "match") | "\(.data.path.text):\(.data.line_number): \(.data.lines.text | rtrimstr("\n"))"'

Builds a custom file:line: text format from JSON fields. .data.lines.text includes the trailing newline — rtrimstr("\n") strips it. This gives you the same output as normal rg, but you control the format precisely.


Count matches per file from JSON output
rg --json 'include::' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/rg/ | jq -r 'select(.type == "match") | .data.path.text' | sort | uniq -c | sort -rn

The JSON stream has one object per match, so counting path occurrences gives matches-per-file. sort -rn ranks by frequency. Equivalent to rg -c but with the full path and sortable output.


Build CSV from matches — export for spreadsheets
rg --json ':navtitle:' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/rg/ | jq -r 'select(.type == "match") | [(.data.path.text | split("/") | last), .data.line_number, (.data.lines.text | rtrimstr("\n") | ltrimstr(":navtitle: "))] | @csv'

@csv produces RFC 4180 CSV with proper quoting. split("/") | last extracts just the filename from the full path. Pipe to a file: > /tmp/navtitles.csv for import into any spreadsheet tool.


JSON piped to Python one-liner — complex aggregation
rg --json 'xref:' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/rg/index.adoc | python3 -c "
import sys, json
for line in sys.stdin:
    obj = json.loads(line)
    if obj['type'] == 'match':
        for sub in obj['data']['submatches']:
            print(f\"  line {obj['data']['line_number']}: {sub['match']['text']}\")" 2>/dev/null || echo "No xref matches found"

When jq’s filtering gets unwieldy, switch to Python. The submatches array contains the exact matched text and byte offsets — useful for extracting multiple matches per line or building custom reports.


Compare JSON output across two searches
diff <(rg --json 'include::partial' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/rg/ | jq -r 'select(.type == "match") | .data.path.text' | sort -u) <(rg --json 'include::example' ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/pages/codex/rg/ | jq -r 'select(.type == "match") | .data.path.text' | sort -u) || true

Process substitution feeds two rg JSON pipelines into diff. Shows which files use partial includes vs example includes. The || true prevents a non-zero exit when differences exist — expected behavior, not an error.


Aggregate statistics from JSON summary objects
rg --json '.' -t adoc ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/partials/codex/rg/ | jq -r 'select(.type == "summary") | "Files searched: \(.data.stats.searches_with_match)/\(.data.stats.searches) | Matches: \(.data.stats.matches) | Bytes: \(.data.stats.bytes_searched)"'

The summary type object (emitted once at the end) contains aggregate stats: total files searched, files with matches, total matches, bytes processed, and elapsed time. This is the machine-readable equivalent of --stats.