1use std::ffi::OsString;
2use std::iter::Peekable;
3use std::path::PathBuf;
4
5use regex::Regex;
6
7#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
8pub enum Format {
9 #[default]
10 Normal,
11 Unified,
12 Context,
13 Ed,
14 SideBySide,
15}
16
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct Params {
19 pub executable: OsString,
20 pub from: OsString,
21 pub to: OsString,
22 pub format: Format,
23 pub context_count: usize,
24 pub report_identical_files: bool,
25 pub brief: bool,
26 pub expand_tabs: bool,
27 pub tabsize: usize,
28 pub width: usize,
29}
30
31impl Default for Params {
32 fn default() -> Self {
33 Self {
34 executable: OsString::default(),
35 from: OsString::default(),
36 to: OsString::default(),
37 format: Format::default(),
38 context_count: 3,
39 report_identical_files: false,
40 brief: false,
41 expand_tabs: false,
42 tabsize: 8,
43 width: 130,
44 }
45 }
46}
47
48pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, String> {
49 let Some(executable) = opts.next() else {
52 return Err("Usage: <exe> <from> <to>".to_string());
53 };
54 let mut params = Params {
55 executable,
56 ..Default::default()
57 };
58 let mut from = None;
59 let mut to = None;
60 let mut format = None;
61 let mut context = None;
62 let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
63 let width_re = Regex::new(r"--width=(?P<long>\d+)$").unwrap();
64 while let Some(param) = opts.next() {
65 let next_param = opts.peek();
66 if param == "--" {
67 break;
68 }
69 if param == "-" {
70 if from.is_none() {
71 from = Some(param);
72 } else if to.is_none() {
73 to = Some(param);
74 } else {
75 return Err(format!(
76 "Usage: {} <from> <to>",
77 params.executable.to_string_lossy()
78 ));
79 }
80 continue;
81 }
82 if param == "-s" || param == "--report-identical-files" {
83 params.report_identical_files = true;
84 continue;
85 }
86 if param == "-q" || param == "--brief" {
87 params.brief = true;
88 continue;
89 }
90 if param == "-t" || param == "--expand-tabs" {
91 params.expand_tabs = true;
92 continue;
93 }
94 if param == "--normal" {
95 if format.is_some() && format != Some(Format::Normal) {
96 return Err("Conflicting output style options".to_string());
97 }
98 format = Some(Format::Normal);
99 continue;
100 }
101 if param == "-e" || param == "--ed" {
102 if format.is_some() && format != Some(Format::Ed) {
103 return Err("Conflicting output style options".to_string());
104 }
105 format = Some(Format::Ed);
106 continue;
107 }
108 if param == "-y" || param == "--side-by-side" {
109 if format.is_some() && format != Some(Format::SideBySide) {
110 return Err("Conflicting output style option".to_string());
111 }
112 format = Some(Format::SideBySide);
113 continue;
114 }
115 if width_re.is_match(param.to_string_lossy().as_ref()) {
116 let param = param.into_string().unwrap();
117 let width_str: &str = width_re
118 .captures(param.as_str())
119 .unwrap()
120 .name("long")
121 .unwrap()
122 .as_str();
123
124 params.width = match width_str.parse::<usize>() {
125 Ok(num) => {
126 if num == 0 {
127 return Err("invalid width «0»".to_string());
128 }
129
130 num
131 }
132 Err(_) => return Err(format!("invalid width «{width_str}»")),
133 };
134 continue;
135 }
136 if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
137 let param = param.into_string().unwrap();
140 let tabsize_str = tabsize_re
141 .captures(param.as_str())
142 .unwrap()
143 .name("num")
144 .unwrap()
145 .as_str();
146 params.tabsize = match tabsize_str.parse::<usize>() {
147 Ok(num) => {
148 if num == 0 {
149 return Err("invalid tabsize «0»".to_string());
150 }
151
152 num
153 }
154 Err(_) => return Err(format!("invalid tabsize «{tabsize_str}»")),
155 };
156
157 continue;
158 }
159 match match_context_diff_params(¶m, next_param, format) {
160 Ok(DiffStyleMatch {
161 is_match,
162 context_count,
163 next_param_consumed,
164 }) => {
165 if is_match {
166 format = Some(Format::Context);
167 if context_count.is_some() {
168 context = context_count;
169 }
170 if next_param_consumed {
171 opts.next();
172 }
173 continue;
174 }
175 }
176 Err(error) => return Err(error),
177 }
178 match match_unified_diff_params(¶m, next_param, format) {
179 Ok(DiffStyleMatch {
180 is_match,
181 context_count,
182 next_param_consumed,
183 }) => {
184 if is_match {
185 format = Some(Format::Unified);
186 if context_count.is_some() {
187 context = context_count;
188 }
189 if next_param_consumed {
190 opts.next();
191 }
192 continue;
193 }
194 }
195 Err(error) => return Err(error),
196 }
197 if param.to_string_lossy().starts_with('-') {
198 return Err(format!("Unknown option: {param:?}"));
199 }
200 if from.is_none() {
201 from = Some(param);
202 } else if to.is_none() {
203 to = Some(param);
204 } else {
205 return Err(format!(
206 "Usage: {} <from> <to>",
207 params.executable.to_string_lossy()
208 ));
209 }
210 }
211 params.from = if let Some(from) = from {
212 from
213 } else if let Some(param) = opts.next() {
214 param
215 } else {
216 return Err(format!(
217 "Usage: {} <from> <to>",
218 params.executable.to_string_lossy()
219 ));
220 };
221 params.to = if let Some(to) = to {
222 to
223 } else if let Some(param) = opts.next() {
224 param
225 } else {
226 return Err(format!(
227 "Usage: {} <from> <to>",
228 params.executable.to_string_lossy()
229 ));
230 };
231
232 let mut from_path: PathBuf = PathBuf::from(¶ms.from);
235 let mut to_path: PathBuf = PathBuf::from(¶ms.to);
236
237 if from_path.is_dir() && to_path.is_file() {
238 from_path.push(to_path.file_name().unwrap());
239 params.from = from_path.into_os_string();
240 } else if from_path.is_file() && to_path.is_dir() {
241 to_path.push(from_path.file_name().unwrap());
242 params.to = to_path.into_os_string();
243 }
244
245 params.format = format.unwrap_or(Format::default());
246 if let Some(context_count) = context {
247 params.context_count = context_count;
248 }
249 Ok(params)
250}
251
252struct DiffStyleMatch {
253 is_match: bool,
254 context_count: Option<usize>,
255 next_param_consumed: bool,
256}
257
258fn match_context_diff_params(
259 param: &OsString,
260 next_param: Option<&OsString>,
261 format: Option<Format>,
262) -> Result<DiffStyleMatch, String> {
263 const CONTEXT_RE: &str = r"^(-[cC](?<num1>\d*)|--context(=(?<num2>\d*))?|-(?<num3>\d+)c)$";
264 let regex = Regex::new(CONTEXT_RE).unwrap();
265 let is_match = regex.is_match(param.to_string_lossy().as_ref());
266 let mut context_count = None;
267 let mut next_param_consumed = false;
268 if is_match {
269 if format.is_some() && format != Some(Format::Context) {
270 return Err("Conflicting output style options".to_string());
271 }
272 let captures = regex.captures(param.to_str().unwrap()).unwrap();
273 let num = captures
274 .name("num1")
275 .or(captures.name("num2"))
276 .or(captures.name("num3"));
277 if let Some(numvalue) = num {
278 if !numvalue.as_str().is_empty() {
279 context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
280 }
281 }
282 if param == "-C" {
283 if let Some(p) = next_param {
284 let size_str = p.to_string_lossy();
285 match size_str.parse::<usize>() {
286 Ok(context_size) => {
287 context_count = Some(context_size);
288 next_param_consumed = true;
289 }
290 Err(_) => return Err(format!("invalid context length '{size_str}'")),
291 }
292 }
293 }
294 }
295 Ok(DiffStyleMatch {
296 is_match,
297 context_count,
298 next_param_consumed,
299 })
300}
301
302fn match_unified_diff_params(
303 param: &OsString,
304 next_param: Option<&OsString>,
305 format: Option<Format>,
306) -> Result<DiffStyleMatch, String> {
307 const UNIFIED_RE: &str = r"^(-[uU](?<num1>\d*)|--unified(=(?<num2>\d*))?|-(?<num3>\d+)u)$";
308 let regex = Regex::new(UNIFIED_RE).unwrap();
309 let is_match = regex.is_match(param.to_string_lossy().as_ref());
310 let mut context_count = None;
311 let mut next_param_consumed = false;
312 if is_match {
313 if format.is_some() && format != Some(Format::Unified) {
314 return Err("Conflicting output style options".to_string());
315 }
316 let captures = regex.captures(param.to_str().unwrap()).unwrap();
317 let num = captures
318 .name("num1")
319 .or(captures.name("num2"))
320 .or(captures.name("num3"));
321 if let Some(numvalue) = num {
322 if !numvalue.as_str().is_empty() {
323 context_count = Some(numvalue.as_str().parse::<usize>().unwrap());
324 }
325 }
326 if param == "-U" {
327 if let Some(p) = next_param {
328 let size_str = p.to_string_lossy();
329 match size_str.parse::<usize>() {
330 Ok(context_size) => {
331 context_count = Some(context_size);
332 next_param_consumed = true;
333 }
334 Err(_) => return Err(format!("invalid context length '{size_str}'")),
335 }
336 }
337 }
338 }
339 Ok(DiffStyleMatch {
340 is_match,
341 context_count,
342 next_param_consumed,
343 })
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 fn os(s: &str) -> OsString {
350 OsString::from(s)
351 }
352 #[test]
353 fn basics() {
354 assert_eq!(
355 Ok(Params {
356 executable: os("diff"),
357 from: os("foo"),
358 to: os("bar"),
359 ..Default::default()
360 }),
361 parse_params(
362 [os("diff"), os("foo"), os("bar")]
363 .iter()
364 .cloned()
365 .peekable()
366 )
367 );
368 assert_eq!(
369 Ok(Params {
370 executable: os("diff"),
371 from: os("foo"),
372 to: os("bar"),
373 ..Default::default()
374 }),
375 parse_params(
376 [os("diff"), os("--normal"), os("foo"), os("bar")]
377 .iter()
378 .cloned()
379 .peekable()
380 )
381 );
382 }
383 #[test]
384 fn basics_ed() {
385 for arg in ["-e", "--ed"] {
386 assert_eq!(
387 Ok(Params {
388 executable: os("diff"),
389 from: os("foo"),
390 to: os("bar"),
391 format: Format::Ed,
392 ..Default::default()
393 }),
394 parse_params(
395 [os("diff"), os(arg), os("foo"), os("bar")]
396 .iter()
397 .cloned()
398 .peekable()
399 )
400 );
401 }
402 }
403 #[test]
404 fn context_valid() {
405 for args in [vec!["-c"], vec!["--context"], vec!["--context="]] {
406 let mut params = vec!["diff"];
407 params.extend(args);
408 params.extend(["foo", "bar"]);
409 assert_eq!(
410 Ok(Params {
411 executable: os("diff"),
412 from: os("foo"),
413 to: os("bar"),
414 format: Format::Context,
415 ..Default::default()
416 }),
417 parse_params(params.iter().map(|x| os(x)).peekable())
418 );
419 }
420 for args in [
421 vec!["-c42"],
422 vec!["-C42"],
423 vec!["-C", "42"],
424 vec!["--context=42"],
425 vec!["-42c"],
426 ] {
427 let mut params = vec!["diff"];
428 params.extend(args);
429 params.extend(["foo", "bar"]);
430 assert_eq!(
431 Ok(Params {
432 executable: os("diff"),
433 from: os("foo"),
434 to: os("bar"),
435 format: Format::Context,
436 context_count: 42,
437 ..Default::default()
438 }),
439 parse_params(params.iter().map(|x| os(x)).peekable())
440 );
441 }
442 }
443 #[test]
444 fn context_invalid() {
445 for args in [
446 vec!["-c", "42"],
447 vec!["-c=42"],
448 vec!["-c="],
449 vec!["-C"],
450 vec!["-C=42"],
451 vec!["-C="],
452 vec!["--context42"],
453 vec!["--context", "42"],
454 vec!["-42C"],
455 ] {
456 let mut params = vec!["diff"];
457 params.extend(args);
458 params.extend(["foo", "bar"]);
459 assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
460 }
461 }
462 #[test]
463 fn unified_valid() {
464 for args in [vec!["-u"], vec!["--unified"], vec!["--unified="]] {
465 let mut params = vec!["diff"];
466 params.extend(args);
467 params.extend(["foo", "bar"]);
468 assert_eq!(
469 Ok(Params {
470 executable: os("diff"),
471 from: os("foo"),
472 to: os("bar"),
473 format: Format::Unified,
474 ..Default::default()
475 }),
476 parse_params(params.iter().map(|x| os(x)).peekable())
477 );
478 }
479 for args in [
480 vec!["-u42"],
481 vec!["-U42"],
482 vec!["-U", "42"],
483 vec!["--unified=42"],
484 vec!["-42u"],
485 ] {
486 let mut params = vec!["diff"];
487 params.extend(args);
488 params.extend(["foo", "bar"]);
489 assert_eq!(
490 Ok(Params {
491 executable: os("diff"),
492 from: os("foo"),
493 to: os("bar"),
494 format: Format::Unified,
495 context_count: 42,
496 ..Default::default()
497 }),
498 parse_params(params.iter().map(|x| os(x)).peekable())
499 );
500 }
501 }
502 #[test]
503 fn unified_invalid() {
504 for args in [
505 vec!["-u", "42"],
506 vec!["-u=42"],
507 vec!["-u="],
508 vec!["-U"],
509 vec!["-U=42"],
510 vec!["-U="],
511 vec!["--unified42"],
512 vec!["--unified", "42"],
513 vec!["-42U"],
514 ] {
515 let mut params = vec!["diff"];
516 params.extend(args);
517 params.extend(["foo", "bar"]);
518 assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err());
519 }
520 }
521 #[test]
522 fn context_count() {
523 assert_eq!(
524 Ok(Params {
525 executable: os("diff"),
526 from: os("foo"),
527 to: os("bar"),
528 format: Format::Unified,
529 context_count: 54,
530 ..Default::default()
531 }),
532 parse_params(
533 [os("diff"), os("-u54"), os("foo"), os("bar")]
534 .iter()
535 .cloned()
536 .peekable()
537 )
538 );
539 assert_eq!(
540 Ok(Params {
541 executable: os("diff"),
542 from: os("foo"),
543 to: os("bar"),
544 format: Format::Unified,
545 context_count: 54,
546 ..Default::default()
547 }),
548 parse_params(
549 [os("diff"), os("-U54"), os("foo"), os("bar")]
550 .iter()
551 .cloned()
552 .peekable()
553 )
554 );
555 assert_eq!(
556 Ok(Params {
557 executable: os("diff"),
558 from: os("foo"),
559 to: os("bar"),
560 format: Format::Unified,
561 context_count: 54,
562 ..Default::default()
563 }),
564 parse_params(
565 [os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
566 .iter()
567 .cloned()
568 .peekable()
569 )
570 );
571 assert_eq!(
572 Ok(Params {
573 executable: os("diff"),
574 from: os("foo"),
575 to: os("bar"),
576 format: Format::Context,
577 context_count: 54,
578 ..Default::default()
579 }),
580 parse_params(
581 [os("diff"), os("-c54"), os("foo"), os("bar")]
582 .iter()
583 .cloned()
584 .peekable()
585 )
586 );
587 }
588 #[test]
589 fn report_identical_files() {
590 assert_eq!(
591 Ok(Params {
592 executable: os("diff"),
593 from: os("foo"),
594 to: os("bar"),
595 ..Default::default()
596 }),
597 parse_params(
598 [os("diff"), os("foo"), os("bar")]
599 .iter()
600 .cloned()
601 .peekable()
602 )
603 );
604 assert_eq!(
605 Ok(Params {
606 executable: os("diff"),
607 from: os("foo"),
608 to: os("bar"),
609 report_identical_files: true,
610 ..Default::default()
611 }),
612 parse_params(
613 [os("diff"), os("-s"), os("foo"), os("bar")]
614 .iter()
615 .cloned()
616 .peekable()
617 )
618 );
619 assert_eq!(
620 Ok(Params {
621 executable: os("diff"),
622 from: os("foo"),
623 to: os("bar"),
624 report_identical_files: true,
625 ..Default::default()
626 }),
627 parse_params(
628 [
629 os("diff"),
630 os("--report-identical-files"),
631 os("foo"),
632 os("bar"),
633 ]
634 .iter()
635 .cloned()
636 .peekable()
637 )
638 );
639 }
640 #[test]
641 fn brief() {
642 assert_eq!(
643 Ok(Params {
644 executable: os("diff"),
645 from: os("foo"),
646 to: os("bar"),
647 ..Default::default()
648 }),
649 parse_params(
650 [os("diff"), os("foo"), os("bar")]
651 .iter()
652 .cloned()
653 .peekable()
654 )
655 );
656 assert_eq!(
657 Ok(Params {
658 executable: os("diff"),
659 from: os("foo"),
660 to: os("bar"),
661 brief: true,
662 ..Default::default()
663 }),
664 parse_params(
665 [os("diff"), os("-q"), os("foo"), os("bar")]
666 .iter()
667 .cloned()
668 .peekable()
669 )
670 );
671 assert_eq!(
672 Ok(Params {
673 executable: os("diff"),
674 from: os("foo"),
675 to: os("bar"),
676 brief: true,
677 ..Default::default()
678 }),
679 parse_params(
680 [os("diff"), os("--brief"), os("foo"), os("bar"),]
681 .iter()
682 .cloned()
683 .peekable()
684 )
685 );
686 }
687 #[test]
688 fn expand_tabs() {
689 assert_eq!(
690 Ok(Params {
691 executable: os("diff"),
692 from: os("foo"),
693 to: os("bar"),
694 ..Default::default()
695 }),
696 parse_params(
697 [os("diff"), os("foo"), os("bar")]
698 .iter()
699 .cloned()
700 .peekable()
701 )
702 );
703 for option in ["-t", "--expand-tabs"] {
704 assert_eq!(
705 Ok(Params {
706 executable: os("diff"),
707 from: os("foo"),
708 to: os("bar"),
709 expand_tabs: true,
710 ..Default::default()
711 }),
712 parse_params(
713 [os("diff"), os(option), os("foo"), os("bar")]
714 .iter()
715 .cloned()
716 .peekable()
717 )
718 );
719 }
720 }
721 #[test]
722 fn tabsize() {
723 assert_eq!(
724 Ok(Params {
725 executable: os("diff"),
726 from: os("foo"),
727 to: os("bar"),
728 ..Default::default()
729 }),
730 parse_params(
731 [os("diff"), os("foo"), os("bar")]
732 .iter()
733 .cloned()
734 .peekable()
735 )
736 );
737 assert_eq!(
738 Ok(Params {
739 executable: os("diff"),
740 from: os("foo"),
741 to: os("bar"),
742 tabsize: 1,
743 ..Default::default()
744 }),
745 parse_params(
746 [os("diff"), os("--tabsize=1"), os("foo"), os("bar")]
747 .iter()
748 .cloned()
749 .peekable()
750 )
751 );
752 assert_eq!(
753 Ok(Params {
754 executable: os("diff"),
755 from: os("foo"),
756 to: os("bar"),
757 tabsize: 42,
758 ..Default::default()
759 }),
760 parse_params(
761 [os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
762 .iter()
763 .cloned()
764 .peekable()
765 )
766 );
767 assert!(parse_params(
768 [os("diff"), os("--tabsize"), os("foo"), os("bar")]
769 .iter()
770 .cloned()
771 .peekable()
772 )
773 .is_err());
774 assert!(parse_params(
775 [os("diff"), os("--tabsize="), os("foo"), os("bar")]
776 .iter()
777 .cloned()
778 .peekable()
779 )
780 .is_err());
781 assert!(parse_params(
782 [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
783 .iter()
784 .cloned()
785 .peekable()
786 )
787 .is_err());
788 assert!(parse_params(
789 [os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
790 .iter()
791 .cloned()
792 .peekable()
793 )
794 .is_err());
795 assert!(parse_params(
796 [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
797 .iter()
798 .cloned()
799 .peekable()
800 )
801 .is_err());
802 assert!(parse_params(
803 [
804 os("diff"),
805 os("--tabsize=92233720368547758088"),
806 os("foo"),
807 os("bar")
808 ]
809 .iter()
810 .cloned()
811 .peekable()
812 )
813 .is_err());
814 }
815 #[test]
816 fn double_dash() {
817 assert_eq!(
818 Ok(Params {
819 executable: os("diff"),
820 from: os("-g"),
821 to: os("-h"),
822 ..Default::default()
823 }),
824 parse_params(
825 [os("diff"), os("--"), os("-g"), os("-h")]
826 .iter()
827 .cloned()
828 .peekable()
829 )
830 );
831 }
832 #[test]
833 fn default_to_stdin() {
834 assert_eq!(
835 Ok(Params {
836 executable: os("diff"),
837 from: os("foo"),
838 to: os("-"),
839 ..Default::default()
840 }),
841 parse_params([os("diff"), os("foo"), os("-")].iter().cloned().peekable())
842 );
843 assert_eq!(
844 Ok(Params {
845 executable: os("diff"),
846 from: os("-"),
847 to: os("bar"),
848 ..Default::default()
849 }),
850 parse_params([os("diff"), os("-"), os("bar")].iter().cloned().peekable())
851 );
852 assert_eq!(
853 Ok(Params {
854 executable: os("diff"),
855 from: os("-"),
856 to: os("-"),
857 ..Default::default()
858 }),
859 parse_params([os("diff"), os("-"), os("-")].iter().cloned().peekable())
860 );
861 assert!(parse_params(
862 [os("diff"), os("foo"), os("bar"), os("-")]
863 .iter()
864 .cloned()
865 .peekable()
866 )
867 .is_err());
868 assert!(parse_params(
869 [os("diff"), os("-"), os("-"), os("-")]
870 .iter()
871 .cloned()
872 .peekable()
873 )
874 .is_err());
875 }
876 #[test]
877 fn missing_arguments() {
878 assert!(parse_params([os("diff")].iter().cloned().peekable()).is_err());
879 assert!(parse_params([os("diff"), os("foo")].iter().cloned().peekable()).is_err());
880 }
881 #[test]
882 fn unknown_argument() {
883 assert!(parse_params(
884 [os("diff"), os("-g"), os("foo"), os("bar")]
885 .iter()
886 .cloned()
887 .peekable()
888 )
889 .is_err());
890 assert!(
891 parse_params([os("diff"), os("-g"), os("bar")].iter().cloned().peekable()).is_err()
892 );
893 assert!(parse_params([os("diff"), os("-g")].iter().cloned().peekable()).is_err());
894 }
895 #[test]
896 fn empty() {
897 assert!(parse_params([].iter().cloned().peekable()).is_err());
898 }
899 #[test]
900 fn conflicting_output_styles() {
901 for (arg1, arg2) in [
902 ("-u", "-c"),
903 ("-u", "-e"),
904 ("-c", "-u"),
905 ("-c", "-U42"),
906 ("-u", "--normal"),
907 ("--normal", "-e"),
908 ("--context", "--normal"),
909 ] {
910 assert!(parse_params(
911 [os("diff"), os(arg1), os(arg2), os("foo"), os("bar")]
912 .iter()
913 .cloned()
914 .peekable()
915 )
916 .is_err());
917 }
918 }
919}