Skip to content

Commit a7499b5

Browse files
committed
diff: compare DIR/FILE with FILE when stdin is a directory
1 parent e2274fe commit a7499b5

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

‎src/diff.rs‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@ use std::process::{exit, ExitCode};
1919
// 1 means some differences were found,
2020
// and 2 means trouble.
2121
pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
22-
let params = parse_params(opts).unwrap_or_else(|error| {
22+
let mut params = parse_params(opts).unwrap_or_else(|error| {
2323
eprintln!("{error}");
2424
exit(2);
2525
});
26+
// diff - FILE with a directory on stdin => diff DIRECTORY/FILE FILE
27+
if params.from == "-" && params.to != "-" {
28+
if let Some(from) = stdin_directory_entry(&params.to) {
29+
params.from = from;
30+
}
31+
}
2632
// if from and to are the same file, no need to perform any comparison
2733
let maybe_report_identical_files = || {
2834
if params.report_identical_files {
@@ -136,3 +142,34 @@ const BINARY_PROBE_LEN: usize = 16 * 1024;
136142
fn is_binary(content: &[u8]) -> bool {
137143
content[..content.len().min(BINARY_PROBE_LEN)].contains(&0)
138144
}
145+
146+
/// When standard input is a directory, returns the path of the entry in it
147+
/// that has the same name as `other`, provided `other` is not a directory.
148+
/// Entries are reached through `/dev/fd/0`, which only names the directory
149+
/// itself where it links to the open file, as with Linux procfs.
150+
#[cfg(any(target_os = "linux", target_os = "android"))]
151+
fn stdin_directory_entry(other: &OsString) -> Option<OsString> {
152+
use std::os::fd::AsFd;
153+
use std::path::Path;
154+
155+
let stdin_is_dir = io::stdin()
156+
.as_fd()
157+
.try_clone_to_owned()
158+
.ok()
159+
.and_then(|fd| fs::File::from(fd).metadata().ok())
160+
.is_some_and(|metadata| metadata.is_dir());
161+
let other = Path::new(other);
162+
if !stdin_is_dir || other.is_dir() {
163+
return None;
164+
}
165+
Some(
166+
Path::new("/dev/fd/0")
167+
.join(other.file_name()?)
168+
.into_os_string(),
169+
)
170+
}
171+
172+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
173+
fn stdin_directory_entry(_other: &OsString) -> Option<OsString> {
174+
None
175+
}

‎tests/by-util/test_diff.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
use diffutilslib::assert_diff_eq;
77

8+
#[cfg(any(target_os = "linux", target_os = "android"))]
9+
use assert_cmd::assert::OutputAssertExt;
810
use assert_cmd::cargo::cargo_bin_cmd;
911
use predicates::prelude::*;
1012
use std::fs::File;
@@ -280,6 +282,31 @@ fn binary_files() -> Result<(), Box<dyn std::error::Error>> {
280282
Ok(())
281283
}
282284

285+
#[cfg(any(target_os = "linux", target_os = "android"))]
286+
#[test]
287+
fn stdin_is_directory() -> Result<(), Box<dyn std::error::Error>> {
288+
let tmp_dir = tempdir()?;
289+
let dir = tmp_dir.path().join("dir");
290+
std::fs::create_dir(&dir)?;
291+
std::fs::write(dir.join("notes"), "alpha\n")?;
292+
let notes = tmp_dir.path().join("notes");
293+
std::fs::write(&notes, "beta\n")?;
294+
295+
// The directory itself must be standard input, not its contents.
296+
let output = std::process::Command::new(assert_cmd::cargo::cargo_bin!("diffutils"))
297+
.arg("diff")
298+
.arg("-")
299+
.arg(&notes)
300+
.stdin(File::open(&dir)?)
301+
.output()?;
302+
output
303+
.assert()
304+
.code(predicate::eq(1))
305+
.failure()
306+
.stdout("1c1\n< alpha\n---\n> beta\n");
307+
Ok(())
308+
}
309+
283310
#[test]
284311
fn compare_file_to_directory() -> Result<(), Box<dyn std::error::Error>> {
285312
let tmp_dir = tempdir()?;

0 commit comments

Comments
 (0)