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 = "grep", value_name = "PATTERN", display_order = 9)]
60 pub grep: Vec<String>,
61
62 #[arg(long = "head", value_name = "N", conflicts_with = "tail", display_order = 10)]
64 pub head: Option<usize>,
65
66 #[arg(short = 'N', long = "LINE-NUMBERS", display_order = 11)]
68 pub line_numbers: bool,
69
70 #[arg(long = "list-formats", display_order = 12)]
72 pub list_formats: bool,
73
74 #[arg(long = "live", conflicts_with = "follow", display_order = 13)]
80 pub live: bool,
81
82 #[arg(long = "manual", display_order = 14)]
84 pub manual: bool,
85
86 #[arg(short = 'o', long = "output", value_name = "FILE", display_order = 15)]
93 pub output: Option<String>,
94
95 #[arg(long = "prettify", display_order = 16)]
101 pub prettify: bool,
102
103 #[arg(long = "record-start", value_name = "REGEX", display_order = 17)]
110 pub record_start: Option<String>,
111
112 #[arg(long = "stdout", conflicts_with = "output", display_order = 18)]
114 pub stdout: bool,
115
116 #[arg(long = "tab-width", default_value_t = 8, display_order = 19)]
118 pub tab_width: u8,
119
120 #[arg(long = "tail", value_name = "N", conflicts_with = "head", display_order = 20)]
124 pub tail: Option<usize>,
125
126 pub files: Vec<PathBuf>,
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn parses_no_flags_no_files() {
136 let a = Args::parse_from(["tess"]);
137 assert!(!a.line_numbers);
138 assert!(!a.chop);
139 assert_eq!(a.tab_width, 8);
140 assert!(a.files.is_empty());
141 }
142
143 #[test]
144 fn parses_short_flags_and_file() {
145 let a = Args::parse_from(["tess", "-N", "-S", "foo.txt"]);
146 assert!(a.line_numbers);
147 assert!(a.chop);
148 assert_eq!(a.files, vec![PathBuf::from("foo.txt")]);
149 }
150
151 #[test]
152 fn parses_tab_width() {
153 let a = Args::parse_from(["tess", "--tab-width", "4", "x"]);
154 assert_eq!(a.tab_width, 4);
155 }
156
157 #[test]
158 fn collects_multiple_files() {
159 let a = Args::parse_from(["tess", "a", "b", "c"]);
160 assert_eq!(a.files.len(), 3);
161 }
162
163 #[test]
164 fn parses_follow_short_flag() {
165 let a = Args::parse_from(["tess", "-f", "log.txt"]);
166 assert!(a.follow);
167 assert_eq!(a.files, vec![PathBuf::from("log.txt")]);
168 }
169
170 #[test]
171 fn parses_follow_long_flag() {
172 let a = Args::parse_from(["tess", "--follow"]);
173 assert!(a.follow);
174 }
175
176 #[test]
177 fn follow_defaults_off() {
178 let a = Args::parse_from(["tess", "x"]);
179 assert!(!a.follow);
180 }
181
182 #[test]
183 fn parses_head() {
184 let a = Args::parse_from(["tess", "--head", "100", "x"]);
185 assert_eq!(a.head, Some(100));
186 assert_eq!(a.tail, None);
187 }
188
189 #[test]
190 fn parses_tail() {
191 let a = Args::parse_from(["tess", "--tail", "50", "x"]);
192 assert_eq!(a.tail, Some(50));
193 assert_eq!(a.head, None);
194 }
195
196 #[test]
197 fn head_and_tail_are_mutually_exclusive() {
198 let r = Args::try_parse_from(["tess", "--head", "10", "--tail", "20", "x"]);
199 assert!(r.is_err(), "clap should reject combining --head and --tail");
200 }
201
202 #[test]
203 fn head_tail_default_to_none() {
204 let a = Args::parse_from(["tess", "x"]);
205 assert!(a.head.is_none());
206 assert!(a.tail.is_none());
207 }
208
209 #[test]
210 fn parses_grep_repeatable_and_no_format_required() {
211 let a = Args::parse_from([
212 "tess",
213 "--grep", "error",
214 "--grep", r"^\[",
215 "log",
216 ]);
217 assert_eq!(a.grep.len(), 2);
218 assert_eq!(a.grep[0], "error");
219 assert_eq!(a.grep[1], r"^\[");
220 assert_eq!(a.format, None);
221 }
222
223 #[test]
224 fn parses_format_and_filter() {
225 let a = Args::parse_from([
226 "tess", "--format", "apache-combined",
227 "--filter", "status=500",
228 "--filter", "ip~^10\\.",
229 "log",
230 ]);
231 assert_eq!(a.format.as_deref(), Some("apache-combined"));
232 assert_eq!(a.filter.len(), 2);
233 assert_eq!(a.filter[0], "status=500");
234 }
235
236 #[test]
237 fn parses_dim() {
238 let a = Args::parse_from(["tess", "--format", "x", "--filter", "y=z", "--dim", "f"]);
239 assert!(a.dim);
240 }
241
242 #[test]
243 fn parses_list_formats() {
244 let a = Args::parse_from(["tess", "--list-formats"]);
245 assert!(a.list_formats);
246 }
247
248 #[test]
249 fn parses_manual() {
250 let a = Args::parse_from(["tess", "--manual"]);
251 assert!(a.manual);
252 }
253
254 #[test]
255 fn parses_examples() {
256 let a = Args::parse_from(["tess", "--examples"]);
257 assert!(a.examples);
258 }
259
260 #[test]
261 fn parses_live() {
262 let a = Args::parse_from(["tess", "--live", "f"]);
263 assert!(a.live);
264 assert!(!a.follow);
265 }
266
267 #[test]
268 fn live_and_follow_are_mutually_exclusive() {
269 let r = Args::try_parse_from(["tess", "--live", "--follow", "f"]);
270 assert!(r.is_err(), "clap should reject combining --live and --follow");
271 }
272
273 #[test]
274 fn parses_prettify() {
275 let a = Args::parse_from(["tess", "--prettify", "f.json"]);
276 assert!(a.prettify);
277 assert_eq!(a.content_type, None);
278 }
279
280 #[test]
281 fn parses_content_type() {
282 let a = Args::parse_from(["tess", "--content-type", "json", "f"]);
283 assert_eq!(a.content_type.as_deref(), Some("json"));
284 }
285
286 #[test]
287 fn parses_output_long_and_short() {
288 let a = Args::parse_from(["tess", "-o", "/tmp/out.txt", "f"]);
289 assert_eq!(a.output.as_deref(), Some("/tmp/out.txt"));
290 let b = Args::parse_from(["tess", "--output", "/tmp/out.txt", "f"]);
291 assert_eq!(b.output.as_deref(), Some("/tmp/out.txt"));
292 }
293
294 #[test]
295 fn parses_stdout_flag() {
296 let a = Args::parse_from(["tess", "--stdout", "f"]);
297 assert!(a.stdout);
298 assert_eq!(a.output, None);
299 }
300
301 #[test]
302 fn output_and_stdout_are_mutually_exclusive() {
303 let r = Args::try_parse_from(["tess", "-o", "x", "--stdout", "f"]);
304 assert!(r.is_err(), "clap should reject combining --output and --stdout");
305 }
306
307 #[test]
308 fn help_lists_flags_in_alphabetical_order() {
309 use clap::CommandFactory;
310 let mut cmd = Args::command();
311 let help = cmd.render_help().to_string();
312
313 let expected = [
314 "--chop-long-lines",
315 "--content-type",
316 "--dim",
317 "--display",
318 "--examples",
319 "--filter",
320 "--follow",
321 "--format",
322 "--grep",
323 "--head",
324 "--LINE-NUMBERS",
325 "--list-formats",
326 "--live",
327 "--manual",
328 "--output",
329 "--prettify",
330 "--record-start",
331 "--stdout",
332 "--tab-width",
333 "--tail",
334 ];
335 let listed: Vec<&str> = help
336 .lines()
337 .map(str::trim_start)
338 .filter(|l| l.starts_with('-'))
339 .filter_map(|l| {
340 l.split(|c: char| c.is_whitespace() || c == ',')
341 .find(|tok| expected.contains(tok))
342 })
343 .collect();
344 assert_eq!(listed, expected, "help long-flag order should be alphabetical");
345 }
346}