diff --git a/src/context_diff.rs b/src/context_diff.rs index a37e776..055f5e0 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -8,7 +8,7 @@ use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; -use crate::utils::get_modification_time; +use crate::utils::header_name; #[derive(Debug, PartialEq)] pub enum DiffLine { @@ -315,14 +315,10 @@ fn make_diff( #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { - let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); - let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); let mut output = format!( - "*** {0}\t{1}\n--- {2}\t{3}\n", - params.from.to_string_lossy(), - from_modified_time, - params.to.to_string_lossy(), - to_modified_time + "*** {}\n--- {}\n", + header_name(¶ms.from, params.from_label.as_ref()), + header_name(¶ms.to, params.to_label.as_ref()) ) .into_bytes(); let diff_results = make_diff(expected, actual, params.context_count, params.brief); diff --git a/src/diff.rs b/src/diff.rs index f4c0614..740d96e 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -26,10 +26,11 @@ pub fn main(opts: Peekable) -> ExitCode { // if from and to are the same file, no need to perform any comparison let maybe_report_identical_files = || { if params.report_identical_files { + let (from, to) = params.names(); println!( "Files {} and {} are identical", - params.from.to_string_lossy(), - params.to.to_string_lossy(), + from.to_string_lossy(), + to.to_string_lossy(), ); } }; @@ -85,10 +86,11 @@ pub fn main(opts: Peekable) -> ExitCode { } }; if params.brief && !result.is_empty() { + let (from, to) = params.names(); println!( "Files {} and {} differ", - params.from.to_string_lossy(), - params.to.to_string_lossy() + from.to_string_lossy(), + to.to_string_lossy() ); } else { io::stdout().write_all(&result).unwrap(); diff --git a/src/params.rs b/src/params.rs index 239ebca..aa9f92c 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,4 +1,4 @@ -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::iter::Peekable; use std::path::PathBuf; @@ -26,6 +26,19 @@ pub struct Params { pub expand_tabs: bool, pub tabsize: usize, pub width: usize, + pub from_label: Option, + pub to_label: Option, +} + +impl Params { + /// The names to show for the from-file and the to-file: their labels if + /// given, else their paths. + pub fn names(&self) -> (&OsString, &OsString) { + ( + self.from_label.as_ref().unwrap_or(&self.from), + self.to_label.as_ref().unwrap_or(&self.to), + ) + } } impl Default for Params { @@ -41,6 +54,8 @@ impl Default for Params { expand_tabs: false, tabsize: 8, width: 130, + from_label: None, + to_label: None, } } } @@ -62,6 +77,32 @@ pub fn parse_params>(mut opts: Peekable) -> Resu let tabsize_re = Regex::new(r"^--tabsize=(?\d+)$").unwrap(); let width_re = Regex::new(r"^--width=(?P\d+)$").unwrap(); while let Some(param) = opts.next() { + // -L LABEL, -LLABEL, --label LABEL or --label=LABEL: the first names + // the from-file in headers and messages, the second the to-file. + // The label may be any bytes, so match on those, not on a str. + let bytes = param.as_encoded_bytes(); + let label = if bytes == b"-L" || bytes == b"--label" { + Some(opts.next().ok_or_else(|| { + format!("option '{}' requires an argument", param.to_string_lossy()) + })?) + } else { + bytes + .strip_prefix(b"--label=") + .or_else(|| bytes.strip_prefix(b"-L")) + // SAFETY: the bytes come from an OsStr and are split right + // after an ASCII prefix. + .map(|rest| unsafe { OsStr::from_encoded_bytes_unchecked(rest) }.to_os_string()) + }; + if let Some(label) = label { + if params.from_label.is_none() { + params.from_label = Some(label); + } else if params.to_label.is_none() { + params.to_label = Some(label); + } else { + return Err("too many file label options".to_string()); + } + continue; + } let next_param = opts.peek(); if param == "--" { break; @@ -831,6 +872,49 @@ mod tests { ); } #[test] + fn labels() { + assert_eq!( + Ok(Params { + executable: os("diff"), + from: os("foo"), + to: os("bar"), + from_label: Some(os("old")), + to_label: Some(os("new")), + ..Default::default() + }), + parse_params( + [ + os("diff"), + os("--label"), + os("old"), + os("-Lnew"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + assert_eq!( + Err("too many file label options".to_string()), + parse_params( + [ + os("diff"), + os("--label=a"), + os("-L"), + os("b"), + os("-Lc"), + os("foo"), + os("bar") + ] + .iter() + .cloned() + .peekable() + ) + ); + } + #[test] fn width_suffix_is_an_operand() { assert_eq!( Ok(Params { @@ -862,6 +946,29 @@ mod tests { .unwrap(); assert_eq!(params.from, bad); } + #[cfg(unix)] + #[test] + fn labels_non_utf8() { + use std::os::unix::ffi::OsStringExt; + let bytes = |b: &[u8]| OsString::from_vec(b.to_vec()); + let params = parse_params( + [ + os("diff"), + bytes(b"-L\xffold"), + bytes(b"--label=\xfenew"), + os("foo"), + os("bar"), + ] + .iter() + .cloned() + .peekable(), + ) + .unwrap(); + assert_eq!(params.from_label, Some(bytes(b"\xffold"))); + assert_eq!(params.to_label, Some(bytes(b"\xfenew"))); + assert_eq!(params.from, os("foo")); + assert_eq!(params.to, os("bar")); + } #[test] fn double_dash() { assert_eq!( diff --git a/src/unified_diff.rs b/src/unified_diff.rs index bd83afe..0d866d1 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -8,7 +8,7 @@ use std::io::Write; use crate::params::Params; use crate::utils::do_write_line; -use crate::utils::get_modification_time; +use crate::utils::header_name; #[derive(Debug, PartialEq)] pub enum DiffLine { @@ -242,14 +242,10 @@ fn make_diff( #[must_use] pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { - let from_modified_time = get_modification_time(¶ms.from.to_string_lossy()); - let to_modified_time = get_modification_time(¶ms.to.to_string_lossy()); let mut output = format!( - "--- {0}\t{1}\n+++ {2}\t{3}\n", - params.from.to_string_lossy(), - from_modified_time, - params.to.to_string_lossy(), - to_modified_time + "--- {}\n+++ {}\n", + header_name(¶ms.from, params.from_label.as_ref()), + header_name(¶ms.to, params.to_label.as_ref()) ) .into_bytes(); let diff_results = make_diff(expected, actual, params.context_count, params.brief); diff --git a/src/utils.rs b/src/utils.rs index d9415af..43e1e91 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -71,6 +71,19 @@ pub fn get_modification_time(file_path: &str) -> String { modification_time } +/// The file name and modification time of a context or unified diff header, +/// or just the label when one was given. +pub fn header_name(path: &OsString, label: Option<&OsString>) -> String { + match label { + Some(label) => label.to_string_lossy().into_owned(), + None => format!( + "{}\t{}", + path.to_string_lossy(), + get_modification_time(&path.to_string_lossy()) + ), + } +} + pub fn format_failure_to_read_input_file( executable: &OsString, filepath: &OsString, diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index fbdd494..a363fb8 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -118,6 +118,71 @@ fn differences_brief() -> Result<(), Box> { Ok(()) } +#[test] +fn labels_replace_file_names() -> Result<(), Box> { + let mut file1 = NamedTempFile::new()?; + file1.write_all("foo\n".as_bytes())?; + let mut file2 = NamedTempFile::new()?; + file2.write_all("bar\n".as_bytes())?; + for (args, expected) in [ + ( + vec!["-u", "--label", "a/x", "--label=b/x"], + "--- a/x\n+++ b/x\n@@ -1 +1 @@\n-foo\n+bar\n", + ), + ( + vec!["-c", "-L", "old", "-Lnew"], + "*** old\n--- new\n***************\n*** 1 ****\n! foo\n--- 1 ----\n! bar\n", + ), + ( + vec!["-q", "-L", "old", "-L", "new"], + "Files old and new differ\n", + ), + ] { + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff") + .args(args) + .arg(file1.path()) + .arg(file2.path()); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout(predicate::eq(expected)); + } + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff") + .args(["-L", "a", "-L", "b", "-L", "c"]) + .arg(file1.path()) + .arg(file2.path()); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::contains("too many file label options")); + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff") + .arg(file1.path()) + .arg(file2.path()) + .arg("-L"); + cmd.assert() + .code(predicate::eq(2)) + .failure() + .stderr(predicate::str::contains("option '-L' requires an argument")); + + let mut same = NamedTempFile::new()?; + same.write_all("foo\n".as_bytes())?; + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff") + .args(["-s", "-L", "old", "-L", "new"]) + .arg(file1.path()) + .arg(same.path()); + cmd.assert() + .code(predicate::eq(0)) + .success() + .stdout(predicate::eq("Files old and new are identical\n")); + Ok(()) +} + #[test] fn missing_newline() -> Result<(), Box> { let mut file1 = NamedTempFile::new()?;