Take utility name as first parameter on diffutils

This is in preparation for adding the other diffutils commands, cmp,
diff3, sdiff.

We use a similar strategy to uutils/coreutils, with the single binary
acting as one of the supported tools if called through a symlink with
the appropriate name. When using the multi-tool binary directly, the
utility needds to be the first parameter.
This commit is contained in:
Gustavo Noronha Silva
2024-09-22 22:48:36 -03:00
parent 9103365691
commit 72c7802f06
5 changed files with 296 additions and 127 deletions
+98
View File
@@ -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<ArgsOs>) -> 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(&params.from, &params.to).unwrap_or(false)
{
maybe_report_identical_files();
return ExitCode::SUCCESS;
}
// read files
fn read_file_contents(filepath: &OsString) -> io::Result<Vec<u8>> {
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(&params.from) {
Ok(from_content) => from_content,
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.from, &e);
io_error = true;
vec![]
}
};
let to_content = match read_file_contents(&params.to) {
Ok(to_content) => to_content,
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.to, &e);
io_error = true;
vec![]
}
};
if io_error {
return ExitCode::from(2);
}
// run diff
let result: Vec<u8> = match params.format {
Format::Normal => normal_diff::diff(&from_content, &to_content, &params),
Format::Unified => unified_diff::diff(&from_content, &to_content, &params),
Format::Context => context_diff::diff(&from_content, &to_content, &params),
Format::Ed => ed_diff::diff(&from_content, &to_content, &params).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)
}
}
+56 -98
View File
@@ -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<ArgsOs>) -> 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("<unknown binary>");
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(&params.from, &params.to).unwrap_or(false)
{
maybe_report_identical_files();
return ExitCode::SUCCESS;
}
// read files
fn read_file_contents(filepath: &OsString) -> io::Result<Vec<u8>> {
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(&params.from) {
Ok(from_content) => from_content,
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.from, &e);
io_error = true;
vec![]
}
};
let to_content = match read_file_contents(&params.to) {
Ok(to_content) => to_content,
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.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<u8> = match params.format {
Format::Normal => normal_diff::diff(&from_content, &to_content, &params),
Format::Unified => unified_diff::diff(&from_content, &to_content, &params),
Format::Context => context_diff::diff(&from_content, &to_content, &params),
Format::Ed => ed_diff::diff(&from_content, &to_content, &params).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),
}
}
+108 -28
View File
@@ -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<I: IntoIterator<Item = OsString>>(opts: I) -> Result<Params, String> {
let mut opts = opts.into_iter().peekable();
pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, String> {
// 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());
}
+18 -1
View File
@@ -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::*;