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:
parent
4bbf708c81
commit
bb5dc8bd2f
2 changed files with 106 additions and 28 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue