1use std::path::PathBuf;
2use clap::Parser;
3
4#[derive(Parser, Debug, Clone)]
5#[command(name = "tess", version, about = "A less-style terminal pager.")]
6pub struct Args {
7 #[arg(short = 'S', long = "chop-long-lines", display_order = 1)]
9 pub chop: bool,
10
11 #[arg(long = "content-type", value_name = "TYPE", display_order = 2)]
16 pub content_type: Option<String>,
17
18 #[arg(long = "dim", display_order = 3)]
21 pub dim: bool,
22
23 #[arg(long = "display", value_name = "TEMPLATE", display_order = 4)]
29 pub display: Option<String>,
30
31 #[arg(long = "examples", display_order = 5)]
33 pub examples: bool,
34
35 #[arg(long = "filter", value_name = "FIELD<op>VALUE", display_order = 6)]
42 pub filter: Vec<String>,
43
44 #[arg(short = 'f', long = "follow", display_order = 7)]
47 pub follow: bool,
48
49 #[arg(long = "format", value_name = "NAME", display_order = 8)]
52 pub format: Option<String>,
53
54 #[arg(long = "head", value_name = "N", conflicts_with = "tail", display_order = 9)]
56 pub head: Option<usize>,
57
58 #[arg(short = 'N', long = "LINE-NUMBERS", display_order = 10)]
60 pub line_numbers: bool,
61
62 #[arg(long = "list-formats", display_order = 11)]
64 pub list_formats: bool,
65
66 #[arg(long = "live", conflicts_with = "follow", display_order = 12)]
72 pub live: bool,
73
74 #[arg(long = "manual", display_order = 13)]
76 pub manual: bool,
77
78 #[arg(short = 'o', long = "output", value_name = "FILE", display_order = 14)]
85 pub output: Option<String>,
86
87 #[arg(long = "prettify", display_order = 15)]
93 pub prettify: bool,
94
95 #[arg(long = "stdout", conflicts_with = "output", display_order = 16)]
97 pub stdout: bool,
98
99 #[arg(long = "tab-width", default_value_t = 8, display_order = 17)]
101 pub tab_width: u8,
102
103 #[arg(long = "tail", value_name = "N", conflicts_with = "head", display_order = 18)]
107 pub tail: Option<usize>,
108
109 pub files: Vec<PathBuf>,
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn parses_no_flags_no_files() {
119 let a = Args::parse_from(["tess"]);
120 assert!(!a.line_numbers);
121 assert!(!a.chop);
122 assert_eq!(a.tab_width, 8);
123 assert!(a.files.is_empty());
124 }
125
126 #[test]
127 fn parses_short_flags_and_file() {
128 let a = Args::parse_from(["tess", "-N", "-S", "foo.txt"]);
129 assert!(a.line_numbers);
130 assert!(a.chop);
131 assert_eq!(a.files, vec![PathBuf::from("foo.txt")]);
132 }
133
134 #[test]
135 fn parses_tab_width() {
136 let a = Args::parse_from(["tess", "--tab-width", "4", "x"]);
137 assert_eq!(a.tab_width, 4);
138 }
139
140 #[test]
141 fn collects_multiple_files() {
142 let a = Args::parse_from(["tess", "a", "b", "c"]);
143 assert_eq!(a.files.len(), 3);
144 }
145
146 #[test]
147 fn parses_follow_short_flag() {
148 let a = Args::parse_from(["tess", "-f", "log.txt"]);
149 assert!(a.follow);
150 assert_eq!(a.files, vec![PathBuf::from("log.txt")]);
151 }
152
153 #[test]
154 fn parses_follow_long_flag() {
155 let a = Args::parse_from(["tess", "--follow"]);
156 assert!(a.follow);
157 }
158
159 #[test]
160 fn follow_defaults_off() {
161 let a = Args::parse_from(["tess", "x"]);
162 assert!(!a.follow);
163 }
164
165 #[test]
166 fn parses_head() {
167 let a = Args::parse_from(["tess", "--head", "100", "x"]);
168 assert_eq!(a.head, Some(100));
169 assert_eq!(a.tail, None);
170 }
171
172 #[test]
173 fn parses_tail() {
174 let a = Args::parse_from(["tess", "--tail", "50", "x"]);
175 assert_eq!(a.tail, Some(50));
176 assert_eq!(a.head, None);
177 }
178
179 #[test]
180 fn head_and_tail_are_mutually_exclusive() {
181 let r = Args::try_parse_from(["tess", "--head", "10", "--tail", "20", "x"]);
182 assert!(r.is_err(), "clap should reject combining --head and --tail");
183 }
184
185 #[test]
186 fn head_tail_default_to_none() {
187 let a = Args::parse_from(["tess", "x"]);
188 assert!(a.head.is_none());
189 assert!(a.tail.is_none());
190 }
191
192 #[test]
193 fn parses_format_and_filter() {
194 let a = Args::parse_from([
195 "tess", "--format", "apache-combined",
196 "--filter", "status=500",
197 "--filter", "ip~^10\\.",
198 "log",
199 ]);
200 assert_eq!(a.format.as_deref(), Some("apache-combined"));
201 assert_eq!(a.filter.len(), 2);
202 assert_eq!(a.filter[0], "status=500");
203 }
204
205 #[test]
206 fn parses_dim() {
207 let a = Args::parse_from(["tess", "--format", "x", "--filter", "y=z", "--dim", "f"]);
208 assert!(a.dim);
209 }
210
211 #[test]
212 fn parses_list_formats() {
213 let a = Args::parse_from(["tess", "--list-formats"]);
214 assert!(a.list_formats);
215 }
216
217 #[test]
218 fn parses_manual() {
219 let a = Args::parse_from(["tess", "--manual"]);
220 assert!(a.manual);
221 }
222
223 #[test]
224 fn parses_examples() {
225 let a = Args::parse_from(["tess", "--examples"]);
226 assert!(a.examples);
227 }
228
229 #[test]
230 fn parses_live() {
231 let a = Args::parse_from(["tess", "--live", "f"]);
232 assert!(a.live);
233 assert!(!a.follow);
234 }
235
236 #[test]
237 fn live_and_follow_are_mutually_exclusive() {
238 let r = Args::try_parse_from(["tess", "--live", "--follow", "f"]);
239 assert!(r.is_err(), "clap should reject combining --live and --follow");
240 }
241
242 #[test]
243 fn parses_prettify() {
244 let a = Args::parse_from(["tess", "--prettify", "f.json"]);
245 assert!(a.prettify);
246 assert_eq!(a.content_type, None);
247 }
248
249 #[test]
250 fn parses_content_type() {
251 let a = Args::parse_from(["tess", "--content-type", "json", "f"]);
252 assert_eq!(a.content_type.as_deref(), Some("json"));
253 }
254
255 #[test]
256 fn parses_output_long_and_short() {
257 let a = Args::parse_from(["tess", "-o", "/tmp/out.txt", "f"]);
258 assert_eq!(a.output.as_deref(), Some("/tmp/out.txt"));
259 let b = Args::parse_from(["tess", "--output", "/tmp/out.txt", "f"]);
260 assert_eq!(b.output.as_deref(), Some("/tmp/out.txt"));
261 }
262
263 #[test]
264 fn parses_stdout_flag() {
265 let a = Args::parse_from(["tess", "--stdout", "f"]);
266 assert!(a.stdout);
267 assert_eq!(a.output, None);
268 }
269
270 #[test]
271 fn output_and_stdout_are_mutually_exclusive() {
272 let r = Args::try_parse_from(["tess", "-o", "x", "--stdout", "f"]);
273 assert!(r.is_err(), "clap should reject combining --output and --stdout");
274 }
275
276 #[test]
277 fn help_lists_flags_in_alphabetical_order() {
278 use clap::CommandFactory;
279 let mut cmd = Args::command();
280 let help = cmd.render_help().to_string();
281
282 let expected = [
283 "--chop-long-lines",
284 "--content-type",
285 "--dim",
286 "--display",
287 "--examples",
288 "--filter",
289 "--follow",
290 "--format",
291 "--head",
292 "--LINE-NUMBERS",
293 "--list-formats",
294 "--live",
295 "--manual",
296 "--output",
297 "--prettify",
298 "--stdout",
299 "--tab-width",
300 "--tail",
301 ];
302 let listed: Vec<&str> = help
303 .lines()
304 .map(str::trim_start)
305 .filter(|l| l.starts_with('-'))
306 .filter_map(|l| {
307 l.split(|c: char| c.is_whitespace() || c == ',')
308 .find(|tok| expected.contains(tok))
309 })
310 .collect();
311 assert_eq!(listed, expected, "help long-flag order should be alphabetical");
312 }
313}