coding_tools/cli/ct_each.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! The `ct-each` command grammar (see [`crate::cli`]); the `ct-each` bin is a
5//! 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;
14
15#[derive(Parser, Debug)]
16#[command(
17 name = "ct-each",
18 version,
19 about = "Run a command template once per item (no shell), with per-item verdicts and an aggregate --expect.",
20 long_about = "ct-each dispatches one command over a set of distinct items: {ITEM} and {INDEX} \
21 expand inside the argv elements after `--`, each expansion is launched directly \
22 (never through a shell), each run is classified by exit status, and the SUCCESS \
23 count is judged against --expect (also reachable as `ct each`). See \
24 `ct-each --explain` for agent-oriented documentation."
25)]
26pub struct Cli {
27 /// Items to dispatch over, in order (repeatable; one run per item). file:PATH expands to the file's non-empty lines; text:VALUE is one literal item.
28 #[arg(long, num_args = 1.., value_name = "ITEM")]
29 pub items: Vec<String>,
30
31 /// Pin how --name/--ext walker patterns are interpreted (promotion off): literal, glob, or regex.
32 #[arg(long, value_enum)]
33 pub mode: Option<pattern::Mode>,
34
35 /// Also read items from standard input, one per line (blank lines skipped), after any walker items.
36 #[arg(long)]
37 pub stdin: bool,
38
39 /// Walker item source: files under this root become items (paths). A file yields itself; a directory is descended.
40 #[arg(long)]
41 pub base: Option<PathBuf>,
42
43 /// Walker item source: limit to files whose name matches; '|'-separated alternatives, each substring->glob->regex promoted and anchored. Implies --base . when --base is absent.
44 #[arg(long)]
45 pub name: Option<String>,
46
47 /// Walker item source: restrict to these extensions (comma-separated, no dots). Combined with --name as alternatives. Implies --base . when --base is absent.
48 #[arg(long, value_delimiter = ',')]
49 pub ext: Vec<String>,
50
51 /// Include dot-entries while walking; default skips them.
52 #[arg(long)]
53 pub hidden: bool,
54
55 /// Follow symlinks while walking.
56 #[arg(long)]
57 pub follow: bool,
58
59 /// Walk gitignored / .ignore files too (the .git directory is always skipped); by default the walk skips what git would.
60 #[arg(long)]
61 pub no_ignore: bool,
62
63 /// Question this sweep answers; printed as a "== ... ==" banner.
64 #[arg(long)]
65 pub question: Option<String>,
66
67 /// Expectation over the per-item SUCCESS count: all|any|none|N|=N|+N|-N (default: all).
68 #[arg(long)]
69 pub expect: Option<String>,
70
71 /// Stop after the first per-item ERROR; remaining items are reported as skipped.
72 #[arg(long)]
73 pub fail_fast: bool,
74
75 /// Permit the suite's mutating tools (ct-edit, ct-patch) as the command.
76 #[arg(long)]
77 pub mutating: bool,
78
79 /// Print each expanded command without running anything.
80 #[arg(long)]
81 pub dry_run: bool,
82
83 /// Per item: kill the run and classify that item ERROR after SECS seconds (fractional allowed); its {CODE} becomes "timeout".
84 #[arg(long, value_name = "SECS")]
85 pub timeout: Option<f64>,
86
87 #[command(flatten)]
88 pub heartbeat: HeartbeatOpts,
89
90 /// Per-item template written to stdout. Tokens: {RESULT} {ITEM} {INDEX} {CODE} {CMD} {STDOUT} {STDERR}. Default (unless --quiet): "{RESULT} {ITEM}".
91 #[arg(long, value_name = "TEMPLATE")]
92 pub emit_each: Option<String>,
93
94 /// Summary template written to stdout after the sweep. Tokens: {RESULT} {OK} {ERRORS} {SKIPPED} {TOTAL} {QUESTION} {EXPECT} {REASON}.
95 #[arg(long, alias = "emit-stdout", value_name = "TEMPLATE")]
96 pub emit: Option<String>,
97
98 /// Summary template written to stderr (same tokens as --emit).
99 #[arg(long, value_name = "TEMPLATE")]
100 pub emit_stderr: Option<String>,
101
102 /// Also pass each child's stdout/stderr through verbatim.
103 #[arg(long)]
104 pub show_output: bool,
105
106 /// Suppress the question banner, the default per-item lines, and the default summary.
107 #[arg(long)]
108 pub quiet: bool,
109
110 /// Emit a structured JSON result instead of text (overrides the emit templates).
111 #[arg(long)]
112 pub json: bool,
113
114 /// Print agent usage docs (md or json) and exit.
115 #[arg(long, value_enum, num_args = 0..=1, default_missing_value = "md")]
116 pub explain: Option<Format>,
117
118 /// Command and arguments run per item (after `--`); {ITEM} and {INDEX} expand in every element.
119 #[arg(last = true, value_name = "CMD [ARGS...]")]
120 pub command: Vec<String>,
121}