coding_tools/cli/ct_search.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! The `ct-search` command grammar (see [`crate::cli`]); the `ct-search` bin is
5//! a thin parse-and-dispatch wrapper over this `Cli`.
6
7use std::path::PathBuf;
8
9use clap::Parser;
10
11use crate::explain::Format;
12use crate::pattern;
13use crate::pulse::HeartbeatOpts;
14use crate::walk::EntryType;
15
16#[derive(Parser, Debug)]
17#[command(
18 name = "ct-search",
19 version,
20 about = "Recursively find files by name, type, size, and content from a chosen root.",
21 long_about = "ct-search combines the predicates you would otherwise assemble from find, xargs, \
22 and grep into one declarative command (also reachable as `ct search`). An entry \
23 matches only when every supplied predicate holds. See `ct-search --explain` for \
24 agent-oriented documentation."
25)]
26#[command(group = clap::ArgGroup::new("output_mode")
27 .args(["list", "summary", "detail", "quiet"])
28 .multiple(false))]
29pub struct Cli {
30 /// Search root (relative or absolute), independent of the current directory.
31 #[arg(long, default_value = ".")]
32 pub base: PathBuf,
33
34 /// File-name pattern; '|'-separated alternatives, each substring->glob->regex promoted and anchored to the whole name.
35 #[arg(long)]
36 pub name: Option<String>,
37
38 /// Restrict to entry kinds: f=file, d=dir, l=symlink (repeatable or comma-joined).
39 #[arg(long, value_enum, value_delimiter = ',')]
40 pub r#type: Vec<EntryType>,
41
42 /// Content pattern (substring->glob->regex promoted); searches file contents. Accepts file:PATH / text:VALUE; a multi-line pattern matches as a line-anchored literal block.
43 #[arg(long)]
44 pub grep: Option<String>,
45
46 /// Pin how patterns are interpreted (promotion off): literal, glob, or regex.
47 #[arg(long, value_enum)]
48 pub mode: Option<pattern::Mode>,
49
50 /// Size predicate [+|-]N[k|m|g]: +N larger than, -N smaller than, N at least N.
51 #[arg(long)]
52 pub size: Option<String>,
53
54 /// Include dot-entries (names starting with '.'); default skips them.
55 #[arg(long)]
56 pub hidden: bool,
57
58 /// Follow symlinks while traversing.
59 #[arg(long)]
60 pub follow: bool,
61
62 /// Walk gitignored / .ignore files too (the .git directory is always skipped); by default the walk skips what git would.
63 #[arg(long)]
64 pub no_ignore: bool,
65
66 /// Stop after N matches.
67 #[arg(long)]
68 pub limit: Option<usize>,
69
70 /// Abort with exit 2 if the search exceeds SECS seconds (fractional allowed).
71 #[arg(long, value_name = "SECS")]
72 pub timeout: Option<f64>,
73
74 #[command(flatten)]
75 pub heartbeat: HeartbeatOpts,
76
77 /// Question this search answers, framing it as a test; printed as a "== ... ==" banner unless --quiet.
78 #[arg(long)]
79 pub question: Option<String>,
80
81 /// Verdict expectation over the match count: any|none|N|=N|+N|-N (default: any). Turns the search into a pass/fail test whose exit status follows the verdict.
82 #[arg(long)]
83 pub expect: Option<String>,
84
85 /// Template written to stdout after the search. Tokens: {RESULT} {QUESTION} {COUNT} {LINES} {BASE} {MATCHES}.
86 #[arg(long, alias = "emit-stdout")]
87 pub emit: Option<String>,
88
89 /// Template written to stderr after the search (same tokens as --emit).
90 #[arg(long)]
91 pub emit_stderr: Option<String>,
92
93 /// Output mode: print one matching path per line (default).
94 #[arg(long)]
95 pub list: bool,
96
97 /// Output mode: print counts only.
98 #[arg(long)]
99 pub summary: bool,
100
101 /// Output mode: print matches plus, for --grep, each hit as path:line:text.
102 #[arg(long)]
103 pub detail: bool,
104
105 /// Output mode: print nothing; report via exit status only.
106 #[arg(long)]
107 pub quiet: bool,
108
109 /// Emit a structured JSON result instead of text (overrides the output mode and --emit).
110 #[arg(long)]
111 pub json: bool,
112
113 /// Print agent usage docs (md or json) and exit.
114 #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
115 pub explain: Option<Format>,
116}