Skip to main content

trg/
print.rs

1//! Printer construction and output-mode dispatch. `path(true)` is mandatory:
2//! without it `sink_with_path` silently degrades to `sink` and member names
3//! vanish.
4
5use grep_printer::{
6    ColorSpecs, Standard, StandardBuilder, Summary, SummaryBuilder, SummaryKind, JSON, JSONBuilder,
7};
8use termcolor::Buffer;
9
10use crate::escape::Escaped;
11
12/// The mutually exclusive output modes.
13///
14/// Mutually exclusive because each one is a different answer to "what is a
15/// result": a line, a count, a path, or nothing at all. `cli` collapses the
16/// flags into exactly one of these, so nothing downstream has to re-derive
17/// precedence.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Mode {
20    /// One line per match — the default.
21    Standard,
22    /// `-c`: one `member:count` line per member that matched.
23    Count,
24    /// `-l`: one `member` line per member that matched.
25    FilesWithMatches,
26    /// `-q`: nothing at all; the exit code is the whole report.
27    Quiet,
28    /// `--json`: one JSON object per event, JSON Lines.
29    Json,
30}
31
32pub struct PrintOpts {
33    pub only_matching: bool,
34    pub column: bool,
35    pub mode: Mode,
36    /// Resolved by `main` from `--color` plus whether stdout is a tty; by the
37    /// time it reaches here it is a decision, not a preference.
38    pub color: bool,
39    /// Resolved the same way as `color`, from `--escape` and stdout's tty-ness.
40    pub escape: bool,
41}
42
43impl Default for PrintOpts {
44    fn default() -> Self {
45        Self {
46            only_matching: false,
47            column: false,
48            mode: Mode::Standard,
49            color: false,
50            escape: false,
51        }
52    }
53}
54
55/// One archive's printer, in whichever mode the run asked for.
56///
57/// An enum rather than a trait object because the three printers' sinks are
58/// unrelated concrete types with different lifetimes, and boxing them would
59/// buy nothing: the match arms live in exactly one place, `search::search_member`.
60pub enum Printer<'a> {
61    Standard(Standard<Escaped<&'a mut Buffer>>),
62    Summary(Summary<Escaped<&'a mut Buffer>>),
63    Json(JSON<Escaped<&'a mut Buffer>>),
64}
65
66pub fn build<'a>(o: &PrintOpts, buf: &'a mut Buffer) -> Printer<'a> {
67    // On a `Buffer` built for a `ColorChoice::Never` writer these specs are
68    // inert, so passing them unconditionally would still be correct; gating
69    // keeps the "did the operator ask for colour" decision visible in one
70    // place instead of implied by the buffer's construction.
71    let specs =
72        if o.color { ColorSpecs::default_with_color() } else { ColorSpecs::default() };
73    // Every printer, not just the standard one: `-l` and `-c` print member
74    // names, and member names come out of the archive.
75    let buf = Escaped::new(buf, o.escape);
76
77    match o.mode {
78        Mode::Standard => Printer::Standard(
79            StandardBuilder::new()
80                .path(true)
81                // Never grouped under a path heading: every line is prefixed
82                // `archive:member:line`, and a heading would hide the archive
83                // the line came from — the one thing the operator is reading
84                // for. `-n`/`-N` reach the printer through `SearchOpts`, which
85                // is what `Searcher` consults, so there is nothing to mirror.
86                .heading(false)
87                .only_matching(o.only_matching)
88                .column(o.column)
89                .stats(false)
90                .color_specs(specs)
91                .build(buf),
92        ),
93        // The JSON schema carries the path, line number and offsets itself, so
94        // none of the standard printer's layout options apply.
95        //
96        // Its output is already JSON, so it gets JSON's escapes, not ours.
97        Mode::Json => Printer::Json(JSONBuilder::new().build(buf.into_json())),
98        // `-c`, `-l` and `-q` are all "one aggregate result per member", which
99        // is exactly what `Summary` is for. Writing the count line by hand
100        // instead would mean re-deriving `exclude_zero`, the field separator
101        // and the path colouring that this printer already gets right.
102        //
103        // `stats` stays off: none of these modes needs the extra statistics,
104        // and enabling it would force `QuietWithMatch` to keep searching a
105        // member after the first match purely to compute numbers nobody reads.
106        Mode::Count | Mode::FilesWithMatches | Mode::Quiet => {
107            let kind = match o.mode {
108                Mode::Count => SummaryKind::Count,
109                Mode::FilesWithMatches => SummaryKind::PathWithMatch,
110                _ => SummaryKind::QuietWithMatch,
111            };
112            Printer::Summary(
113                SummaryBuilder::new()
114                    .kind(kind)
115                    .path(true)
116                    .stats(false)
117                    .color_specs(specs)
118                    .build(buf),
119            )
120        }
121    }
122}