Merge pull request #28 from oSoMoN/implement-expand-tabs-option

Implement `-t`/`--expand-tabs` option (fixes #26) and `--tabsize=NUM` option (fixes #27)
This commit is contained in:
Sylvestre Ledru
2024-03-30 15:28:53 +01:00
committed by GitHub
13 changed files with 429 additions and 36 deletions
Generated
+8
View File
@@ -81,8 +81,10 @@ dependencies = [
"diff",
"predicates",
"pretty_assertions",
"regex",
"same-file",
"tempfile",
"unicode-width",
]
[[package]]
@@ -313,6 +315,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "wait-timeout"
version = "0.2.0"
+2
View File
@@ -16,7 +16,9 @@ path = "src/main.rs"
[dependencies]
diff = "0.1.10"
regex = "1.10.3"
same-file = "1.0.6"
unicode-width = "0.1.11"
[dev-dependencies]
pretty_assertions = "1"
+1 -1
View File
@@ -8,7 +8,7 @@ use std::io::Write;
use std::process::Command;
fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
let mut output = ed_diff::diff(expected, actual)?;
let mut output = ed_diff::diff(expected, actual, false, false, 8)?;
writeln!(&mut output, "w {filename}").unwrap();
Ok(output)
}
+1 -1
View File
@@ -21,7 +21,7 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>)| {
} else {
return
}*/
let diff = normal_diff::diff(&from, &to);
let diff = normal_diff::diff(&from, &to, false, false, 8);
File::create("target/fuzz.file.original")
.unwrap()
.write_all(&from)
+3
View File
@@ -26,6 +26,9 @@ fuzz_target!(|x: (Vec<u8>, Vec<u8>, u8)| {
&to,
"target/fuzz.file",
context as usize,
false,
false,
8,
);
File::create("target/fuzz.file.original")
.unwrap()
+33 -6
View File
@@ -6,6 +6,8 @@
use std::collections::VecDeque;
use std::io::Write;
use crate::utils::do_write_line;
#[derive(Debug, PartialEq)]
pub enum DiffLine {
Context(Vec<u8>),
@@ -263,6 +265,7 @@ fn make_diff(
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn diff(
expected: &[u8],
expected_filename: &str,
@@ -270,6 +273,8 @@ pub fn diff(
actual_filename: &str,
context_size: usize,
stop_early: bool,
expand_tabs: bool,
tabsize: usize,
) -> Vec<u8> {
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);
@@ -314,17 +319,20 @@ pub fn diff(
match line {
DiffLine::Context(e) => {
write!(output, " ").expect("write to Vec is infallible");
output.write_all(&e).expect("write to Vec is infallible");
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");
output.write_all(&e).expect("write to Vec is infallible");
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");
output.write_all(&e).expect("write to Vec is infallible");
do_write_line(&mut output, &e, expand_tabs, tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
}
@@ -341,17 +349,20 @@ pub fn diff(
match line {
DiffLine::Context(e) => {
write!(output, " ").expect("write to Vec is infallible");
output.write_all(&e).expect("write to Vec is infallible");
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");
output.write_all(&e).expect("write to Vec is infallible");
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");
output.write_all(&e).expect("write to Vec is infallible");
do_write_line(&mut output, &e, expand_tabs, tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
}
@@ -424,6 +435,8 @@ mod tests {
&format!("{target}/alef"),
2,
false,
false,
8,
);
File::create(&format!("{target}/ab.diff"))
.unwrap()
@@ -503,6 +516,8 @@ mod tests {
&format!("{target}/alef_"),
2,
false,
false,
8,
);
File::create(&format!("{target}/ab_.diff"))
.unwrap()
@@ -585,6 +600,8 @@ mod tests {
&format!("{target}/alefx"),
2,
false,
false,
8,
);
File::create(&format!("{target}/abx.diff"))
.unwrap()
@@ -670,6 +687,8 @@ mod tests {
&format!("{target}/alefr"),
2,
false,
false,
8,
);
File::create(&format!("{target}/abr.diff"))
.unwrap()
@@ -715,6 +734,8 @@ mod tests {
to_filename,
context_size,
false,
false,
8,
);
let expected_full = [
"*** foo\t",
@@ -740,6 +761,8 @@ mod tests {
to_filename,
context_size,
true,
false,
8,
);
let expected_brief = ["*** foo\t", "--- bar\t", ""].join("\n");
assert_eq!(diff_brief, expected_brief.as_bytes());
@@ -751,6 +774,8 @@ mod tests {
to_filename,
context_size,
false,
false,
8,
);
assert!(nodiff_full.is_empty());
@@ -761,6 +786,8 @@ mod tests {
to_filename,
context_size,
true,
false,
8,
);
assert!(nodiff_brief.is_empty());
}
+16 -8
View File
@@ -5,6 +5,8 @@
use std::io::Write;
use crate::utils::do_write_line;
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: usize,
@@ -107,7 +109,13 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<Mis
Ok(results)
}
pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<u8>, DiffError> {
pub fn diff(
expected: &[u8],
actual: &[u8],
stop_early: bool,
expand_tabs: bool,
tabsize: usize,
) -> Result<Vec<u8>, DiffError> {
let mut output = Vec::new();
let diff_results = make_diff(expected, actual, stop_early)?;
if stop_early && !diff_results.is_empty() {
@@ -145,7 +153,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Result<Vec<u8>,
if actual == b"." {
writeln!(&mut output, "..\n.\ns/.//\na").unwrap();
} else {
output.write_all(actual).unwrap();
do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap();
writeln!(&mut output).unwrap();
}
}
@@ -160,7 +168,7 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
pub fn diff_w(expected: &[u8], actual: &[u8], filename: &str) -> Result<Vec<u8>, DiffError> {
let mut output = diff(expected, actual, false)?;
let mut output = diff(expected, actual, false, false, 8)?;
writeln!(&mut output, "w {filename}").unwrap();
Ok(output)
}
@@ -169,7 +177,7 @@ mod tests {
fn test_basic() {
let from = b"a\n";
let to = b"b\n";
let diff = diff(from, to, false).unwrap();
let diff = diff(from, to, false, false, 8).unwrap();
let expected = ["1c", "b", ".", ""].join("\n");
assert_eq!(diff, expected.as_bytes());
}
@@ -404,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).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).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).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).unwrap();
let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8).unwrap();
assert!(nodiff_brief.is_empty());
}
}
+1
View File
@@ -2,6 +2,7 @@ pub mod context_diff;
pub mod ed_diff;
pub mod normal_diff;
pub mod unified_diff;
pub mod utils;
// Re-export the public functions/types you need
pub use context_diff::diff as context_diff;
+15 -5
View File
@@ -15,6 +15,7 @@ mod ed_diff;
mod normal_diff;
mod params;
mod unified_diff;
mod utils;
// Exit codes are documented at
// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html.
@@ -30,6 +31,8 @@ fn main() -> ExitCode {
format,
report_identical_files,
brief,
expand_tabs,
tabsize,
} = parse_params(opts).unwrap_or_else(|error| {
eprintln!("{error}");
exit(2);
@@ -65,7 +68,9 @@ fn main() -> ExitCode {
};
// run diff
let result: Vec<u8> = match format {
Format::Normal => normal_diff::diff(&from_content, &to_content, brief),
Format::Normal => {
normal_diff::diff(&from_content, &to_content, brief, expand_tabs, tabsize)
}
Format::Unified => unified_diff::diff(
&from_content,
&from.to_string_lossy(),
@@ -73,6 +78,8 @@ fn main() -> ExitCode {
&to.to_string_lossy(),
context_count,
brief,
expand_tabs,
tabsize,
),
Format::Context => context_diff::diff(
&from_content,
@@ -81,11 +88,14 @@ fn main() -> ExitCode {
&to.to_string_lossy(),
context_count,
brief,
expand_tabs,
tabsize,
),
Format::Ed => ed_diff::diff(&from_content, &to_content, brief).unwrap_or_else(|error| {
eprintln!("{error}");
exit(2);
}),
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!(
+20 -12
View File
@@ -5,6 +5,8 @@
use std::io::Write;
use crate::utils::do_write_line;
#[derive(Debug, PartialEq)]
struct Mismatch {
pub line_number_expected: usize,
@@ -114,7 +116,13 @@ fn make_diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<Mismatch>
}
#[must_use]
pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<u8> {
pub fn diff(
expected: &[u8],
actual: &[u8],
stop_early: bool,
expand_tabs: bool,
tabsize: usize,
) -> Vec<u8> {
// 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();
@@ -188,7 +196,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<u8> {
}
for expected in &result.expected {
write!(&mut output, "< ").unwrap();
output.write_all(expected).unwrap();
do_write_line(&mut output, expected, expand_tabs, tabsize).unwrap();
writeln!(&mut output).unwrap();
}
if result.expected_missing_nl {
@@ -199,7 +207,7 @@ pub fn diff(expected: &[u8], actual: &[u8], stop_early: bool) -> Vec<u8> {
}
for actual in &result.actual {
write!(&mut output, "> ").unwrap();
output.write_all(actual).unwrap();
do_write_line(&mut output, actual, expand_tabs, tabsize).unwrap();
writeln!(&mut output).unwrap();
}
if result.actual_missing_nl {
@@ -220,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);
let diff = diff(&a, &b, false, false, 8);
let expected = b"1c1\n< a\n---\n> b\n".to_vec();
assert_eq!(diff, expected);
}
@@ -273,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);
let diff = diff(&alef, &bet, false, false, 8);
File::create(&format!("{target}/ab.diff"))
.unwrap()
.write_all(&diff)
@@ -365,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);
let diff = diff(&alef, &bet, false, false, 8);
File::create(&format!("{target}/abn.diff"))
.unwrap()
.write_all(&diff)
@@ -439,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);
let diff = diff(&alef, &bet, false, false, 8);
File::create(&format!("{target}/ab_.diff"))
.unwrap()
.write_all(&diff)
@@ -517,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);
let diff = diff(&alef, &bet, false, false, 8);
File::create(&format!("{target}/abr.diff"))
.unwrap()
.write_all(&diff)
@@ -552,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);
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);
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);
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);
let nodiff_brief = diff(from.as_bytes(), from.as_bytes(), true, false, 8);
assert!(nodiff_brief.is_empty());
}
}
+183
View File
@@ -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;
@@ -27,6 +31,8 @@ pub struct Params {
pub context_count: usize,
pub report_identical_files: bool,
pub brief: bool,
pub expand_tabs: bool,
pub tabsize: usize,
}
pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params, String> {
@@ -42,6 +48,9 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
let mut context_count = 3;
let mut report_identical_files = false;
let mut brief = false;
let mut expand_tabs = false;
let tabsize_re = Regex::new(r"^--tabsize=(?<num>\d+)$").unwrap();
let mut tabsize = DEFAULT_TABSIZE;
while let Some(param) = opts.next() {
if param == "--" {
break;
@@ -64,6 +73,26 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
brief = true;
continue;
}
if param == "-t" || param == "--expand-tabs" {
expand_tabs = true;
continue;
}
if tabsize_re.is_match(param.to_string_lossy().as_ref()) {
// Because param matches the regular expression,
// it is safe to assume it is valid UTF-8.
let param = param.into_string().unwrap();
let tabsize_str = tabsize_re
.captures(param.as_str())
.unwrap()
.name("num")
.unwrap()
.as_str();
tabsize = match tabsize_str.parse::<usize>() {
Ok(num) => num,
Err(_) => return Err(format!("invalid tabsize «{}»", tabsize_str)),
};
continue;
}
let p = osstr_bytes(&param);
if p.first() == Some(&b'-') && p.get(1) != Some(&b'-') {
let mut bit = p[1..].iter().copied().peekable();
@@ -147,6 +176,8 @@ pub fn parse_params<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params,
context_count,
report_identical_files,
brief,
expand_tabs,
tabsize,
})
}
@@ -166,6 +197,8 @@ mod tests {
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
@@ -180,6 +213,8 @@ mod tests {
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params([os("diff"), os("-e"), os("foo"), os("bar")].iter().cloned())
);
@@ -194,6 +229,8 @@ mod tests {
context_count: 54,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params(
[os("diff"), os("-u54"), os("foo"), os("bar")]
@@ -209,6 +246,8 @@ mod tests {
context_count: 54,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params(
[os("diff"), os("-U54"), os("foo"), os("bar")]
@@ -224,6 +263,8 @@ mod tests {
context_count: 54,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params(
[os("diff"), os("-U"), os("54"), os("foo"), os("bar")]
@@ -239,6 +280,8 @@ mod tests {
context_count: 54,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params(
[os("diff"), os("-c54"), os("foo"), os("bar")]
@@ -257,6 +300,8 @@ mod tests {
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
@@ -268,6 +313,8 @@ mod tests {
context_count: 3,
report_identical_files: true,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned())
);
@@ -279,6 +326,8 @@ mod tests {
context_count: 3,
report_identical_files: true,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params(
[
@@ -302,6 +351,8 @@ mod tests {
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: 8,
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
@@ -313,6 +364,8 @@ mod tests {
context_count: 3,
report_identical_files: false,
brief: true,
expand_tabs: false,
tabsize: 8,
}),
parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned())
);
@@ -324,6 +377,8 @@ mod tests {
context_count: 3,
report_identical_files: false,
brief: true,
expand_tabs: false,
tabsize: 8,
}),
parse_params(
[os("diff"), os("--brief"), os("foo"), os("bar"),]
@@ -333,6 +388,132 @@ mod tests {
);
}
#[test]
fn expand_tabs() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
format: Format::Normal,
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
for option in ["-t", "--expand-tabs"] {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
format: Format::Normal,
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: true,
tabsize: DEFAULT_TABSIZE,
}),
parse_params(
[os("diff"), os(option), os("foo"), os("bar")]
.iter()
.cloned()
)
);
}
}
#[test]
fn tabsize() {
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
format: Format::Normal,
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params([os("diff"), os("foo"), os("bar")].iter().cloned())
);
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
format: Format::Normal,
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: 0,
}),
parse_params(
[os("diff"), os("--tabsize=0"), os("foo"), os("bar")]
.iter()
.cloned()
)
);
assert_eq!(
Ok(Params {
from: os("foo"),
to: os("bar"),
format: Format::Normal,
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: 42,
}),
parse_params(
[os("diff"), os("--tabsize=42"), os("foo"), os("bar")]
.iter()
.cloned()
)
);
assert!(parse_params(
[os("diff"), os("--tabsize"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize="), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=-1"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[os("diff"), os("--tabsize=r2"), os("foo"), os("bar")]
.iter()
.cloned()
)
.is_err());
assert!(parse_params(
[
os("diff"),
os("--tabsize=92233720368547758088"),
os("foo"),
os("bar")
]
.iter()
.cloned()
)
.is_err());
}
#[test]
fn double_dash() {
assert_eq!(
Ok(Params {
@@ -342,6 +523,8 @@ mod tests {
context_count: 3,
report_identical_files: false,
brief: false,
expand_tabs: false,
tabsize: DEFAULT_TABSIZE,
}),
parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned())
);
+29 -3
View File
@@ -6,6 +6,8 @@
use std::collections::VecDeque;
use std::io::Write;
use crate::utils::do_write_line;
#[derive(Debug, PartialEq)]
pub enum DiffLine {
Context(Vec<u8>),
@@ -234,6 +236,7 @@ fn make_diff(
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn diff(
expected: &[u8],
expected_filename: &str,
@@ -241,6 +244,8 @@ pub fn diff(
actual_filename: &str,
context_size: usize,
stop_early: bool,
expand_tabs: bool,
tabsize: usize,
) -> Vec<u8> {
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);
@@ -371,17 +376,20 @@ pub fn diff(
match line {
DiffLine::Expected(e) => {
write!(output, "-").expect("write to Vec is infallible");
output.write_all(&e).expect("write to Vec is infallible");
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");
output.write_all(&c).expect("write to Vec is infallible");
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");
output.write_all(&r).expect("write to Vec is infallible");
do_write_line(&mut output, &r, expand_tabs, tabsize)
.expect("write to Vec is infallible");
writeln!(output).unwrap();
}
DiffLine::MissingNL => {
@@ -454,6 +462,8 @@ mod tests {
&format!("{target}/alef"),
2,
false,
false,
8,
);
File::create(&format!("{target}/ab.diff"))
.unwrap()
@@ -568,6 +578,8 @@ mod tests {
&format!("{target}/alefn"),
2,
false,
false,
8,
);
File::create(&format!("{target}/abn.diff"))
.unwrap()
@@ -662,6 +674,8 @@ mod tests {
&format!("{target}/alef_"),
2,
false,
false,
8,
);
File::create(&format!("{target}/ab_.diff"))
.unwrap()
@@ -741,6 +755,8 @@ mod tests {
&format!("{target}/alefx"),
2,
false,
false,
8,
);
File::create(&format!("{target}/abx.diff"))
.unwrap()
@@ -825,6 +841,8 @@ mod tests {
&format!("{target}/alefr"),
2,
false,
false,
8,
);
File::create(&format!("{target}/abr.diff"))
.unwrap()
@@ -869,6 +887,8 @@ mod tests {
to_filename,
context_size,
false,
false,
8,
);
let expected_full = [
"--- foo\t",
@@ -890,6 +910,8 @@ mod tests {
to_filename,
context_size,
true,
false,
8,
);
let expected_brief = ["--- foo\t", "+++ bar\t", ""].join("\n");
assert_eq!(diff_brief, expected_brief.as_bytes());
@@ -901,6 +923,8 @@ mod tests {
to_filename,
context_size,
false,
false,
8,
);
assert!(nodiff_full.is_empty());
@@ -911,6 +935,8 @@ mod tests {
to_filename,
context_size,
true,
false,
8,
);
assert!(nodiff_brief.is_empty());
}
+117
View File
@@ -0,0 +1,117 @@
// This file is part of the uutils diffutils package.
//
// For the full copyright and license information, please view the LICENSE-*
// files that was distributed with this source code.
use std::io::Write;
use unicode_width::UnicodeWidthStr;
/// Replace tabs by spaces in the input line.
/// Correctly handle multi-bytes characters.
/// This assumes that line does not contain any line breaks (if it does, the result is undefined).
pub fn do_expand_tabs(line: &[u8], tabsize: usize) -> Vec<u8> {
let tab = b'\t';
let ntabs = line.iter().filter(|c| **c == tab).count();
if ntabs == 0 {
return line.to_vec();
}
let mut result = Vec::with_capacity(line.len() + ntabs * (tabsize - 1));
let mut offset = 0;
let mut iter = line.split(|c| *c == tab).peekable();
while let Some(chunk) = iter.next() {
match String::from_utf8(chunk.to_vec()) {
Ok(s) => offset += UnicodeWidthStr::width(s.as_str()),
Err(_) => offset += chunk.len(),
}
result.extend_from_slice(chunk);
if iter.peek().is_some() {
result.resize(result.len() + tabsize - offset % tabsize, b' ');
offset = 0;
}
}
result
}
/// 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<u8>,
line: &[u8],
expand_tabs: bool,
tabsize: usize,
) -> std::io::Result<()> {
if expand_tabs {
output.write_all(do_expand_tabs(line, tabsize).as_slice())
} else {
output.write_all(line)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod expand_tabs {
use super::*;
use pretty_assertions::assert_eq;
fn assert_tab_expansion(line: &str, tabsize: usize, expected: &str) {
assert_eq!(
do_expand_tabs(line.as_bytes(), tabsize),
expected.as_bytes()
);
}
#[test]
fn basics() {
assert_tab_expansion("foo barr baz", 8, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 8, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 5, "foo barr baz");
assert_tab_expansion("foo\tbarr\tbaz", 2, "foo barr baz");
}
#[test]
fn multibyte_chars() {
assert_tab_expansion("foo\tépée\tbaz", 8, "foo épée baz");
assert_tab_expansion("foo\t😉\tbaz", 5, "foo 😉 baz");
// Note: The Woman Scientist emoji (👩‍🔬) is a ZWJ sequence combining
// the Woman emoji (👩) and the Microscope emoji (🔬). On supported platforms
// it is displayed as a single emoji and should have a print size of 2 columns,
// but terminal emulators tend to not support this, and display the two emojis
// side by side, thus accounting for a print size of 4 columns.
assert_tab_expansion("foo\t👩‍🔬\tbaz", 6, "foo 👩‍🔬 baz");
}
#[test]
fn invalid_utf8() {
// [240, 240, 152, 137] is an invalid UTF-8 sequence, so it is handled as 4 bytes
assert_eq!(
do_expand_tabs(&[240, 240, 152, 137, 9, 102, 111, 111], 8),
&[240, 240, 152, 137, 32, 32, 32, 32, 102, 111, 111]
);
}
}
mod write_line {
use super::*;
use pretty_assertions::assert_eq;
fn assert_line_written(line: &str, expand_tabs: bool, tabsize: usize, expected: &str) {
let mut output: Vec<u8> = Vec::new();
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, 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");
}
}
}