diff --git a/Cargo.lock b/Cargo.lock index 9dc8153..15c81e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,7 @@ dependencies = [ "diff", "predicates", "pretty_assertions", + "regex", "same-file", "tempfile", "unicode-width", diff --git a/Cargo.toml b/Cargo.toml index 7eeb35b..4ddf5fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [dependencies] diff = "0.1.10" +regex = "1.10.3" same-file = "1.0.6" unicode-width = "0.1.11" diff --git a/src/context_diff.rs b/src/context_diff.rs index 408821f..9f1db55 100644 --- a/src/context_diff.rs +++ b/src/context_diff.rs @@ -273,6 +273,7 @@ pub fn diff( context_size: usize, stop_early: bool, expand_tabs: bool, + tabsize: usize, ) -> Vec { let mut output = format!("*** {expected_filename}\t\n--- {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size, stop_early); @@ -317,19 +318,19 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "- ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -347,19 +348,19 @@ pub fn diff( match line { DiffLine::Context(e) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Change(e) => { write!(output, "! ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Add(e) => { write!(output, "+ ").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -434,6 +435,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -514,6 +516,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -597,6 +600,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -683,6 +687,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -729,6 +734,7 @@ mod tests { context_size, false, false, + 8, ); let expected_full = [ "*** foo\t", @@ -755,6 +761,7 @@ mod tests { context_size, true, false, + 8, ); let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); @@ -767,6 +774,7 @@ mod tests { context_size, false, false, + 8, ); assert!(nodiff_full.is_empty()); @@ -778,6 +786,7 @@ mod tests { context_size, true, false, + 8, ); assert!(nodiff_brief.is_empty()); } diff --git a/src/ed_diff.rs b/src/ed_diff.rs index 6d47b9f..c02289c 100644 --- a/src/ed_diff.rs +++ b/src/ed_diff.rs @@ -114,6 +114,7 @@ pub fn diff( actual: &[u8], stop_early: bool, expand_tabs: bool, + tabsize: usize, ) -> Result, DiffError> { let mut output = Vec::new(); let diff_results = make_diff(expected, actual, stop_early)?; @@ -152,7 +153,7 @@ pub fn diff( if actual == b"." { writeln!(&mut output, "..\n.\ns/.//\na").unwrap(); } else { - do_write_line(&mut output, actual, expand_tabs).unwrap(); + do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap(); writeln!(&mut output).unwrap(); } } @@ -167,7 +168,7 @@ mod tests { use super::*; use pretty_assertions::assert_eq; pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result, DiffError> { - let mut output = diff(expected, actual, false, false)?; + let mut output = diff(expected, actual, false, false, 8)?; writeln!(&mut output, "w {filename}").unwrap(); Ok(output) } @@ -176,7 +177,7 @@ mod tests { fn test_basic() { let from = b"a\n"; let to = b"b\n"; - let diff = diff(from, to, false, false).unwrap(); + let diff = diff(from, to, false, false, 8).unwrap(); let expected = ["1c", "b", ".", ""].join("\n"); assert_eq!(diff, expected.as_bytes()); } @@ -411,18 +412,18 @@ mod tests { let from = ["a", "b", "c", ""].join("\n"); let to = ["a", "d", "c", ""].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false).unwrap(); + let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8).unwrap(); let expected_full = ["2c", "d", ".", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false).unwrap(); + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8).unwrap(); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false).unwrap(); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8).unwrap(); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false).unwrap(); + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8).unwrap(); assert!(nodiff_brief.is_empty()); } } diff --git a/src/main.rs b/src/main.rs index f074cb1..2a6d4ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ fn main() -> ExitCode { report_identical_files, brief, expand_tabs, + tabsize, } = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); @@ -67,7 +68,9 @@ fn main() -> ExitCode { }; // run diff let result: Vec = match format { - Format::Normal => normal_diff::diff(&from_content, &to_content, brief, expand_tabs), + Format::Normal => { + normal_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize) + } Format::Unified => unified_diff::diff( &from_content, &from.to_string_lossy(), @@ -76,6 +79,7 @@ fn main() -> ExitCode { context_count, brief, expand_tabs, + tabsize, ), Format::Context => context_diff::diff( &from_content, @@ -85,13 +89,13 @@ fn main() -> ExitCode { context_count, brief, expand_tabs, + tabsize, ), - Format::Ed => { - ed_diff::diff(&from_content, &to_content, brief, expand_tabs).unwrap_or_else(|error| { + Format::Ed => ed_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize) + .unwrap_or_else(|error| { eprintln!("{error}"); exit(2); - }) - } + }), }; if brief && !result.is_empty() { println!( diff --git a/src/normal_diff.rs b/src/normal_diff.rs index e25a6c6..b26de77 100644 --- a/src/normal_diff.rs +++ b/src/normal_diff.rs @@ -116,7 +116,13 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec } #[must_use] -pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool, expand_tabs: bool) -> Vec { +pub fn diff( + expected: &[u8], + actual: &[u8], + stop_early: bool, + expand_tabs: bool, + tabsize: usize, +) -> Vec { // See https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html // for details on the syntax of the normal format. let mut output = Vec::new(); @@ -190,7 +196,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool, expand_tabs: bool) } for expected in &result.expected { write!(&mut output, "< ").unwrap(); - do_write_line(&mut output, expected, expand_tabs).unwrap(); + do_write_line(&mut output, expected, expand_tabs, tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.expected_missing_nl { @@ -201,7 +207,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool, expand_tabs: bool) } for actual in &result.actual { write!(&mut output, "> ").unwrap(); - do_write_line(&mut output, actual, expand_tabs).unwrap(); + do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap(); writeln!(&mut output).unwrap(); } if result.actual_missing_nl { @@ -222,7 +228,7 @@ mod tests { a.write_all(b"a\n").unwrap(); let mut b = Vec::new(); b.write_all(b"b\n").unwrap(); - let diff = diff(&a, &b, false, false); + let diff = diff(&a, &b, false, false, 8); let expected = b"1c1\n< a\n---\n> b\n".to_vec(); assert_eq!(diff, expected); } @@ -275,7 +281,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/ab.diff")) .unwrap() .write_all(&diff) @@ -367,7 +373,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/abn.diff")) .unwrap() .write_all(&diff) @@ -441,7 +447,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/ab_.diff")) .unwrap() .write_all(&diff) @@ -519,7 +525,7 @@ mod tests { } // This test diff is intentionally reversed. // We want it to turn the alef into bet. - let diff = diff(&alef, &bet, false, false); + let diff = diff(&alef, &bet, false, false, 8); File::create(&format!("{target}/abr.diff")) .unwrap() .write_all(&diff) @@ -554,18 +560,18 @@ mod tests { let from = ["a", "b", "c"].join("\n"); let to = ["a", "d", "c"].join("\n"); - let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false); + let diff_full = diff(from.as_bytes(), to.as_bytes(), false, false, 8); let expected_full = ["2c2", "< b", "---", "> d", ""].join("\n"); assert_eq!(diff_full, expected_full.as_bytes()); - let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false); + let diff_brief = diff(from.as_bytes(), to.as_bytes(), true, false, 8); let expected_brief = "\0".as_bytes(); assert_eq!(diff_brief, expected_brief); - let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false); + let nodiff_full = diff(from.as_bytes(), from.as_bytes(), false, false, 8); assert!(nodiff_full.is_empty()); - let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false); + let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8); assert!(nodiff_brief.is_empty()); } } diff --git a/src/params.rs b/src/params.rs index a576f3d..f511e7c 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,5 +1,7 @@ use std::ffi::{OsStr, OsString}; +use regex::Regex; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Format { Normal, @@ -8,6 +10,8 @@ pub enum Format { Ed, } +const DEFAULT_TABSIZE: usize = 8; + #[cfg(unix)] fn osstr_bytes(osstr: &OsStr) -> &[u8] { use std::os::unix::ffi::OsStrExt; @@ -28,6 +32,7 @@ pub struct Params { pub report_identical_files: bool, pub brief: bool, pub expand_tabs: bool, + pub tabsize: usize, } pub fn parse_params>(opts: I) -> Result { @@ -44,6 +49,8 @@ pub fn parse_params>(opts: I) -> Result\d+)$").unwrap(); + let mut tabsize = DEFAULT_TABSIZE; while let Some(param) = opts.next() { if param == "--" { break; @@ -70,6 +77,22 @@ pub fn parse_params>(opts: I) -> Result() { + Ok(num) => num, + Err(_) => return Err(format!("invalid tabsize «{}»", tabsize_str)), + }; + continue; + } let p = osstr_bytes(¶m); if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') { let mut bit = p[1..].iter().copied().peekable(); @@ -154,6 +177,7 @@ pub fn parse_params>(opts: I) -> Result Vec { let mut output = format!("--- {expected_filename}\t\n+++ {actual_filename}\t\n").into_bytes(); let diff_results = make_diff(expected, actual, context_size, stop_early); @@ -374,19 +375,19 @@ pub fn diff( match line { DiffLine::Expected(e) => { write!(output, "-").expect("write to Vec is infallible"); - do_write_line(&mut output, &e, expand_tabs) + do_write_line(&mut output, &e, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Context(c) => { write!(output, " ").expect("write to Vec is infallible"); - do_write_line(&mut output, &c, expand_tabs) + do_write_line(&mut output, &c, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } DiffLine::Actual(r) => { write!(output, "+",).expect("write to Vec is infallible"); - do_write_line(&mut output, &r, expand_tabs) + do_write_line(&mut output, &r, expand_tabs, tabsize) .expect("write to Vec is infallible"); writeln!(output).unwrap(); } @@ -461,6 +462,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab.diff")) .unwrap() @@ -576,6 +578,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abn.diff")) .unwrap() @@ -671,6 +674,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/ab_.diff")) .unwrap() @@ -751,6 +755,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abx.diff")) .unwrap() @@ -836,6 +841,7 @@ mod tests { 2, false, false, + 8, ); File::create(&format!("{target}/abr.diff")) .unwrap() @@ -881,6 +887,7 @@ mod tests { context_size, false, false, + 8, ); let expected_full = [ "--- foo\t", @@ -903,6 +910,7 @@ mod tests { context_size, true, false, + 8, ); let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n"); assert_eq!(diff_brief, expected_brief.as_bytes()); @@ -915,6 +923,7 @@ mod tests { context_size, false, false, + 8, ); assert!(nodiff_full.is_empty()); @@ -926,6 +935,7 @@ mod tests { context_size, true, false, + 8, ); assert!(nodiff_brief.is_empty()); } diff --git a/src/utils.rs b/src/utils.rs index 1d13682..94d950f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -38,9 +38,14 @@ pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec { /// Write a single line to an output stream, expanding tabs to space if necessary. /// This assumes that line does not contain any line breaks /// (if it does and tabs are to be expanded to spaces, the result is undefined). -pub fn do_write_line(output: &mut Vec, line: &[u8], expand_tabs: bool) -> std::io::Result<()> { +pub fn do_write_line( + output: &mut Vec, + line: &[u8], + expand_tabs: bool, + tabsize: usize, +) -> std::io::Result<()> { if expand_tabs { - output.write_all(do_expand_tabs(line, 8).as_slice()) + output.write_all(do_expand_tabs(line, tabsize).as_slice()) } else { output.write_all(line) } @@ -96,17 +101,17 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - fn assert_line_written(line: &str, expand_tabs: bool, expected: &str) { + fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) { let mut output: Vec = Vec::new(); - assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs).is_ok()); + assert!(do_write_line(&mut output, line.as_bytes(), expand_tabs, tabsize).is_ok()); assert_eq!(output, expected.as_bytes()); } #[test] fn basics() { - assert_line_written("foo bar baz", false, "foo bar baz"); - assert_line_written("foo bar\tbaz", false, "foo bar\tbaz"); - assert_line_written("foo bar\tbaz", true, "foo bar baz"); + assert_line_written("foo bar baz", false, 8, "foo bar baz"); + assert_line_written("foo bar\tbaz", false, 8, "foo bar\tbaz"); + assert_line_written("foo bar\tbaz", true, 8, "foo bar baz"); } } }