diffutilslib/
params.rs

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    // parse CLI
50
51    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            // Because param matches the regular expression,
138            // it is safe to assume it is valid UTF-8.
139            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(&param, 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(&param, 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    // diff DIRECTORY FILE => diff DIRECTORY/FILE FILE
233    // diff FILE DIRECTORY => diff FILE DIRECTORY/FILE
234    let mut from_path: PathBuf = PathBuf::from(&params.from);
235    let mut to_path: PathBuf = PathBuf::from(&params.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}