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