1pub struct Example {
11 pub cmd: &'static str,
15 pub exit: i32,
17 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
27pub struct File {
29 pub path: &'static str,
30 pub members: &'static [&'static str],
32 pub note: &'static str,
33}
34
35pub 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
495pub 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
529fn 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}