Skip to main content

trg/
examples.rs

1//! `trg --examples`: worked examples of every option, printed as text.
2//!
3//! This is living documentation, and `tests/examples.rs` keeps it alive. It
4//! builds the `logs/` tree described in [`FILES`], runs the trg part of every
5//! example against it, and checks the exit code each one claims. It also fails
6//! when an option in `--help` has no example. So a new flag, a renamed one, or
7//! a change in what a command does breaks the build until this file says so.
8
9/// One example: a shell line and what it does.
10pub struct Example {
11    /// What to type. The first `trg …` in it, up to the first unquoted `|`,
12    /// `;`, `&`, `>`, `<`, `)` or newline, is what the test runs; anything
13    /// around it is shell the reader can copy as it stands.
14    pub cmd: &'static str,
15    /// The exit code that trg part gives against the tree in [`FILES`].
16    pub exit: i32,
17    /// Plain prose, wrapped when printed.
18    pub says: &'static str,
19}
20
21pub struct Section {
22    pub title: &'static str,
23    pub intro: &'static str,
24    pub examples: &'static [Example],
25}
26
27/// A file in the example tree.
28pub struct File {
29    pub path: &'static str,
30    /// What `trg -T` lists for it; empty for a file that is not an archive.
31    pub members: &'static [&'static str],
32    pub note: &'static str,
33}
34
35/// The tree every example searches. `tests/examples.rs` builds exactly this.
36pub const FILES: &[File] = &[
37    File {
38        path: "logs/web-2026-08-23.tgz",
39        members: &["access.log", "access.log.1.gz", "error.log", "core.811"],
40        note: "Apache logs, one rotated, and a core dump",
41    },
42    File { path: "logs/web-2026-08-24.tar.zst", members: &["access.log", "error.log"], note: "" },
43    File {
44        path: "logs/2026-07/web-2026-07-31.tar.xz",
45        members: &["access.log", "error.log"],
46        note: "",
47    },
48    File {
49        path: "logs/app-2026-08-24.zip",
50        members: &["app/server.log", "app/worker.log"],
51        note: "",
52    },
53    File { path: "logs/mail-2026-08-24.tar.bz2", members: &["maillog"], note: "" },
54    File { path: "logs/db-2026-08-24.tar.lz4", members: &["slow.log"], note: "" },
55    File {
56        path: "legacy/web-2019-03-01.tar.Z",
57        members: &["access.log"],
58        note: "Unix compress",
59    },
60    File {
61        path: "logs/iis-2026-08-24.zip",
62        members: &["u_ex260824.log"],
63        note: "Windows line endings",
64    },
65    File { path: "current/access.log", members: &[], note: "today's log, not compressed" },
66    File { path: "incoming/download.bin", members: &["access.log"], note: "a .tgz by another name" },
67    File { path: "broken/web-2026-08-25.tgz", members: &[], note: "cut off halfway through" },
68];
69
70pub const SECTIONS: &[Section] = &[
71    Section {
72        title: "FIRST SEARCHES",
73        intro: "A pattern, then one or more archives or directories.",
74        examples: &[
75            Example {
76                cmd: "trg 'POST /checkout' logs/web-2026-08-23.tgz",
77                exit: 0,
78                says: "Search one archive. Each hit prints as archive:member:line:text, \
79                       so you always know which file inside which archive it came from.",
80            },
81            Example {
82                cmd: "trg 'POST /checkout' logs/*.tgz logs/*.tar.zst",
83                exit: 0,
84                says: "Several archives. Output comes in the order the archives were \
85                       named, whatever order they finish in.",
86            },
87            Example {
88                cmd: "trg 'POST /checkout' logs/",
89                exit: 0,
90                says: "A directory is walked recursively and every file in it is \
91                       searched. Each file's format is detected from its content, so \
92                       gzip, zstd, xz, bzip2, lz4, zip and plain files can sit side by \
93                       side.",
94            },
95            Example {
96                cmd: "trg ' 500 ' current/access.log logs/",
97                exit: 0,
98                says: "Uncompressed files are searched too, so today's live log and the \
99                       archived days go in one command.",
100            },
101            Example {
102                cmd: "trg 'POST /checkout' incoming/download.bin",
103                exit: 0,
104                says: "The file name does not matter: this .tgz is read as one because \
105                       of what is in it. There is no -z flag to forget.",
106            },
107            Example {
108                cmd: "trg --examples | less",
109                exit: 0,
110                says: "This text. It is checked against trg on every build, so it \
111                       describes the version that printed it.",
112            },
113            Example { cmd: "trg -V", exit: 0, says: "Which version you have." },
114        ],
115    },
116    Section {
117        title: "PATTERNS",
118        intro: "Patterns are regular expressions with ripgrep's syntax and flags.",
119        examples: &[
120            Example {
121                cmd: "trg -i 'timeout' logs/",
122                exit: 0,
123                says: "Ignore case: finds Timeout, TIMEOUT and timeout.",
124            },
125            Example {
126                cmd: "trg -S 'timeout' logs/",
127                exit: 0,
128                says: "Smart case: an all-lowercase pattern ignores case, and one with \
129                       an uppercase letter (-S 'Timeout') matches it exactly.",
130            },
131            Example {
132                cmd: "trg -w '4711' logs/",
133                exit: 0,
134                says: "Whole words only: finds order 4711 in /api/orders/4711 and \
135                       order=4711, but not job 47110.",
136            },
137            Example {
138                cmd: "trg -F '[proxy:error]' logs/",
139                exit: 0,
140                says: "Fixed string: the brackets are literal. Without -F they would be \
141                       a character class matching any one of the letters inside.",
142            },
143            Example {
144                cmd: "trg -e 'PHP Fatal' -e 'Segmentation fault' logs/",
145                exit: 0,
146                says: "Several patterns: a line matching any of them is a hit. With -e, \
147                       every positional argument is a path.",
148            },
149            Example {
150                cmd: "trg -e '--dry-run' logs/app-2026-08-24.zip",
151                exit: 0,
152                says: "-e also takes a pattern that starts with a dash, which would \
153                       otherwise be read as a flag.",
154            },
155            Example {
156                cmd: "trg '\" 5[0-9]{2} ' logs/",
157                exit: 0,
158                says: "Any 5xx status in an Apache access log: the status follows the \
159                       closing quote of the request.",
160            },
161            Example {
162                cmd: "trg '^10\\.0\\.0\\.7 ' logs/",
163                exit: 0,
164                says: "Everything one client did. ^ anchors at the start of the line, \
165                       and the escaped dots match only dots.",
166            },
167            Example {
168                cmd: "trg '24/Aug/2026:10:0[0-4]' logs/",
169                exit: 0,
170                says: "A time window: 10:00 to 10:04 on 24 August, in Apache's \
171                       timestamp format.",
172            },
173            Example {
174                cmd: "trg -v '\" 200 ' logs/web-2026-08-23.tgz",
175                exit: 0,
176                says: "Invert: every line that is not a 200. A member counts as a hit \
177                       when it has at least one such line.",
178            },
179            Example {
180                cmd: "trg --crlf ' [0-9]{4,}$' logs/iis-2026-08-24.zip",
181                exit: 0,
182                says: "Windows logs end lines with \\r\\n. --crlf lets $ match before \
183                       the \\r. This finds IIS requests whose last field, time-taken, \
184                       is 1000 ms or more.",
185            },
186            Example {
187                cmd: "trg ' [0-9]{4,}$' logs/iis-2026-08-24.zip",
188                exit: 1,
189                says: "The same search without --crlf finds nothing: every line ends in \
190                       \\r, so $ never follows a digit.",
191            },
192            Example {
193                cmd: "trg -a 'Segmentation fault' -g 'core.*' logs/web-2026-08-23.tgz",
194                exit: 0,
195                says: "Search binary members as text. Without -a a member with NUL \
196                       bytes, like this core dump, is skipped and the same command \
197                       exits 1.",
198            },
199        ],
200    },
201    Section {
202        title: "CHOOSING MEMBERS",
203        intro: "-g filters the files inside archives. It never picks archives; \
204                --archive-glob does that.",
205        examples: &[
206            Example {
207                cmd: "trg -T logs/web-2026-08-23.tgz",
208                exit: 0,
209                says: "List the members without searching. -T takes no pattern: every \
210                       argument is a path, so trg -T logs/*.tgz lists all of them.",
211            },
212            Example {
213                cmd: "trg -T -g '*.gz' logs/",
214                exit: 0,
215                says: "-T honours -g, so this shows which members a -g would search.",
216            },
217            Example {
218                cmd: "trg -g 'error.log' 'Timeout' logs/",
219                exit: 0,
220                says: "Only members named error.log. A glob without a / matches the file \
221                       name at any depth inside the archive.",
222            },
223            Example {
224                cmd: "trg -g 'app/*' 'ERROR' logs/app-2026-08-24.zip",
225                exit: 0,
226                says: "A glob with a / matches the whole member path.",
227            },
228            Example {
229                cmd: "trg -g 'access.log*' '^10\\.0\\.0\\.7 ' logs/web-2026-08-23.tgz",
230                exit: 0,
231                says: "access.log and its rotated copies. access.log.1.gz is gzip inside \
232                       the archive; trg decompresses it and searches it as text.",
233            },
234            Example {
235                cmd: "trg -g '!core.*' 'Segmentation fault' logs/",
236                exit: 0,
237                says: "A leading ! excludes. Here: everything but core dumps.",
238            },
239            Example {
240                cmd: "trg -g '*.log' -g '!worker.log' 'ERROR' logs/",
241                exit: 0,
242                says: "Several globs: the last one that matches a member decides. So \
243                       every .log except worker.log.",
244            },
245        ],
246    },
247    Section {
248        title: "CHOOSING ARCHIVES",
249        intro: "--archive-glob picks which files a directory walk searches.",
250        examples: &[
251            Example {
252                cmd: "trg --archive-glob 'web-2026-08-*' ' 500 ' logs/",
253                exit: 0,
254                says: "Only August's web archives. A glob without a / matches the file \
255                       name at any depth below logs/.",
256            },
257            Example {
258                cmd: "trg --archive-glob '2026-07/*' ' 500 ' logs/",
259                exit: 0,
260                says: "A glob with a / matches the path below the directory named: here, \
261                       the files in logs/2026-07/.",
262            },
263            Example {
264                cmd: "trg --archive-glob '!iis-*' ' 200 ' logs/",
265                exit: 0,
266                says: "Everything except the IIS archives.",
267            },
268            Example {
269                cmd: "trg --archive-glob 'web-2025-*' ' 500 ' logs/",
270                exit: 2,
271                says: "A glob that matches no file is exit 2, not a quiet 1: a typo in \
272                       the date must not look like a clean year.",
273            },
274            Example {
275                cmd: "trg --archive-glob '*.zip' ' 500 ' logs/ current/access.log",
276                exit: 0,
277                says: "Files named on the command line are always searched; the glob \
278                       only filters what a directory walk finds.",
279            },
280        ],
281    },
282    Section {
283        title: "OUTPUT",
284        intro: "Output flags mean what they mean in ripgrep.",
285        examples: &[
286            Example {
287                cmd: "trg -C 2 'PHP Fatal' logs/",
288                exit: 0,
289                says: "Two lines of context before and after each hit.",
290            },
291            Example {
292                cmd: "trg -B 3 -A 1 'Segmentation fault' logs/",
293                exit: 0,
294                says: "Three lines before and one after: what led up to the crash.",
295            },
296            Example {
297                cmd: "trg -N 'sqlmap' logs/",
298                exit: 0,
299                says: "No line numbers: archive:member:text.",
300            },
301            Example {
302                cmd: "trg -n 'sqlmap' logs/",
303                exit: 0,
304                says: "Line numbers are on by default. -n turns them back on after an \
305                       -N, say one in a shell alias.",
306            },
307            Example {
308                cmd: "trg -c '\" 5[0-9]{2} ' -g 'access.log*' logs/",
309                exit: 0,
310                says: "Count 5xx responses per member, one line each as \
311                       archive:member:count.",
312            },
313            Example {
314                cmd: "trg -l 'sqlmap' logs/",
315                exit: 0,
316                says: "Only the archive:member names that have a hit.",
317            },
318            Example {
319                cmd: "trg -m 1 'Timeout' logs/",
320                exit: 0,
321                says: "Stop after the first hit in each member: was it there at all, \
322                       and where did it start?",
323            },
324            Example {
325                cmd: "trg -o -N '^[0-9.]+' -g 'access.log*' logs/ |\n  \
326                      awk -F: '{print $NF}' | sort | uniq -c | sort -rn",
327                exit: 0,
328                says: "Top client IPs. -o prints only the match, so each line ends in \
329                       an address and awk takes the last field.",
330            },
331            Example {
332                cmd: "trg -l --archive-sep '|' 'sqlmap' logs/ | cut -d'|' -f1 | sort -u",
333                exit: 0,
334                says: "Which archives (not members) have a hit. --archive-sep changes \
335                       the separator between archive and member, so it cannot clash \
336                       with a : in a member name.",
337            },
338            Example {
339                cmd: "trg --json 'PHP Fatal' logs/ |\n  \
340                      jq -r 'select(.type == \"match\") | .data.path.text'",
341                exit: 0,
342                says: "JSON Lines in ripgrep's format, for scripts. The path field is \
343                       archive:member.",
344            },
345            Example {
346                cmd: "trg --color always ' 500 ' logs/ | less -R",
347                exit: 0,
348                says: "Keep colours when paging. By default colour is used only on a \
349                       terminal (--color auto); --color never turns it off.",
350            },
351            Example {
352                cmd: "trg --escape always 'owned' logs/web-2026-08-24.tar.zst",
353                exit: 0,
354                says: "A log can hold terminal escape sequences, for example planted in \
355                       a User-Agent. On a terminal trg shows them as text (\\x1b) rather \
356                       than letting them act. --escape always does that even when piped; \
357                       --escape never sends the bytes raw.",
358            },
359            Example {
360                cmd: "trg -j 4 --no-sort -l 'sqlmap' logs/",
361                exit: 0,
362                says: "Print each archive's results as soon as it finishes rather than \
363                       in the order given. Only matters with -j above 1.",
364            },
365        ],
366    },
367    Section {
368        title: "FORMATS",
369        intro: "Compression is detected from content. Every format below is a \
370                plain trg PATTERN PATH.",
371        examples: &[
372            Example {
373                cmd: "trg 'ERROR' logs/app-2026-08-24.zip",
374                exit: 0,
375                says: "zip: members are searched like a tar's, compressed or not. An \
376                       encrypted or damaged member is reported and makes the run exit 2; \
377                       the other members are still searched.",
378            },
379            Example {
380                cmd: "trg 'status=bounced' logs/mail-2026-08-24.tar.bz2",
381                exit: 0,
382                says: "bzip2, including the multi-stream files pbzip2 writes.",
383            },
384            Example {
385                cmd: "trg 'Query_time: [1-9]' logs/db-2026-08-24.tar.lz4",
386                exit: 0,
387                says: "lz4: MySQL slow queries over a second.",
388            },
389            Example {
390                cmd: "trg '^10\\.0\\.0\\.7 ' logs/2026-07/web-2026-07-31.tar.xz",
391                exit: 0,
392                says: "xz. zstd (.tar.zst) and gzip (.tgz) work the same way.",
393            },
394            Example {
395                cmd: "trg 'POST /checkout' legacy/web-2019-03-01.tar.Z",
396                exit: 1,
397                says: ".Z (Unix compress) has no length and no checksum, so a cut-off \
398                       file reads as a shorter one. A .Z with no match is still exit 1, \
399                       with a note on stderr that the file could not vouch for its end.",
400            },
401        ],
402    },
403    Section {
404        title: "EXIT CODES AND SCRIPTS",
405        intro: "0: matches, everything read. 1: no matches, everything read. \
406                2: something went unread. Never trust a 1 you did not get.",
407        examples: &[
408            Example {
409                cmd: "trg -q 'sqlmap' logs/\n\
410                      case $? in\n  \
411                        0) echo 'scanner seen' ;;\n  \
412                        1) echo 'clean' ;;\n  \
413                        *) echo 'some logs were not read' >&2 ;;\n\
414                      esac",
415                exit: 0,
416                says: "-q prints nothing; the exit code is the answer. Handle 2 apart \
417                       from 1: if trg -q ...; then treats an unread archive as clean.",
418            },
419            Example {
420                cmd: "trg ' 500 ' broken/web-2026-08-25.tgz",
421                exit: 2,
422                says: "A truncated archive: the hits before the damage are printed, the \
423                       error goes to stderr with the archive's name, and the exit code \
424                       is 2.",
425            },
426            Example {
427                cmd: "trg ' 500 ' logs/ broken/",
428                exit: 2,
429                says: "One bad archive makes the whole run a 2, but every other archive \
430                       is still searched and printed.",
431            },
432            Example {
433                cmd: "trg 'POST /checkout' $LOGS",
434                exit: 2,
435                says: "If $LOGS is unset there is no path, and that is an error. trg \
436                       never falls back to searching the current directory.",
437            },
438            Example {
439                cmd: "trg ' 500 ' logs/ | head -n 3",
440                exit: 0,
441                says: "When the reader stops early, trg stops too and exits 0. The \
442                       archives it had not reached are left unread.",
443            },
444        ],
445    },
446    Section {
447        title: "BIG SWEEPS AND BUSY SERVERS",
448        intro: "By default trg reads one archive at a time at nice 10 and idle I/O \
449                priority, and drops each archive from the page cache once it is \
450                read, so a sweep does not push the server's own data out of memory.",
451        examples: &[
452            Example {
453                cmd: "trg -j 4 ' 500 ' logs/",
454                exit: 0,
455                says: "Four archives at once. Output still comes in the order given.",
456            },
457            Example {
458                cmd: "trg --turbo -c ' 500 ' logs/",
459                exit: 0,
460                says: "One job per CPU at normal priority: for a machine that is yours \
461                       alone. Reads stay at idle I/O priority and the page cache is \
462                       still dropped. -j still sets the job count.",
463            },
464            Example {
465                cmd: "trg -j 8 --load-limit 6 ' 500 ' logs/",
466                exit: 0,
467                says: "Up to eight jobs, but drop to one whenever the load from other \
468                       processes passes 6, and add jobs back as it falls. Checked every \
469                       5 seconds.",
470            },
471            Example {
472                cmd: "trg --nice 19 ' 500 ' logs/",
473                exit: 0,
474                says: "The lowest priority: only use CPU nobody else wants.",
475            },
476            Example {
477                cmd: "trg --no-nice ' 500 ' logs/",
478                exit: 0,
479                says: "Keep the priority you started with.",
480            },
481            Example {
482                cmd: "trg --no-drop-cache ' 500 ' logs/",
483                exit: 0,
484                says: "Leave the archives in the page cache, so searching the same \
485                       archives again is faster.",
486            },
487        ],
488    },
489];
490
491const WIDTH: usize = 78;
492const CMD_INDENT: &str = "  ";
493const SAYS_INDENT: &str = "      ";
494
495/// The whole text of `trg --examples`.
496pub fn render() -> String {
497    let mut s = format!(
498        "trg {} — examples\n\n\
499         Every example searches this tree; adjust the paths to yours.\n\n",
500        env!("CARGO_PKG_VERSION")
501    );
502    let width = FILES.iter().map(|f| f.path.len()).max().unwrap_or(0);
503    for f in FILES {
504        let mut what = f.members.join(", ");
505        if !f.note.is_empty() {
506            if !what.is_empty() {
507                what.push_str(" — ");
508            }
509            what.push_str(f.note);
510        }
511        let first = format!("{CMD_INDENT}{:width$}  ", f.path);
512        let rest = " ".repeat(first.len());
513        wrap_into(&mut s, &what, &first, &rest);
514    }
515    for sec in SECTIONS {
516        s.push_str(&format!("\n{}\n", sec.title));
517        wrap_into(&mut s, sec.intro, "", "");
518        for ex in sec.examples {
519            s.push('\n');
520            for line in ex.cmd.lines() {
521                s.push_str(&format!("{CMD_INDENT}{line}\n"));
522            }
523            wrap_into(&mut s, ex.says, SAYS_INDENT, SAYS_INDENT);
524        }
525    }
526    s
527}
528
529/// Word-wrap `text` to [`WIDTH`], starting the first line with `first` and
530/// every later one with `rest`.
531fn wrap_into(out: &mut String, text: &str, first: &str, rest: &str) {
532    let mut line = String::from(first);
533    let mut empty = true;
534    for word in text.split_whitespace() {
535        if !empty && line.chars().count() + 1 + word.len() > WIDTH {
536            out.push_str(&line);
537            out.push('\n');
538            line = String::from(rest);
539            empty = true;
540        }
541        if !empty {
542            line.push(' ');
543        }
544        line.push_str(word);
545        empty = false;
546    }
547    if !empty {
548        out.push_str(&line);
549        out.push('\n');
550    }
551}