1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

tail: verify that -[nc]0 without -f, exit without reading

This passes: "gnu/tests/tail-2/tail-n0f.sh"

* add tests for "-[nc]0 wo -f"

* add bubble-up UResult
* rename return_code -> exit_code
This commit is contained in:
Jan Scheer 2022-05-27 23:36:31 +02:00
parent 4bbf708c81
commit bb5dc8bd2f
No known key found for this signature in database
GPG key ID: C62AD4C29E2B9828
2 changed files with 106 additions and 28 deletions

View file

@ -96,7 +96,7 @@ pub mod options {
pub static PRESUME_INPUT_PIPE: &str = "-presume-input-pipe"; pub static PRESUME_INPUT_PIPE: &str = "-presume-input-pipe";
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
enum FilterMode { enum FilterMode {
Bytes(u64), Bytes(u64),
Lines(u64, u8), // (number of lines, delimiter) Lines(u64, u8), // (number of lines, delimiter)
@ -123,7 +123,7 @@ struct Settings {
paths: VecDeque<PathBuf>, paths: VecDeque<PathBuf>,
pid: platform::Pid, pid: platform::Pid,
retry: bool, retry: bool,
return_code: i32, exit_code: i32,
sleep_sec: Duration, sleep_sec: Duration,
use_polling: bool, use_polling: bool,
verbose: bool, verbose: bool,
@ -187,12 +187,15 @@ impl Settings {
} }
} }
let mut starts_with_plus = false;
let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) {
starts_with_plus = arg.starts_with("+");
match parse_num(arg) { match parse_num(arg) {
Ok((n, beginning)) => (FilterMode::Bytes(n), beginning), Ok((n, beginning)) => (FilterMode::Bytes(n), beginning),
Err(e) => return Err(format!("invalid number of bytes: {}", e)), Err(e) => return Err(format!("invalid number of bytes: {}", e)),
} }
} else if let Some(arg) = matches.value_of(options::LINES) { } else if let Some(arg) = matches.value_of(options::LINES) {
starts_with_plus = arg.starts_with("+");
match parse_num(arg) { match parse_num(arg) {
Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning), Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning),
Err(e) => return Err(format!("invalid number of lines: {}", e)), Err(e) => return Err(format!("invalid number of lines: {}", e)),
@ -203,6 +206,17 @@ impl Settings {
settings.mode = mode_and_beginning.0; settings.mode = mode_and_beginning.0;
settings.beginning = mode_and_beginning.1; settings.beginning = mode_and_beginning.1;
// Mimic GNU's tail for -[nc]0 without -f and exit immediately
if settings.follow.is_none() && !starts_with_plus && {
if let FilterMode::Lines(l, _) = settings.mode {
l == 0
} else {
settings.mode == FilterMode::Bytes(0)
}
} {
std::process::exit(0)
}
settings.use_polling = matches.is_present(options::USE_POLLING); settings.use_polling = matches.is_present(options::USE_POLLING);
settings.retry = settings.retry =
matches.is_present(options::RETRY) || matches.is_present(options::FOLLOW_RETRY); matches.is_present(options::RETRY) || matches.is_present(options::FOLLOW_RETRY);
@ -296,7 +310,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
if !path.exists() && !settings.stdin_is_pipe_or_fifo { if !path.exists() && !settings.stdin_is_pipe_or_fifo {
settings.return_code = 1; settings.exit_code = 1;
show_error!( show_error!(
"cannot open {} for reading: {}", "cannot open {} for reading: {}",
display_name.quote(), display_name.quote(),
@ -321,7 +335,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
// } // }
} }
settings.return_code = 1; settings.exit_code = 1;
show_error!("error reading {}: {}", display_name.quote(), err_msg); show_error!("error reading {}: {}", display_name.quote(), err_msg);
if settings.follow.is_some() { if settings.follow.is_some() {
let msg = if !settings.retry { let msg = if !settings.retry {
@ -368,7 +382,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
); );
} }
} else { } else {
settings.return_code = 1; settings.exit_code = 1;
show_error!( show_error!(
"cannot fstat {}: {}", "cannot fstat {}: {}",
text::STDIN_HEADER.quote(), text::STDIN_HEADER.quote(),
@ -401,7 +415,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
if settings.follow.is_some() { if settings.follow.is_some() {
// Insert existing/file `path` into `files.map`. // Insert existing/file `path` into `files.map`.
files.insert( files.insert(
path.canonicalize().unwrap(), path.canonicalize()?,
PathData { PathData {
reader: Some(Box::new(reader)), reader: Some(Box::new(reader)),
metadata: md, metadata: md,
@ -411,7 +425,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
} }
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
settings.return_code = 1; settings.exit_code = 1;
show_error!( show_error!(
"cannot open {} for reading: Permission denied", "cannot open {} for reading: Permission denied",
display_name.quote() display_name.quote()
@ -426,7 +440,7 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
} else if settings.retry && settings.follow.is_some() { } else if settings.retry && settings.follow.is_some() {
if path.is_relative() { if path.is_relative() {
path = std::env::current_dir().unwrap().join(&path); path = std::env::current_dir()?.join(&path);
} }
// Insert non-is_tailable() paths into `files.map`. // Insert non-is_tailable() paths into `files.map`.
files.insert( files.insert(
@ -459,12 +473,12 @@ fn uu_tail(mut settings: Settings) -> UResult<()> {
} }
} }
if settings.return_code > 0 { if settings.exit_code > 0 {
#[cfg(unix)] #[cfg(unix)]
if stdin_is_bad_fd() { if stdin_is_bad_fd() {
show_error!("-: {}", text::BAD_FD); show_error!("-: {}", text::BAD_FD);
} }
return Err(USimpleError::new(settings.return_code, "")); return Err(USimpleError::new(settings.exit_code, ""));
} }
Ok(()) Ok(())
@ -657,7 +671,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
"{} cannot be used, reverting to polling: Too many open files", "{} cannot be used, reverting to polling: Too many open files",
text::BACKEND text::BACKEND
); );
settings.return_code = 1; settings.exit_code = 1;
let config = notify::poll::PollWatcherConfig { let config = notify::poll::PollWatcherConfig {
poll_interval: settings.sleep_sec, poll_interval: settings.sleep_sec,
..Default::default() ..Default::default()
@ -680,7 +694,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
// 2x "inotify_add_watch" and 1x "inotify_rm_watch" // 2x "inotify_add_watch" and 1x "inotify_rm_watch"
let path = get_path(path, settings); let path = get_path(path, settings);
watcher watcher
.watch(&path.canonicalize().unwrap(), RecursiveMode::NonRecursive) .watch(&path.canonicalize()?, RecursiveMode::NonRecursive)
.unwrap(); .unwrap();
} else if settings.follow.is_some() && settings.retry { } else if settings.follow.is_some() && settings.retry {
if path.is_orphan() { if path.is_orphan() {
@ -688,7 +702,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
} else { } else {
let parent = path.parent().unwrap(); let parent = path.parent().unwrap();
watcher watcher
.watch(&parent.canonicalize().unwrap(), RecursiveMode::NonRecursive) .watch(&parent.canonicalize()?, RecursiveMode::NonRecursive)
.unwrap(); .unwrap();
} }
} else { } else {
@ -716,7 +730,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
show_error!("{} has appeared; following new file", display_name.quote()); show_error!("{} has appeared; following new file", display_name.quote());
if let Ok(new_path_canonical) = new_path.canonicalize() { if let Ok(new_path_canonical) = new_path.canonicalize() {
files.update_metadata(&new_path_canonical, None); files.update_metadata(&new_path_canonical, None);
files.update_reader(&new_path_canonical).unwrap(); files.update_reader(&new_path_canonical)?;
read_some = files.tail_file(&new_path_canonical, settings.verbose)?; read_some = files.tail_file(&new_path_canonical, settings.verbose)?;
let new_path = get_path(&new_path_canonical, settings); let new_path = get_path(&new_path_canonical, settings);
watcher watcher
@ -752,13 +766,13 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
// TODO: [2021-10; jhscheer] reduce duplicate code // TODO: [2021-10; jhscheer] reduce duplicate code
let display_name = files.map.get(path).unwrap().display_name.to_path_buf(); let display_name = files.map.get(path).unwrap().display_name.to_path_buf();
if new_md.len() <= old_md.len() if new_md.len() <= old_md.len()
&& new_md.modified().unwrap() != old_md.modified().unwrap() && new_md.modified()? != old_md.modified()?
&& new_md.is_file() && new_md.is_file()
&& old_md.is_file() && old_md.is_file()
{ {
show_error!("{}: file truncated", display_name.display()); show_error!("{}: file truncated", display_name.display());
files.update_metadata(path, None); files.update_metadata(path, None);
files.update_reader(path).unwrap(); files.update_reader(path)?;
} }
} }
} }
@ -778,7 +792,7 @@ fn follow(files: &mut FileHandling, settings: &mut Settings) -> UResult<()> {
// dbg!(files.map.keys()); // dbg!(files.map.keys());
// dbg!(&files.last); // dbg!(&files.last);
// eprintln!("=={:=>3}=====================dbg===", _event_counter); // eprintln!("=={:=>3}=====================dbg===", _event_counter);
handle_event(&event, files, settings, &mut watcher, &mut orphans); handle_event(&event, files, settings, &mut watcher, &mut orphans)?;
} }
Ok(Err(notify::Error { Ok(Err(notify::Error {
kind: notify::ErrorKind::Io(ref e), kind: notify::ErrorKind::Io(ref e),
@ -834,7 +848,7 @@ fn handle_event(
settings: &Settings, settings: &Settings,
watcher: &mut Box<dyn Watcher>, watcher: &mut Box<dyn Watcher>,
orphans: &mut Vec<PathBuf>, orphans: &mut Vec<PathBuf>,
) { ) -> UResult<()> {
use notify::event::*; use notify::event::*;
if let Some(event_path) = event.paths.first() { if let Some(event_path) = event.paths.first() {
@ -858,7 +872,7 @@ fn handle_event(
display_name.quote() display_name.quote()
); );
files.update_metadata(event_path, None); files.update_metadata(event_path, None);
files.update_reader(event_path).unwrap(); files.update_reader(event_path)?;
} else if !new_md.is_file() && old_md.is_file() { } else if !new_md.is_file() && old_md.is_file() {
show_error!( show_error!(
"{} has been replaced with an untailable file", "{} has been replaced with an untailable file",
@ -874,11 +888,11 @@ fn handle_event(
); );
files.update_metadata(event_path, None); files.update_metadata(event_path, None);
} else if new_md.len() <= old_md.len() } else if new_md.len() <= old_md.len()
&& new_md.modified().unwrap() != old_md.modified().unwrap() && new_md.modified()? != old_md.modified()?
{ {
show_error!("{}: file truncated", display_name.display()); show_error!("{}: file truncated", display_name.display());
files.update_metadata(event_path, None); files.update_metadata(event_path, None);
files.update_reader(event_path).unwrap(); files.update_reader(event_path)?;
} }
} }
} }
@ -903,7 +917,7 @@ fn handle_event(
// assuming it has been truncated to 0. This mimics GNU's `tail` // assuming it has been truncated to 0. This mimics GNU's `tail`
// behavior and is the usual truncation operation for log files. // behavior and is the usual truncation operation for log files.
// files.update_metadata(event_path, None); // files.update_metadata(event_path, None);
files.update_reader(event_path).unwrap(); files.update_reader(event_path)?;
if settings.follow == Some(FollowMode::Name) && settings.retry { if settings.follow == Some(FollowMode::Name) && settings.retry {
// TODO: [2021-10; jhscheer] add test for this // TODO: [2021-10; jhscheer] add test for this
// Path has appeared, it's not an orphan any more. // Path has appeared, it's not an orphan any more.
@ -996,10 +1010,10 @@ fn handle_event(
// after the "mv" tail should only follow "file_b". // after the "mv" tail should only follow "file_b".
if settings.follow == Some(FollowMode::Descriptor) { if settings.follow == Some(FollowMode::Descriptor) {
let new_path = event.paths.last().unwrap().canonicalize().unwrap(); let new_path = event.paths.last().unwrap().canonicalize()?;
// Open new file and seek to End: // Open new file and seek to End:
let mut file = File::open(&new_path).unwrap(); let mut file = File::open(&new_path)?;
file.seek(SeekFrom::End(0)).unwrap(); file.seek(SeekFrom::End(0))?;
// Add new reader and remove old reader: // Add new reader and remove old reader:
files.map.insert( files.map.insert(
new_path.to_owned(), new_path.to_owned(),
@ -1018,7 +1032,7 @@ fn handle_event(
let new_path = get_path(&new_path, settings); let new_path = get_path(&new_path, settings);
watcher watcher
.watch( .watch(
&new_path.canonicalize().unwrap(), &new_path.canonicalize()?,
RecursiveMode::NonRecursive, RecursiveMode::NonRecursive,
) )
.unwrap(); .unwrap();
@ -1028,6 +1042,7 @@ fn handle_event(
} }
} }
} }
Ok(())
} }
fn get_path(path: &Path, settings: &Settings) -> PathBuf { fn get_path(path: &Path, settings: &Settings) -> PathBuf {

View file

@ -97,6 +97,55 @@ fn test_stdin_redirect_file() {
assert!(buf_stderr.is_empty()); assert!(buf_stderr.is_empty());
} }
#[test]
fn test_nc_0_wo_follow() {
// verify that -[nc]0 without -f, exit without reading
let ts = TestScenario::new(util_name!());
ts.ucmd()
.set_stdin(Stdio::null())
.args(&["-n0", "missing"])
.run()
.no_stderr()
.no_stdout()
.succeeded();
ts.ucmd()
.set_stdin(Stdio::null())
.args(&["-c0", "missing"])
.run()
.no_stderr()
.no_stdout()
.succeeded();
}
#[test]
#[cfg(not(target_os = "windows"))]
#[cfg(feature = "chmod")]
fn test_nc_0_wo_follow2() {
// verify that -[nc]0 without -f, exit without reading
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("unreadable");
ts.ccmd("chmod").arg("0").arg("unreadable").succeeds();
ts.ucmd()
.set_stdin(Stdio::null())
.args(&["-n0", "unreadable"])
.run()
.no_stderr()
.no_stdout()
.succeeded();
ts.ucmd()
.set_stdin(Stdio::null())
.args(&["-c0", "unreadable"])
.run()
.no_stderr()
.no_stdout()
.succeeded();
}
#[test] #[test]
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
#[cfg(feature = "chmod")] #[cfg(feature = "chmod")]
@ -831,11 +880,18 @@ fn test_positive_bytes() {
#[test] #[test]
#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android #[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android
fn test_positive_zero_bytes() { fn test_positive_zero_bytes() {
new_ucmd!() let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["-c", "+0"]) .args(&["-c", "+0"])
.pipe_in("abcde") .pipe_in("abcde")
.succeeds() .succeeds()
.stdout_is("abcde"); .stdout_is("abcde");
ts.ucmd()
.args(&["-c", "0"])
.pipe_in("abcde")
.succeeds()
.no_stdout()
.no_stderr();
} }
/// Test for reading all but the first NUM lines: `tail -n +3`. /// Test for reading all but the first NUM lines: `tail -n +3`.
@ -917,11 +973,18 @@ fn test_obsolete_syntax_small_file() {
#[test] #[test]
#[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android #[cfg(all(unix, not(target_os = "android")))] // FIXME: fix this test for Android
fn test_positive_zero_lines() { fn test_positive_zero_lines() {
new_ucmd!() let ts = TestScenario::new(util_name!());
ts.ucmd()
.args(&["-n", "+0"]) .args(&["-n", "+0"])
.pipe_in("a\nb\nc\nd\ne\n") .pipe_in("a\nb\nc\nd\ne\n")
.succeeds() .succeeds()
.stdout_is("a\nb\nc\nd\ne\n"); .stdout_is("a\nb\nc\nd\ne\n");
ts.ucmd()
.args(&["-n", "0"])
.pipe_in("a\nb\nc\nd\ne\n")
.succeeds()
.no_stderr()
.no_stdout();
} }
#[test] #[test]