diff --git a/src/diff.rs b/src/diff.rs new file mode 100644 index 0000000..6998e2b --- /dev/null +++ b/src/diff.rs @@ -0,0 +1,98 @@ +// 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 crate::params::{parse_params, Format}; +use crate::utils::report_failure_to_read_input_file; +use crate::{context_diff, ed_diff, normal_diff, unified_diff}; +use std::env::ArgsOs; +use std::ffi::OsString; +use std::fs; +use std::io::{self, Read, Write}; +use std::iter::Peekable; +use std::process::{exit, ExitCode}; + +// Exit codes are documented at +// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. +// An exit status of 0 means no differences were found, +// 1 means some differences were found, +// and 2 means trouble. +pub(crate) fn main(opts: Peekable) -> ExitCode { + let params = parse_params(opts).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }); + // if from and to are the same file, no need to perform any comparison + let maybe_report_identical_files = || { + if params.report_identical_files { + println!( + "Files {} and {} are identical", + params.from.to_string_lossy(), + params.to.to_string_lossy(), + ); + } + }; + if params.from == "-" && params.to == "-" + || same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) + { + maybe_report_identical_files(); + return ExitCode::SUCCESS; + } + + // read files + fn read_file_contents(filepath: &OsString) -> io::Result> { + if filepath == "-" { + let mut content = Vec::new(); + io::stdin().read_to_end(&mut content).and(Ok(content)) + } else { + fs::read(filepath) + } + } + let mut io_error = false; + let from_content = match read_file_contents(¶ms.from) { + Ok(from_content) => from_content, + Err(e) => { + report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e); + io_error = true; + vec![] + } + }; + let to_content = match read_file_contents(¶ms.to) { + Ok(to_content) => to_content, + Err(e) => { + report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e); + io_error = true; + vec![] + } + }; + if io_error { + return ExitCode::from(2); + } + + // run diff + let result: Vec = match params.format { + Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), + Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms), + Format::Context => context_diff::diff(&from_content, &to_content, ¶ms), + Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| { + eprintln!("{error}"); + exit(2); + }), + }; + if params.brief && !result.is_empty() { + println!( + "Files {} and {} differ", + params.from.to_string_lossy(), + params.to.to_string_lossy() + ); + } else { + io::stdout().write_all(&result).unwrap(); + } + if result.is_empty() { + maybe_report_identical_files(); + ExitCode::SUCCESS + } else { + ExitCode::from(1) + } +} diff --git a/src/main.rs b/src/main.rs index 7e221ea..824b45c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,15 +3,16 @@ // 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 regex::Regex; -use std::env; -use std::ffi::OsString; -use std::fs; -use std::io::{self, Read, Write}; -use std::process::{exit, ExitCode}; +use std::{ + env::ArgsOs, + ffi::OsString, + iter::Peekable, + path::{Path, PathBuf}, + process::ExitCode, +}; mod context_diff; +mod diff; mod ed_diff; mod macros; mod normal_diff; @@ -19,103 +20,60 @@ mod params; mod unified_diff; mod utils; -fn report_failure_to_read_input_file( - executable: &OsString, - filepath: &OsString, - error: &std::io::Error, -) { - // std::io::Error's display trait outputs "{detail} (os error {code})" - // but we want only the {detail} (error string) part - let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap(); - eprintln!( - "{}: {}: {}", - executable.to_string_lossy(), - filepath.to_string_lossy(), - error_code_re.replace(error.to_string().as_str(), ""), - ); +/// # Panics +/// Panics if the binary path cannot be determined +fn binary_path(args: &mut Peekable) -> PathBuf { + match args.peek() { + Some(ref s) if !s.is_empty() => PathBuf::from(s), + _ => std::env::current_exe().unwrap(), + } +} + +fn name(binary_path: &Path) -> Option<&str> { + binary_path.file_stem()?.to_str() +} + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn usage(name: &str) { + println!("{name} {VERSION} (multi-call binary)\n"); + println!("Usage: {name} [function [arguments...]]\n"); + println!("Currently defined functions:\n"); + println!(" diff\n"); +} + +fn second_arg_error(name: &str) -> ! { + println!("Expected utility name as second argument, got nothing."); + usage(name); + std::process::exit(0); } -// Exit codes are documented at -// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff.html. -// An exit status of 0 means no differences were found, -// 1 means some differences were found, -// and 2 means trouble. fn main() -> ExitCode { - let opts = env::args_os(); - let params = parse_params(opts).unwrap_or_else(|error| { - eprintln!("{error}"); - exit(2); + let mut args = std::env::args_os().peekable(); + + let exe_path = binary_path(&mut args); + let exe_name = name(&exe_path).unwrap_or_else(|| { + usage(""); + std::process::exit(1); }); - // if from and to are the same file, no need to perform any comparison - let maybe_report_identical_files = || { - if params.report_identical_files { - println!( - "Files {} and {} are identical", - params.from.to_string_lossy(), - params.to.to_string_lossy(), - ); - } - }; - if params.from == "-" && params.to == "-" - || same_file::is_same_file(¶ms.from, ¶ms.to).unwrap_or(false) - { - maybe_report_identical_files(); - return ExitCode::SUCCESS; - } - // read files - fn read_file_contents(filepath: &OsString) -> io::Result> { - if filepath == "-" { - let mut content = Vec::new(); - io::stdin().read_to_end(&mut content).and(Ok(content)) - } else { - fs::read(filepath) - } - } - let mut io_error = false; - let from_content = match read_file_contents(¶ms.from) { - Ok(from_content) => from_content, - Err(e) => { - report_failure_to_read_input_file(¶ms.executable, ¶ms.from, &e); - io_error = true; - vec![] - } - }; - let to_content = match read_file_contents(¶ms.to) { - Ok(to_content) => to_content, - Err(e) => { - report_failure_to_read_input_file(¶ms.executable, ¶ms.to, &e); - io_error = true; - vec![] - } - }; - if io_error { - return ExitCode::from(2); - } + let util_name = if exe_name == "diffutils" { + // Discard the item we peeked. + let _ = args.next(); - // run diff - let result: Vec = match params.format { - Format::Normal => normal_diff::diff(&from_content, &to_content, ¶ms), - Format::Unified => unified_diff::diff(&from_content, &to_content, ¶ms), - Format::Context => context_diff::diff(&from_content, &to_content, ¶ms), - Format::Ed => ed_diff::diff(&from_content, &to_content, ¶ms).unwrap_or_else(|error| { - eprintln!("{error}"); - exit(2); - }), + args.peek() + .cloned() + .unwrap_or_else(|| second_arg_error(exe_name)) + } else { + OsString::from(exe_name) }; - if params.brief && !result.is_empty() { - println!( - "Files {} and {} differ", - params.from.to_string_lossy(), - params.to.to_string_lossy() - ); - } else { - io::stdout().write_all(&result).unwrap(); - } - if result.is_empty() { - maybe_report_identical_files(); - ExitCode::SUCCESS - } else { - ExitCode::from(1) + + match util_name.to_str() { + Some("diff") => diff::main(args), + Some(name) => { + usage(&format!("{}: utility not supported", name)); + ExitCode::from(1) + } + None => second_arg_error(exe_name), } } diff --git a/src/params.rs b/src/params.rs index c671180..9b3abc4 100644 --- a/src/params.rs +++ b/src/params.rs @@ -1,4 +1,5 @@ use std::ffi::OsString; +use std::iter::Peekable; use std::path::PathBuf; use regex::Regex; @@ -41,8 +42,7 @@ impl Default for Params { } } -pub fn parse_params>(opts: I) -> Result { - let mut opts = opts.into_iter().peekable(); +pub fn parse_params>(mut opts: Peekable) -> Result { // parse CLI let Some(executable) = opts.next() else { @@ -323,7 +323,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -336,6 +341,7 @@ mod tests { [os("diff"), os("--normal"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); } @@ -350,7 +356,12 @@ mod tests { format: Format::Ed, ..Default::default() }), - parse_params([os("diff"), os(arg), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os(arg), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); } } @@ -368,7 +379,7 @@ mod tests { format: Format::Context, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } for args in [ @@ -390,7 +401,7 @@ mod tests { context_count: 42, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } } @@ -410,7 +421,7 @@ mod tests { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); - assert!(parse_params(params.iter().map(|x| os(x))).is_err()); + assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err()); } } #[test] @@ -427,7 +438,7 @@ mod tests { format: Format::Unified, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } for args in [ @@ -449,7 +460,7 @@ mod tests { context_count: 42, ..Default::default() }), - parse_params(params.iter().map(|x| os(x))) + parse_params(params.iter().map(|x| os(x)).peekable()) ); } } @@ -469,7 +480,7 @@ mod tests { let mut params = vec!["diff"]; params.extend(args); params.extend(["foo", "bar"]); - assert!(parse_params(params.iter().map(|x| os(x))).is_err()); + assert!(parse_params(params.iter().map(|x| os(x)).peekable()).is_err()); } } #[test] @@ -487,6 +498,7 @@ mod tests { [os("diff"), os("-u54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -502,6 +514,7 @@ mod tests { [os("diff"), os("-U54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -517,6 +530,7 @@ mod tests { [os("diff"), os("-U"), os("54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -532,6 +546,7 @@ mod tests { [os("diff"), os("-c54"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); } @@ -544,7 +559,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -554,7 +574,12 @@ mod tests { report_identical_files: true, ..Default::default() }), - parse_params([os("diff"), os("-s"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("-s"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -573,6 +598,7 @@ mod tests { ] .iter() .cloned() + .peekable() ) ); } @@ -585,7 +611,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -595,7 +626,12 @@ mod tests { brief: true, ..Default::default() }), - parse_params([os("diff"), os("-q"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("-q"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -609,6 +645,7 @@ mod tests { [os("diff"), os("--brief"), os("foo"), os("bar"),] .iter() .cloned() + .peekable() ) ); } @@ -621,7 +658,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); for option in ["-t", "--expand-tabs"] { assert_eq!( @@ -636,6 +678,7 @@ mod tests { [os("diff"), os(option), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); } @@ -649,7 +692,12 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("bar")].iter().cloned()) + parse_params( + [os("diff"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) ); assert_eq!( Ok(Params { @@ -663,6 +711,7 @@ mod tests { [os("diff"), os("--tabsize=0"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert_eq!( @@ -677,36 +726,42 @@ mod tests { [os("diff"), os("--tabsize=42"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) ); assert!(parse_params( [os("diff"), os("--tabsize"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize="), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=-1"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( [os("diff"), os("--tabsize=r2"), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); assert!(parse_params( @@ -718,6 +773,7 @@ mod tests { ] .iter() .cloned() + .peekable() ) .is_err()); } @@ -730,7 +786,12 @@ mod tests { to: os("-h"), ..Default::default() }), - parse_params([os("diff"), os("--"), os("-g"), os("-h")].iter().cloned()) + parse_params( + [os("diff"), os("--"), os("-g"), os("-h")] + .iter() + .cloned() + .peekable() + ) ); } #[test] @@ -742,7 +803,7 @@ mod tests { to: os("-"), ..Default::default() }), - parse_params([os("diff"), os("foo"), os("-")].iter().cloned()) + parse_params([os("diff"), os("foo"), os("-")].iter().cloned().peekable()) ); assert_eq!( Ok(Params { @@ -751,7 +812,7 @@ mod tests { to: os("bar"), ..Default::default() }), - parse_params([os("diff"), os("-"), os("bar")].iter().cloned()) + parse_params([os("diff"), os("-"), os("bar")].iter().cloned().peekable()) ); assert_eq!( Ok(Params { @@ -760,27 +821,45 @@ mod tests { to: os("-"), ..Default::default() }), - parse_params([os("diff"), os("-"), os("-")].iter().cloned()) + parse_params([os("diff"), os("-"), os("-")].iter().cloned().peekable()) ); - assert!(parse_params([os("diff"), os("foo"), os("bar"), os("-")].iter().cloned()).is_err()); - assert!(parse_params([os("diff"), os("-"), os("-"), os("-")].iter().cloned()).is_err()); + assert!(parse_params( + [os("diff"), os("foo"), os("bar"), os("-")] + .iter() + .cloned() + .peekable() + ) + .is_err()); + assert!(parse_params( + [os("diff"), os("-"), os("-"), os("-")] + .iter() + .cloned() + .peekable() + ) + .is_err()); } #[test] fn missing_arguments() { - assert!(parse_params([os("diff")].iter().cloned()).is_err()); - assert!(parse_params([os("diff"), os("foo")].iter().cloned()).is_err()); + assert!(parse_params([os("diff")].iter().cloned().peekable()).is_err()); + assert!(parse_params([os("diff"), os("foo")].iter().cloned().peekable()).is_err()); } #[test] fn unknown_argument() { + assert!(parse_params( + [os("diff"), os("-g"), os("foo"), os("bar")] + .iter() + .cloned() + .peekable() + ) + .is_err()); assert!( - parse_params([os("diff"), os("-g"), os("foo"), os("bar")].iter().cloned()).is_err() + parse_params([os("diff"), os("-g"), os("bar")].iter().cloned().peekable()).is_err() ); - assert!(parse_params([os("diff"), os("-g"), os("bar")].iter().cloned()).is_err()); - assert!(parse_params([os("diff"), os("-g")].iter().cloned()).is_err()); + assert!(parse_params([os("diff"), os("-g")].iter().cloned().peekable()).is_err()); } #[test] fn empty() { - assert!(parse_params([].iter().cloned()).is_err()); + assert!(parse_params([].iter().cloned().peekable()).is_err()); } #[test] fn conflicting_output_styles() { @@ -797,6 +876,7 @@ mod tests { [os("diff"), os(arg1), os(arg2), os("foo"), os("bar")] .iter() .cloned() + .peekable() ) .is_err()); } diff --git a/src/utils.rs b/src/utils.rs index df1390d..a216784 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,8 +3,9 @@ // For the full copyright and license information, please view the LICENSE-* // files that was distributed with this source code. -use std::io::Write; +use std::{ffi::OsString, io::Write}; +use regex::Regex; use unicode_width::UnicodeWidthStr; /// Replace tabs by spaces in the input line. @@ -71,6 +72,22 @@ pub fn get_modification_time(file_path: &str) -> String { modification_time } +pub fn report_failure_to_read_input_file( + executable: &OsString, + filepath: &OsString, + error: &std::io::Error, +) { + // std::io::Error's display trait outputs "{detail} (os error {code})" + // but we want only the {detail} (error string) part + let error_code_re = Regex::new(r"\ \(os\ error\ \d+\)$").unwrap(); + eprintln!( + "{}: {}: {}", + executable.to_string_lossy(), + filepath.to_string_lossy(), + error_code_re.replace(error.to_string().as_str(), ""), + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/integration.rs b/tests/integration.rs index f8ad515..2b3fd4f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -15,6 +15,7 @@ use tempfile::{tempdir, NamedTempFile}; #[test] fn unknown_param() -> Result<(), Box> { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("--foobar"); cmd.assert() .code(predicate::eq(2)) @@ -37,6 +38,7 @@ fn cannot_read_files() -> Result<(), Box> { let error_message = "The system cannot find the file specified."; let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg(&nopath).arg(file.path()); cmd.assert() .code(predicate::eq(2)) @@ -47,6 +49,7 @@ fn cannot_read_files() -> Result<(), Box> { ))); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg(file.path()).arg(&nopath); cmd.assert() .code(predicate::eq(2)) @@ -57,6 +60,7 @@ fn cannot_read_files() -> Result<(), Box> { ))); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg(&nopath).arg(&nopath); cmd.assert().code(predicate::eq(2)).failure().stderr( predicate::str::contains(format!( @@ -74,6 +78,7 @@ fn no_differences() -> Result<(), Box> { let file = NamedTempFile::new()?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); } @@ -93,6 +98,7 @@ fn no_differences_report_identical_files() -> Result<(), Box Result<(), Box Result<(), Box> { file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); } @@ -155,6 +163,7 @@ fn differences_brief() -> Result<(), Box> { file2.write_all("bar\n".as_bytes())?; for option in ["", "-u", "-c", "-e"] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); if !option.is_empty() { cmd.arg(option); } @@ -178,6 +187,7 @@ fn missing_newline() -> Result<(), Box> { let mut file2 = NamedTempFile::new()?; file2.write_all("bar".as_bytes())?; let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-e").arg(file1.path()).arg(file2.path()); cmd.assert() .code(predicate::eq(2)) @@ -194,6 +204,7 @@ fn read_from_stdin() -> Result<(), Box> { file2.write_all("bar\n".as_bytes())?; let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u") .arg(file1.path()) .arg("-") @@ -210,6 +221,7 @@ fn read_from_stdin() -> Result<(), Box> { ); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u") .arg("-") .arg(file2.path()) @@ -226,6 +238,7 @@ fn read_from_stdin() -> Result<(), Box> { ); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u").arg("-").arg("-"); cmd.assert() .code(predicate::eq(0)) @@ -235,6 +248,7 @@ fn read_from_stdin() -> Result<(), Box> { #[cfg(unix)] { let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u") .arg(file1.path()) .arg("/dev/stdin") @@ -270,6 +284,7 @@ fn compare_file_to_directory() -> Result<(), Box> { da.write_all(b"da\n").unwrap(); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u").arg(&directory).arg(&a_path); cmd.assert().code(predicate::eq(1)).failure(); @@ -284,6 +299,7 @@ fn compare_file_to_directory() -> Result<(), Box> { ); let mut cmd = Command::cargo_bin("diffutils")?; + cmd.arg("diff"); cmd.arg("-u").arg(&a_path).arg(&directory); cmd.assert().code(predicate::eq(1)).failure();