1use std::path::PathBuf;
6
7use clap::{Arg, ArgAction, Command};
8
9#[derive(Debug, Clone)]
11pub struct CliArgs {
12 pub inputs: Vec<PathBuf>,
14
15 pub indent: Option<usize>,
17
18 pub line_length: Option<usize>,
20
21 pub whitespace: Option<u8>,
23
24 pub whitespace_comma: Option<bool>,
26
27 pub whitespace_assignment: Option<bool>,
29
30 pub whitespace_decl: Option<bool>,
32
33 pub whitespace_relational: Option<bool>,
35
36 pub whitespace_logical: Option<bool>,
38
39 pub whitespace_plusminus: Option<bool>,
41
42 pub whitespace_multdiv: Option<bool>,
44
45 pub whitespace_print: Option<bool>,
47
48 pub whitespace_type: Option<bool>,
50
51 pub whitespace_intrinsics: Option<bool>,
53
54 pub whitespace_concat: Option<bool>,
56
57 pub no_indent: bool,
59
60 pub no_whitespace: bool,
62
63 pub strict_indent: bool,
65
66 pub no_indent_fypp: bool,
68
69 pub no_indent_mod: bool,
71
72 pub normalize_comment_spacing: bool,
74
75 pub format_decl: bool,
77
78 pub enable_replacements: bool,
80
81 pub c_relations: bool,
83
84 pub comment_spacing: Option<usize>,
86
87 pub stdout: bool,
89
90 pub diff: bool,
92
93 pub config: Option<PathBuf>,
95
96 pub recursive: bool,
98
99 pub silent: bool,
101
102 pub case: Option<[i32; 4]>,
105
106 pub jobs: Option<usize>,
108
109 pub exclude: Vec<String>,
111
112 pub fortran_extensions: Vec<String>,
114
115 pub exclude_max_lines: Option<usize>,
117
118 pub debug: bool,
120}
121
122#[must_use]
124pub fn build_cli() -> Command {
125 Command::new("fprettier")
126 .version(env!("CARGO_PKG_VERSION"))
127 .author("Fred Jones")
128 .about("Auto-formatter for modern Fortran code (Fortran 90+)")
129 .arg(
130 Arg::new("inputs")
131 .help("Files or directories to format")
132 .value_name("FILE")
133 .num_args(1..)
134 .required(false)
135 .value_parser(clap::value_parser!(PathBuf)),
136 )
137 .arg(
138 Arg::new("indent")
139 .short('i')
140 .long("indent")
141 .help("Number of spaces per indent level [default: 3]")
142 .value_name("NUM")
143 .value_parser(clap::value_parser!(usize)),
144 )
145 .arg(
146 Arg::new("line-length")
147 .short('l')
148 .long("line-length")
149 .help("Maximum line length [default: 132]")
150 .value_name("NUM")
151 .value_parser(clap::value_parser!(usize)),
152 )
153 .arg(
154 Arg::new("whitespace")
155 .short('w')
156 .long("whitespace")
157 .help("Whitespace level: 0=minimal, 1=+operators, 2=+plusminus, 3=+multdiv, 4=all [default: 2]")
158 .value_name("NUM")
159 .value_parser(clap::value_parser!(u8)),
160 )
161 .arg(
163 Arg::new("whitespace-comma")
164 .long("whitespace-comma")
165 .help("Enable/disable spacing after commas and semicolons")
166 .value_name("BOOL")
167 .num_args(0..=1)
168 .require_equals(true)
169 .default_missing_value("true")
170 .value_parser(clap::value_parser!(bool)),
171 )
172 .arg(
173 Arg::new("whitespace-assignment")
174 .long("whitespace-assignment")
175 .help("Enable/disable spacing around assignment operators (=, =>)")
176 .value_name("BOOL")
177 .num_args(0..=1)
178 .require_equals(true)
179 .default_missing_value("true")
180 .value_parser(clap::value_parser!(bool)),
181 )
182 .arg(
183 Arg::new("whitespace-decl")
184 .long("whitespace-decl")
185 .help("Enable/disable spacing around declaration operator (::)")
186 .value_name("BOOL")
187 .num_args(0..=1)
188 .require_equals(true)
189 .default_missing_value("true")
190 .value_parser(clap::value_parser!(bool)),
191 )
192 .arg(
193 Arg::new("whitespace-relational")
194 .long("whitespace-relational")
195 .help("Enable/disable spacing around relational operators (<, >, ==, /=, .eq., etc.)")
196 .value_name("BOOL")
197 .num_args(0..=1)
198 .require_equals(true)
199 .default_missing_value("true")
200 .value_parser(clap::value_parser!(bool)),
201 )
202 .arg(
203 Arg::new("whitespace-logical")
204 .long("whitespace-logical")
205 .help("Enable/disable spacing around logical operators (.and., .or., etc.)")
206 .value_name("BOOL")
207 .num_args(0..=1)
208 .require_equals(true)
209 .default_missing_value("true")
210 .value_parser(clap::value_parser!(bool)),
211 )
212 .arg(
213 Arg::new("whitespace-plusminus")
214 .long("whitespace-plusminus")
215 .help("Enable/disable spacing around plus/minus operators")
216 .value_name("BOOL")
217 .num_args(0..=1)
218 .require_equals(true)
219 .default_missing_value("true")
220 .value_parser(clap::value_parser!(bool)),
221 )
222 .arg(
223 Arg::new("whitespace-multdiv")
224 .long("whitespace-multdiv")
225 .help("Enable/disable spacing around multiply/divide operators")
226 .value_name("BOOL")
227 .num_args(0..=1)
228 .require_equals(true)
229 .default_missing_value("true")
230 .value_parser(clap::value_parser!(bool)),
231 )
232 .arg(
233 Arg::new("whitespace-print")
234 .long("whitespace-print")
235 .help("Enable/disable spacing in print/read statements")
236 .value_name("BOOL")
237 .num_args(0..=1)
238 .require_equals(true)
239 .default_missing_value("true")
240 .value_parser(clap::value_parser!(bool)),
241 )
242 .arg(
243 Arg::new("whitespace-type")
244 .long("whitespace-type")
245 .help("Enable/disable spacing around type selector (%)")
246 .value_name("BOOL")
247 .num_args(0..=1)
248 .require_equals(true)
249 .default_missing_value("true")
250 .value_parser(clap::value_parser!(bool)),
251 )
252 .arg(
253 Arg::new("whitespace-intrinsics")
254 .long("whitespace-intrinsics")
255 .help("Enable/disable spacing before intrinsic function parentheses")
256 .value_name("BOOL")
257 .num_args(0..=1)
258 .require_equals(true)
259 .default_missing_value("true")
260 .value_parser(clap::value_parser!(bool)),
261 )
262 .arg(
263 Arg::new("whitespace-concat")
264 .long("whitespace-concat")
265 .help("Enable/disable spacing around string concatenation operator (//)")
266 .value_name("BOOL")
267 .num_args(0..=1)
268 .require_equals(true)
269 .default_missing_value("true")
270 .value_parser(clap::value_parser!(bool)),
271 )
272 .arg(
273 Arg::new("no-indent")
274 .long("no-indent")
275 .help("Disable indentation")
276 .action(ArgAction::SetTrue),
277 )
278 .arg(
279 Arg::new("no-whitespace")
280 .long("no-whitespace")
281 .help("Disable whitespace formatting")
282 .action(ArgAction::SetTrue),
283 )
284 .arg(
285 Arg::new("strict-indent")
286 .long("strict-indent")
287 .help("Strict indentation checking")
288 .action(ArgAction::SetTrue),
289 )
290 .arg(
291 Arg::new("no-indent-fypp")
292 .long("no-indent-fypp")
293 .help("Don't indent fypp preprocessor directives")
294 .action(ArgAction::SetTrue),
295 )
296 .arg(
297 Arg::new("no-indent-mod")
298 .long("no-indent-mod")
299 .help("Don't indent module/program/submodule blocks")
300 .action(ArgAction::SetTrue),
301 )
302 .arg(
303 Arg::new("normalize-comment-spacing")
304 .long("normalize-comment-spacing")
305 .help("Normalize spacing before inline comments")
306 .action(ArgAction::SetTrue),
307 )
308 .arg(
309 Arg::new("format-decl")
310 .long("format-decl")
311 .help("Format declaration statements")
312 .action(ArgAction::SetTrue),
313 )
314 .arg(
315 Arg::new("enable-replacements")
316 .long("enable-replacements")
317 .help("Replace relational operators between Fortran (.lt., .eq., etc.) and C-style (<, ==, etc.)")
318 .action(ArgAction::SetTrue),
319 )
320 .arg(
321 Arg::new("c-relations")
322 .long("c-relations")
323 .help("Use C-style relational operators (<, <=, >, >=, ==, /=) when --enable-replacements is set")
324 .action(ArgAction::SetTrue),
325 )
326 .arg(
327 Arg::new("comment-spacing")
328 .long("comment-spacing")
329 .help("Number of spaces before inline comments [default: 1]")
330 .value_name("NUM")
331 .value_parser(clap::value_parser!(usize)),
332 )
333 .arg(
334 Arg::new("stdout")
335 .short('s')
336 .long("stdout")
337 .help("Output to stdout instead of modifying files in-place")
338 .action(ArgAction::SetTrue),
339 )
340 .arg(
341 Arg::new("diff")
342 .short('d')
343 .long("diff")
344 .help("Show diff without modifying files")
345 .action(ArgAction::SetTrue),
346 )
347 .arg(
348 Arg::new("config")
349 .short('c')
350 .long("config")
351 .help("Path to configuration file (overrides auto-discovery)")
352 .value_name("FILE")
353 .value_parser(clap::value_parser!(PathBuf)),
354 )
355 .arg(
356 Arg::new("recursive")
357 .short('r')
358 .long("recursive")
359 .help("Recursively format directories")
360 .action(ArgAction::SetTrue),
361 )
362 .arg(
363 Arg::new("exclude")
364 .short('e')
365 .long("exclude")
366 .help("Exclude files/directories matching pattern (glob syntax, can be repeated)")
367 .value_name("PATTERN")
368 .action(ArgAction::Append),
369 )
370 .arg(
371 Arg::new("fortran")
372 .short('f')
373 .long("fortran")
374 .help("Additional Fortran file extension (can be repeated, e.g., -f f03 -f F03)")
375 .value_name("EXT")
376 .action(ArgAction::Append),
377 )
378 .arg(
379 Arg::new("exclude-max-lines")
380 .short('m')
381 .long("exclude-max-lines")
382 .help("Exclude files with more than this many lines")
383 .value_name("NUM")
384 .value_parser(clap::value_parser!(usize)),
385 )
386 .arg(
387 Arg::new("debug")
388 .short('D')
389 .long("debug")
390 .help("Enable debug output (shows config, scope changes, warnings)")
391 .action(ArgAction::SetTrue),
392 )
393 .arg(
394 Arg::new("silent")
395 .short('S')
396 .long("silent")
397 .help("Silent mode (no output, for editor integration)")
398 .action(ArgAction::SetTrue),
399 )
400 .arg(
401 Arg::new("case")
402 .long("case")
403 .help("Enable case formatting: 4 values for keywords, procedures, operators, constants (0=none, 1=lower, 2=upper)")
404 .value_name("NUM")
405 .num_args(4)
406 .value_parser(clap::value_parser!(i32)),
407 )
408 .arg(
409 Arg::new("jobs")
410 .short('j')
411 .long("jobs")
412 .help("Number of parallel jobs (0=auto, 1=sequential)")
413 .value_name("NUM")
414 .value_parser(clap::value_parser!(usize)),
415 )
416}
417
418#[must_use]
420pub fn parse_args() -> CliArgs {
421 args_from_matches(&build_cli().get_matches())
422}
423
424#[must_use]
426pub fn parse_args_from<I, T>(args: I) -> CliArgs
427where
428 I: IntoIterator<Item = T>,
429 T: Into<std::ffi::OsString> + Clone,
430{
431 args_from_matches(&build_cli().get_matches_from(args))
432}
433
434fn args_from_matches(matches: &clap::ArgMatches) -> CliArgs {
436 let case = matches.get_many::<i32>("case").map(|vals| {
437 let v: Vec<i32> = vals.copied().collect();
438 [v[0], v[1], v[2], v[3]]
439 });
440
441 CliArgs {
442 inputs: matches
443 .get_many::<PathBuf>("inputs")
444 .map(|vals| vals.cloned().collect())
445 .unwrap_or_default(),
446 indent: matches.get_one::<usize>("indent").copied(),
447 line_length: matches.get_one::<usize>("line-length").copied(),
448 whitespace: matches.get_one::<u8>("whitespace").copied(),
449 whitespace_comma: matches.get_one::<bool>("whitespace-comma").copied(),
450 whitespace_assignment: matches.get_one::<bool>("whitespace-assignment").copied(),
451 whitespace_decl: matches.get_one::<bool>("whitespace-decl").copied(),
452 whitespace_relational: matches.get_one::<bool>("whitespace-relational").copied(),
453 whitespace_logical: matches.get_one::<bool>("whitespace-logical").copied(),
454 whitespace_plusminus: matches.get_one::<bool>("whitespace-plusminus").copied(),
455 whitespace_multdiv: matches.get_one::<bool>("whitespace-multdiv").copied(),
456 whitespace_print: matches.get_one::<bool>("whitespace-print").copied(),
457 whitespace_type: matches.get_one::<bool>("whitespace-type").copied(),
458 whitespace_intrinsics: matches.get_one::<bool>("whitespace-intrinsics").copied(),
459 whitespace_concat: matches.get_one::<bool>("whitespace-concat").copied(),
460 no_indent: matches.get_flag("no-indent"),
461 no_whitespace: matches.get_flag("no-whitespace"),
462 strict_indent: matches.get_flag("strict-indent"),
463 no_indent_fypp: matches.get_flag("no-indent-fypp"),
464 no_indent_mod: matches.get_flag("no-indent-mod"),
465 normalize_comment_spacing: matches.get_flag("normalize-comment-spacing"),
466 format_decl: matches.get_flag("format-decl"),
467 enable_replacements: matches.get_flag("enable-replacements"),
468 c_relations: matches.get_flag("c-relations"),
469 comment_spacing: matches.get_one::<usize>("comment-spacing").copied(),
470 stdout: matches.get_flag("stdout"),
471 diff: matches.get_flag("diff"),
472 config: matches.get_one::<PathBuf>("config").cloned(),
473 recursive: matches.get_flag("recursive"),
474 exclude: matches
475 .get_many::<String>("exclude")
476 .map(|vals| vals.cloned().collect())
477 .unwrap_or_default(),
478 fortran_extensions: matches
479 .get_many::<String>("fortran")
480 .map(|vals| vals.cloned().collect())
481 .unwrap_or_default(),
482 exclude_max_lines: matches.get_one::<usize>("exclude-max-lines").copied(),
483 debug: matches.get_flag("debug"),
484 silent: matches.get_flag("silent"),
485 case,
486 jobs: matches.get_one::<usize>("jobs").copied(),
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn test_cli_builds() {
496 let cmd = build_cli();
497 assert_eq!(cmd.get_name(), "fprettier");
499 }
500
501 #[test]
502 fn test_cli_defaults() {
503 let cmd = build_cli();
504 let matches = cmd.try_get_matches_from(vec!["fprettier"]).unwrap();
505
506 assert!(matches.get_many::<PathBuf>("inputs").is_none());
507 assert!(!matches.get_flag("no-indent"));
508 assert!(!matches.get_flag("stdout"));
509 }
510
511 #[test]
512 fn test_whitespace_comma_flag() {
513 let args = parse_args_from(vec!["fprettier", "--whitespace-comma", "file.f90"]);
515 assert_eq!(args.whitespace_comma, Some(true));
516 }
517
518 #[test]
519 fn test_whitespace_comma_explicit_true() {
520 let args = parse_args_from(vec!["fprettier", "--whitespace-comma=true", "file.f90"]);
522 assert_eq!(args.whitespace_comma, Some(true));
523 }
524
525 #[test]
526 fn test_whitespace_comma_explicit_false() {
527 let args = parse_args_from(vec!["fprettier", "--whitespace-comma=false", "file.f90"]);
529 assert_eq!(args.whitespace_comma, Some(false));
530 }
531
532 #[test]
533 fn test_whitespace_options_not_set() {
534 let args = parse_args_from(vec!["fprettier", "file.f90"]);
536 assert_eq!(args.whitespace_comma, None);
537 assert_eq!(args.whitespace_assignment, None);
538 assert_eq!(args.whitespace_decl, None);
539 assert_eq!(args.whitespace_relational, None);
540 assert_eq!(args.whitespace_logical, None);
541 assert_eq!(args.whitespace_plusminus, None);
542 assert_eq!(args.whitespace_multdiv, None);
543 assert_eq!(args.whitespace_print, None);
544 assert_eq!(args.whitespace_type, None);
545 assert_eq!(args.whitespace_intrinsics, None);
546 assert_eq!(args.whitespace_concat, None);
547 }
548
549 #[test]
550 fn test_multiple_whitespace_options() {
551 let args = parse_args_from(vec![
553 "fprettier",
554 "--whitespace-comma",
555 "--whitespace-concat=false",
556 "--whitespace-type=true",
557 "file.f90",
558 ]);
559 assert_eq!(args.whitespace_comma, Some(true));
560 assert_eq!(args.whitespace_concat, Some(false));
561 assert_eq!(args.whitespace_type, Some(true));
562 assert_eq!(args.whitespace_assignment, None);
564 }
565
566 #[test]
567 fn test_all_whitespace_options() {
568 let args = parse_args_from(vec![
570 "fprettier",
571 "--whitespace-comma=true",
572 "--whitespace-assignment=false",
573 "--whitespace-decl=true",
574 "--whitespace-relational=false",
575 "--whitespace-logical=true",
576 "--whitespace-plusminus=false",
577 "--whitespace-multdiv=true",
578 "--whitespace-print=false",
579 "--whitespace-type=true",
580 "--whitespace-intrinsics=false",
581 "--whitespace-concat=true",
582 "file.f90",
583 ]);
584 assert_eq!(args.whitespace_comma, Some(true));
585 assert_eq!(args.whitespace_assignment, Some(false));
586 assert_eq!(args.whitespace_decl, Some(true));
587 assert_eq!(args.whitespace_relational, Some(false));
588 assert_eq!(args.whitespace_logical, Some(true));
589 assert_eq!(args.whitespace_plusminus, Some(false));
590 assert_eq!(args.whitespace_multdiv, Some(true));
591 assert_eq!(args.whitespace_print, Some(false));
592 assert_eq!(args.whitespace_type, Some(true));
593 assert_eq!(args.whitespace_intrinsics, Some(false));
594 assert_eq!(args.whitespace_concat, Some(true));
595 }
596
597 #[test]
598 fn test_comment_spacing() {
599 let args = parse_args_from(vec!["fprettier", "--comment-spacing", "3", "file.f90"]);
600 assert_eq!(args.comment_spacing, Some(3));
601 }
602
603 #[test]
604 fn test_comment_spacing_not_set() {
605 let args = parse_args_from(vec!["fprettier", "file.f90"]);
606 assert_eq!(args.comment_spacing, None);
607 }
608
609 #[test]
610 fn test_exclude_single() {
611 let args = parse_args_from(vec!["fprettier", "-r", "-e", "*.mod", "src/"]);
612 assert_eq!(args.exclude, vec!["*.mod"]);
613 }
614
615 #[test]
616 fn test_exclude_multiple() {
617 let args = parse_args_from(vec![
618 "fprettier",
619 "-r",
620 "-e",
621 "*.mod",
622 "--exclude",
623 "build*",
624 "-e",
625 "test_*",
626 "src/",
627 ]);
628 assert_eq!(args.exclude, vec!["*.mod", "build*", "test_*"]);
629 }
630
631 #[test]
632 fn test_exclude_empty() {
633 let args = parse_args_from(vec!["fprettier", "file.f90"]);
634 assert!(args.exclude.is_empty());
635 }
636
637 #[test]
638 fn test_fortran_single_extension() {
639 let args = parse_args_from(vec!["fprettier", "-r", "-f", "f2003", "src/"]);
640 assert_eq!(args.fortran_extensions, vec!["f2003"]);
641 }
642
643 #[test]
644 fn test_fortran_multiple_extensions() {
645 let args = parse_args_from(vec![
646 "fprettier",
647 "-r",
648 "-f",
649 "f2003",
650 "--fortran",
651 "F2003",
652 "-f",
653 "f2008",
654 "src/",
655 ]);
656 assert_eq!(args.fortran_extensions, vec!["f2003", "F2003", "f2008"]);
657 }
658
659 #[test]
660 fn test_fortran_extensions_empty() {
661 let args = parse_args_from(vec!["fprettier", "file.f90"]);
662 assert!(args.fortran_extensions.is_empty());
663 }
664
665 #[test]
666 fn test_exclude_max_lines() {
667 let args = parse_args_from(vec!["fprettier", "--exclude-max-lines", "1000", "file.f90"]);
668 assert_eq!(args.exclude_max_lines, Some(1000));
669 }
670
671 #[test]
672 fn test_exclude_max_lines_short_flag() {
673 let args = parse_args_from(vec!["fprettier", "-m", "500", "file.f90"]);
674 assert_eq!(args.exclude_max_lines, Some(500));
675 }
676
677 #[test]
678 fn test_exclude_max_lines_not_set() {
679 let args = parse_args_from(vec!["fprettier", "file.f90"]);
680 assert_eq!(args.exclude_max_lines, None);
681 }
682
683 #[test]
684 fn test_debug_flag() {
685 let args = parse_args_from(vec!["fprettier", "-D", "file.f90"]);
686 assert!(args.debug);
687 }
688
689 #[test]
690 fn test_debug_long_flag() {
691 let args = parse_args_from(vec!["fprettier", "--debug", "file.f90"]);
692 assert!(args.debug);
693 }
694
695 #[test]
696 fn test_debug_not_set() {
697 let args = parse_args_from(vec!["fprettier", "file.f90"]);
698 assert!(!args.debug);
699 }
700}