diff --git a/src/diff.rs b/src/diff.rs index f4c0614..9a9a578 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use crate::params::{parse_params, Format}; +use crate::params::{parse_params, Format, MissingFile}; use crate::utils::report_failure_to_read_input_file; use crate::{context_diff, ed_diff, normal_diff, side_diff, unified_diff}; use std::env::ArgsOs; @@ -52,6 +52,11 @@ pub fn main(opts: Peekable) -> ExitCode { let mut io_error = false; let from_content = match read_file_contents(¶ms.from) { Ok(from_content) => from_content, + Err(e) + if e.kind() == io::ErrorKind::NotFound && params.missing_file != MissingFile::Error => + { + vec![] + } Err(e) => { report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e); io_error = true; @@ -60,6 +65,11 @@ pub fn main(opts: Peekable) -> ExitCode { }; let to_content = match read_file_contents(¶ms.to) { Ok(to_content) => to_content, + Err(e) + if e.kind() == io::ErrorKind::NotFound && params.missing_file == MissingFile::Empty => + { + vec![] + } Err(e) => { report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e); io_error = true; diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 81ddfb4..f1ac5cd 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -126,6 +126,7 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Result, match (expected_count, actual_count) { (0, 0) => unreachable!(), (0, _) => writeln!(&mut output, "{}a", line_number_expected - 1).unwrap(), + (1, 0) => writeln!(&mut output, "{line_number_expected}d").unwrap(), (_, 0) => writeln!( &mut output, "{},{}d", @@ -180,6 +181,12 @@ mod tests { assert_eq!(diff, expected.as_bytes()); } + #[test] + fn test_single_line_deleted() { + let diff = diff(b"a\nb\n", b"a\n", &Params::default()).unwrap(); + assert_eq!(diff, b"2d\n"); + } + #[test] fn test_permutations() { let target = "target/ed-diff/"; diff --git a/src/normal_diff.rs b/src/normal_diff.rs index 652dc84..c930b95 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -133,6 +133,13 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { let actual_count = result.actual.len(); match (expected_count, actual_count) { (0, 0) => unreachable!(), + (0, 1) => writeln!( + // exactly one line added + &mut output, + "{}a{line_number_actual}", + line_number_expected - 1 + ) + .unwrap(), (0, _) => writeln!( // 'a' stands for "Add lines" &mut output, @@ -142,6 +149,13 @@ pub fn diff(expected: &[u8], actual: &[u8], params: &Params) -> Vec { line_number_actual + actual_count - 1 ) .unwrap(), + (1, 0) => writeln!( + // exactly one line deleted + &mut output, + "{line_number_expected}d{}", + line_number_actual - 1 + ) + .unwrap(), (_, 0) => writeln!( // 'd' stands for "Delete lines" &mut output, @@ -228,6 +242,15 @@ mod tests { assert_eq!(diff, expected); } + #[test] + fn test_single_line_added_or_deleted() { + // A one-line range is written as a single number, as for c. + let added = diff(b"a\n", b"a\nb\n", &Params::default()); + assert_eq!(added, b"1a2\n> b\n".to_vec()); + let deleted = diff(b"a\nb\n", b"a\n", &Params::default()); + assert_eq!(deleted, b"2d1\n< b\n".to_vec()); + } + #[test] fn test_permutations() { let target = "target/normal-diff/"; diff --git a/src/params.rs b/src/params.rs index 239ebca..f3e77f1 100644 --- a/src/params.rs +++ b/src/params.rs @@ -14,6 +14,19 @@ pub enum Format { SideBySide, } +/// How to handle an operand that does not exist (`-N` and +/// `--unidirectional-new-file`). +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum MissingFile { + /// A missing operand is an error. + #[default] + Error, + /// A missing operand is treated as an empty file. + Empty, + /// Only a missing first operand is treated as an empty file. + EmptyIfFirst, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct Params { pub executable: OsString, @@ -26,6 +39,7 @@ pub struct Params { pub expand_tabs: bool, pub tabsize: usize, pub width: usize, + pub missing_file: MissingFile, } impl Default for Params { @@ -41,6 +55,7 @@ impl Default for Params { expand_tabs: false, tabsize: 8, width: 130, + missing_file: MissingFile::default(), } } } @@ -87,6 +102,16 @@ pub fn parse_params>(mut opts: Peekable) -> Resu params.brief = true; continue; } + if param == "-N" || param == "--new-file" { + params.missing_file = MissingFile::Empty; + continue; + } + if param == "--unidirectional-new-file" { + if params.missing_file != MissingFile::Empty { + params.missing_file = MissingFile::EmptyIfFirst; + } + continue; + } if param == "-t" || param == "--expand-tabs" { params.expand_tabs = true; continue; diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index fbdd494..3d57243 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -206,6 +206,44 @@ fn read_from_stdin() -> Result<(), Box> { Ok(()) } +#[test] +fn missing_file_as_empty() -> Result<(), Box> { + let tmp_dir = tempdir()?; + let present = tmp_dir.path().join("present"); + let absent = tmp_dir.path().join("absent"); + std::fs::write(&present, "kiwi\nmango\n")?; + + for option in ["-N", "--new-file", "--unidirectional-new-file"] { + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg(option).arg(&absent).arg(&present); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout("0a1,2\n> kiwi\n> mango\n"); + } + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-N").arg(&present).arg(&absent); + cmd.assert() + .code(predicate::eq(1)) + .failure() + .stdout("1,2d0\n< kiwi\n< mango\n"); + + // Only a missing first operand is accepted here. + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff") + .arg("--unidirectional-new-file") + .arg(&present) + .arg(&absent); + cmd.assert().code(predicate::eq(2)).failure(); + + // Without the option, a missing file is trouble. + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg(&absent).arg(&present); + cmd.assert().code(predicate::eq(2)).failure(); + Ok(()) +} + #[test] fn compare_file_to_directory() -> Result<(), Box> { let tmp_dir = tempdir()?; @@ -288,3 +326,51 @@ fn sdiff_gutter_marker_column() -> Result<(), Box> { Ok(()) } + +#[test] +fn normal_format_single_line_ranges() -> Result<(), Box> { + let tmp_dir = tempdir()?; + let old = tmp_dir.path().join("old"); + let new = tmp_dir.path().join("new"); + std::fs::write(&old, "north\neast\nsouth\nwest\n")?; + std::fs::write(&new, "north\nsouth\nwest\nup\ndown\n")?; + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg(&old).arg(&new); + cmd.assert() + .code(predicate::eq(1)) + .stdout("2d1\n< east\n4a4,5\n> up\n> down\n"); + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("/dev/null").arg(&old); + if cfg!(unix) { + cmd.assert() + .code(predicate::eq(1)) + .stdout("0a1,4\n> north\n> east\n> south\n> west\n"); + } + + std::fs::write(&new, "north\neast\nsouth\nwest\nup\n")?; + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg(&old).arg(&new); + cmd.assert().code(predicate::eq(1)).stdout("4a5\n> up\n"); + Ok(()) +} + +#[test] +fn ed_format_single_line_deletions() -> Result<(), Box> { + let tmp_dir = tempdir()?; + let old = tmp_dir.path().join("old"); + let new = tmp_dir.path().join("new"); + std::fs::write(&old, "red\norange\nyellow\ngreen\nblue\n")?; + std::fs::write(&new, "red\norange\nyellow\nblue\n")?; + + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-e").arg(&old).arg(&new); + cmd.assert().code(predicate::eq(1)).stdout("4d\n"); + + std::fs::write(&new, "red\nblue\n")?; + let mut cmd = cargo_bin_cmd!("diffutils"); + cmd.arg("diff").arg("-e").arg(&old).arg(&new); + cmd.assert().code(predicate::eq(1)).stdout("2,4d\n"); + Ok(()) +}