diff --git a/Cargo.lock b/Cargo.lock index 3904277da..49fd7d4fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2660,6 +2660,7 @@ version = "0.0.16" dependencies = [ "clap", "fs_extra", + "indicatif", "uucore", ] @@ -2984,6 +2985,7 @@ dependencies = [ name = "uu_tail" version = "0.0.16" dependencies = [ + "atty", "clap", "libc", "memchr", diff --git a/build.rs b/build.rs index 056380f28..5201b9169 100644 --- a/build.rs +++ b/build.rs @@ -6,10 +6,8 @@ use std::io::Write; use std::path::Path; pub fn main() { - // println!("cargo:warning=Running build.rs"); - if let Ok(profile) = env::var("PROFILE") { - println!("cargo:rustc-cfg=build={:?}", profile); + println!("cargo:rustc-cfg=build={profile:?}"); } const ENV_FEATURE_PREFIX: &str = "CARGO_FEATURE_"; @@ -17,7 +15,6 @@ pub fn main() { const OVERRIDE_PREFIX: &str = "uu_"; let out_dir = env::var("OUT_DIR").unwrap(); - // println!("cargo:warning=out_dir={}", out_dir); let mut crates = Vec::new(); for (key, val) in env::vars() { @@ -48,7 +45,7 @@ pub fn main() { let mut phf_map = phf_codegen::Map::<&str>::new(); for krate in &crates { - let map_value = format!("({krate}::uumain, {krate}::uu_app)", krate = krate); + let map_value = format!("({krate}::uumain, {krate}::uu_app)"); match krate.as_ref() { // 'test' is named uu_test to avoid collision with rust core crate 'test'. // It can also be invoked by name '[' for the '[ expr ] syntax'. @@ -60,22 +57,14 @@ pub fn main() { phf_map.entry(&k[OVERRIDE_PREFIX.len()..], &map_value); } "false" | "true" => { - phf_map.entry( - krate, - &format!("(r#{krate}::uumain, r#{krate}::uu_app)", krate = krate), - ); + phf_map.entry(krate, &format!("(r#{krate}::uumain, r#{krate}::uu_app)")); } "hashsum" => { - phf_map.entry( - krate, - &format!("({krate}::uumain, {krate}::uu_app_custom)", krate = krate), - ); + phf_map.entry(krate, &format!("({krate}::uumain, {krate}::uu_app_custom)")); - let map_value = format!("({krate}::uumain, {krate}::uu_app_common)", krate = krate); - let map_value_bits = - format!("({krate}::uumain, {krate}::uu_app_bits)", krate = krate); - let map_value_b3sum = - format!("({krate}::uumain, {krate}::uu_app_b3sum)", krate = krate); + let map_value = format!("({krate}::uumain, {krate}::uu_app_common)"); + let map_value_bits = format!("({krate}::uumain, {krate}::uu_app_bits)"); + let map_value_b3sum = format!("({krate}::uumain, {krate}::uu_app_b3sum)"); phf_map.entry("md5sum", &map_value); phf_map.entry("sha1sum", &map_value); phf_map.entry("sha224sum", &map_value); diff --git a/docs/src/extensions.md b/docs/src/extensions.md index 275690a33..56f2edcc9 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -9,6 +9,10 @@ extensions. `cp` can display a progress bar when the `-g`/`--progress` flag is set. +## `mv` + +`mv` can display a progress bar when the `-g`/`--progress` flag is set. + ## `hashsum` This utility does not exist in GNU coreutils. `hashsum` is a utility that diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 74c6f4cdb..08c0774ba 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -20,8 +20,8 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); fn usage(utils: &UtilityMap, name: &str) { - println!("{} {} (multi-call binary)\n", name, VERSION); - println!("Usage: {} [function [arguments...]]\n", name); + println!("{name} {VERSION} (multi-call binary)\n"); + println!("Usage: {name} [function [arguments...]]\n"); println!("Currently defined functions:\n"); #[allow(clippy::map_clone)] let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect(); @@ -153,7 +153,7 @@ fn gen_completions( .get_matches_from(std::iter::once(OsString::from("completion")).chain(args)); let utility = matches.get_one::("utility").unwrap(); - let shell = matches.get_one::("shell").unwrap().to_owned(); + let shell = *matches.get_one::("shell").unwrap(); let mut command = if utility == "coreutils" { gen_coreutils_app(util_map) diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 86beb9e90..54e0db395 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -287,7 +287,7 @@ where OutputFmt::Shell => result.push_str("LS_COLORS='"), OutputFmt::CShell => result.push_str("setenv LS_COLORS '"), OutputFmt::Display => (), - _ => unreachable!(), + OutputFmt::Unknown => unreachable!(), } let mut table: HashMap<&str, &str> = HashMap::with_capacity(48); @@ -405,7 +405,7 @@ where // remove latest "\n" result.pop(); } - _ => unreachable!(), + OutputFmt::Unknown => unreachable!(), } Ok(result) diff --git a/src/uu/hashsum/src/hashsum.rs b/src/uu/hashsum/src/hashsum.rs index 9e6fc1dc5..5e0973a16 100644 --- a/src/uu/hashsum/src/hashsum.rs +++ b/src/uu/hashsum/src/hashsum.rs @@ -634,7 +634,7 @@ where match bad_format.cmp(&1) { Ordering::Equal => show_warning!("{} line is improperly formatted", bad_format), Ordering::Greater => show_warning!("{} lines are improperly formatted", bad_format), - _ => {} + Ordering::Less => {} }; if failed_cksum > 0 { show_warning!("{} computed checksum did NOT match", failed_cksum); @@ -644,7 +644,7 @@ where Ordering::Greater => { show_warning!("{} listed files could not be read", failed_open_file); } - _ => {} + Ordering::Less => {} } } diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 23f010846..71caec101 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -17,6 +17,8 @@ path = "src/mv.rs" [dependencies] clap = { version = "4.0", features = ["wrap_help", "cargo"] } fs_extra = "1.1.0" +indicatif = "0.17" + uucore = { version=">=0.0.16", package="uucore", path="../../uucore" } [[bin]] diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index bcefc39a6..3da9a09ab 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -12,6 +12,7 @@ mod error; use clap::builder::ValueParser; use clap::{crate_version, error::ErrorKind, Arg, ArgAction, ArgMatches, Command}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use std::env; use std::ffi::OsString; use std::fs; @@ -24,9 +25,12 @@ use std::path::{Path, PathBuf}; use uucore::backup_control::{self, BackupMode}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError}; -use uucore::{format_usage, prompt_yes, show, show_if_err}; +use uucore::{format_usage, prompt_yes, show}; -use fs_extra::dir::{move_dir, CopyOptions as DirCopyOptions}; +use fs_extra::dir::{ + get_size as dir_get_size, move_dir, move_dir_with_progress, CopyOptions as DirCopyOptions, + TransitProcess, TransitProcessResult, +}; use crate::error::MvError; @@ -39,6 +43,7 @@ pub struct Behavior { no_target_dir: bool, verbose: bool, strip_slashes: bool, + progress_bar: bool, } #[derive(Clone, Eq, PartialEq)] @@ -63,7 +68,7 @@ static OPT_TARGET_DIRECTORY: &str = "target-directory"; static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory"; static OPT_UPDATE: &str = "update"; static OPT_VERBOSE: &str = "verbose"; - +static OPT_PROGRESS: &str = "progress"; static ARG_FILES: &str = "files"; #[uucore::main] @@ -122,6 +127,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { no_target_dir: matches.get_flag(OPT_NO_TARGET_DIRECTORY), verbose: matches.get_flag(OPT_VERBOSE), strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES), + progress_bar: matches.get_flag(OPT_PROGRESS), }; exec(&files[..], &behavior) @@ -197,6 +203,16 @@ pub fn uu_app() -> Command { .help("explain what is being done") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(OPT_PROGRESS) + .short('g') + .long(OPT_PROGRESS) + .help( + "Display a progress bar. \n\ + Note: this feature is not supported by GNU coreutils.", + ) + .action(ArgAction::SetTrue), + ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) @@ -271,7 +287,7 @@ fn exec(files: &[OsString], b: &Behavior) -> UResult<()> { if !source.is_dir() { Err(MvError::DirectoryToNonDirectory(target.quote().to_string()).into()) } else { - rename(source, target, b).map_err_context(|| { + rename(source, target, b, None).map_err_context(|| { format!("cannot move {} to {}", source.quote(), target.quote()) }) } @@ -294,7 +310,7 @@ fn exec(files: &[OsString], b: &Behavior) -> UResult<()> { ) .into()) } else { - rename(source, target, b).map_err(|e| USimpleError::new(1, format!("{}", e))) + rename(source, target, b, None).map_err(|e| USimpleError::new(1, format!("{}", e))) } } _ => { @@ -321,7 +337,27 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR .canonicalize() .unwrap_or_else(|_| target_dir.to_path_buf()); + let multi_progress = b.progress_bar.then(MultiProgress::new); + + let count_progress = if let Some(ref multi_progress) = multi_progress { + if files.len() > 1 { + Some(multi_progress.add( + ProgressBar::new(files.len().try_into().unwrap()).with_style( + ProgressStyle::with_template("moving {msg} {wide_bar} {pos}/{len}").unwrap(), + ), + )) + } else { + None + } + } else { + None + }; + for sourcepath in files.iter() { + if let Some(ref pb) = count_progress { + pb.set_message(sourcepath.to_string_lossy().to_string()); + } + let targetpath = match sourcepath.file_name() { Some(name) => target_dir.join(name), None => { @@ -352,18 +388,35 @@ fn move_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR } } - show_if_err!( - rename(sourcepath, &targetpath, b).map_err_context(|| format!( - "cannot move {} to {}", - sourcepath.quote(), - targetpath.quote() - )) - ); + let rename_result = rename(sourcepath, &targetpath, b, multi_progress.as_ref()) + .map_err_context(|| { + format!( + "cannot move {} to {}", + sourcepath.quote(), + targetpath.quote() + ) + }); + + if let Err(e) = rename_result { + match multi_progress { + Some(ref pb) => pb.suspend(|| show!(e)), + None => show!(e), + }; + }; + + if let Some(ref pb) = count_progress { + pb.inc(1); + } } Ok(()) } -fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> { +fn rename( + from: &Path, + to: &Path, + b: &Behavior, + multi_progress: Option<&MultiProgress>, +) -> io::Result<()> { let mut backup_path = None; if to.exists() { @@ -385,7 +438,7 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> { backup_path = backup_control::get_backup_path(b.backup, to, &b.suffix); if let Some(ref backup_path) = backup_path { - rename_with_fallback(to, backup_path)?; + rename_with_fallback(to, backup_path, multi_progress)?; } if b.update && fs::metadata(from)?.modified()? <= fs::metadata(to)?.modified()? { @@ -405,21 +458,36 @@ fn rename(from: &Path, to: &Path, b: &Behavior) -> io::Result<()> { } } - rename_with_fallback(from, to)?; + rename_with_fallback(from, to, multi_progress)?; if b.verbose { - print!("{} -> {}", from.quote(), to.quote()); - match backup_path { - Some(path) => println!(" (backup: {})", path.quote()), - None => println!(), - } + let message = match backup_path { + Some(path) => format!( + "{} -> {} (backup: {})", + from.quote(), + to.quote(), + path.quote() + ), + None => format!("{} -> {}", from.quote(), to.quote()), + }; + + match multi_progress { + Some(pb) => pb.suspend(|| { + println!("{}", message); + }), + None => println!("{}", message), + }; } Ok(()) } /// A wrapper around `fs::rename`, so that if it fails, we try falling back on /// copying and removing. -fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> { +fn rename_with_fallback( + from: &Path, + to: &Path, + multi_progress: Option<&MultiProgress>, +) -> io::Result<()> { if fs::rename(from, to).is_err() { // Get metadata without following symlinks let metadata = from.symlink_metadata()?; @@ -441,7 +509,39 @@ fn rename_with_fallback(from: &Path, to: &Path) -> io::Result<()> { copy_inside: true, ..DirCopyOptions::new() }; - if let Err(err) = move_dir(from, to, &options) { + + // Calculate total size of directory + // Silently degrades: + // If finding the total size fails for whatever reason, + // the progress bar wont be shown for this file / dir. + // (Move will probably fail due to permission error later?) + let total_size = dir_get_size(from).ok(); + + let progress_bar = + if let (Some(multi_progress), Some(total_size)) = (multi_progress, total_size) { + let bar = ProgressBar::new(total_size).with_style( + ProgressStyle::with_template( + "{msg}: [{elapsed_precise}] {wide_bar} {bytes:>7}/{total_bytes:7}", + ) + .unwrap(), + ); + + Some(multi_progress.add(bar)) + } else { + None + }; + + let result = if let Some(ref pb) = progress_bar { + move_dir_with_progress(from, to, &options, |process_info: TransitProcess| { + pb.set_position(process_info.copied_bytes); + pb.set_message(process_info.file_name); + TransitProcessResult::ContinueOrAbort + }) + } else { + move_dir(from, to, &options) + }; + + if let Err(err) = result { return match err.kind { fs_extra::error::ErrorKind::PermissionDenied => Err(io::Error::new( io::ErrorKind::PermissionDenied, diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index e0ddc099e..de1af9e30 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -126,7 +126,7 @@ impl<'a> BytesGenerator<'a> { fn new(total_bytes: u64, gen_type: PassType<'a>, exact: bool) -> BytesGenerator { let rng = match gen_type { PassType::Random => Some(RefCell::new(rand::thread_rng())), - _ => None, + PassType::Pattern(_) => None, }; let bytes = [0; BLOCK_SIZE]; diff --git a/src/uu/tail/Cargo.toml b/src/uu/tail/Cargo.toml index 4c2552d0c..3f7b00436 100644 --- a/src/uu/tail/Cargo.toml +++ b/src/uu/tail/Cargo.toml @@ -22,6 +22,7 @@ memchr = "2.5.0" notify = { version = "=5.0.0", features=["macos_kqueue"]} uucore = { version=">=0.0.16", package="uucore", path="../../uucore", features=["ringbuffer", "lines"] } same-file = "1.0.6" +atty = "0.2" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.42.0", default-features = false, features = ["Win32_System_Threading", "Win32_Foundation"] } diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index f2ef918f3..6e972d25f 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -7,8 +7,10 @@ use crate::paths::Input; use crate::{parse, platform, Quotable}; +use atty::Stream; use clap::crate_version; use clap::{parser::ValueSource, Arg, ArgAction, ArgMatches, Command}; +use same_file::Handle; use std::collections::VecDeque; use std::ffi::OsString; use std::time::Duration; @@ -113,6 +115,13 @@ pub enum FollowMode { Name, } +#[derive(Debug)] +pub enum VerificationResult { + Ok, + CannotFollowStdinByName, + NoOutput, +} + #[derive(Debug, Default)] pub struct Settings { pub follow: Option, @@ -149,10 +158,6 @@ impl Settings { settings.retry = matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY); - if settings.retry && settings.follow.is_none() { - show_warning!("--retry ignored; --retry is useful only when following"); - } - if let Some(s) = matches.get_one::(options::SLEEP_INT) { settings.sleep_sec = match s.parse::() { Ok(s) => Duration::from_secs_f32(s), @@ -194,14 +199,8 @@ impl Settings { format!("invalid PID: {}", pid_str.quote()), )); } + settings.pid = pid; - if settings.follow.is_none() { - show_warning!("PID ignored; --pid=PID is useful only when following"); - } - if !platform::supports_pid_checks(settings.pid) { - show_warning!("--pid=PID is not supported on this system"); - settings.pid = 0; - } } Err(e) => { return Err(USimpleError::new( @@ -214,16 +213,6 @@ impl Settings { settings.mode = FilterMode::from(matches)?; - // Mimic GNU's tail for -[nc]0 without -f and exit immediately - if settings.follow.is_none() - && matches!( - settings.mode, - FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero) - ) - { - std::process::exit(0) - } - let mut inputs: VecDeque = matches .get_many::(options::ARG_FILES) .map(|v| v.map(|string| Input::from(string.clone())).collect()) @@ -243,6 +232,81 @@ impl Settings { Ok(settings) } + + pub fn has_only_stdin(&self) -> bool { + self.inputs.iter().all(|input| input.is_stdin()) + } + + pub fn has_stdin(&self) -> bool { + self.inputs.iter().any(|input| input.is_stdin()) + } + + pub fn num_inputs(&self) -> usize { + self.inputs.len() + } + + /// Check [`Settings`] for problematic configurations of tail originating from user provided + /// command line arguments and print appropriate warnings. + pub fn check_warnings(&self) { + if self.retry { + if self.follow.is_none() { + show_warning!("--retry ignored; --retry is useful only when following"); + } else if self.follow == Some(FollowMode::Descriptor) { + show_warning!("--retry only effective for the initial open"); + } + } + + if self.pid != 0 { + if self.follow.is_none() { + show_warning!("PID ignored; --pid=PID is useful only when following"); + } else if !platform::supports_pid_checks(self.pid) { + show_warning!("--pid=PID is not supported on this system"); + } + } + + // This warning originates from gnu's tail implementation of the equivalent warning. If the + // user wants to follow stdin, but tail is blocking indefinitely anyways, because of stdin + // as `tty` (but no otherwise blocking stdin), then we print a warning that `--follow` + // cannot be applied under these circumstances and is therefore ineffective. + if self.follow.is_some() && self.has_stdin() { + let blocking_stdin = self.pid == 0 + && self.follow == Some(FollowMode::Descriptor) + && self.num_inputs() == 1 + && Handle::stdin().map_or(false, |handle| { + handle + .as_file() + .metadata() + .map_or(false, |meta| !meta.is_file()) + }); + + if !blocking_stdin && atty::is(Stream::Stdin) { + show_warning!("following standard input indefinitely is ineffective"); + } + } + } + + /// Verify [`Settings`] and try to find unsolvable misconfigurations of tail originating from + /// user provided command line arguments. In contrast to [`Settings::check_warnings`] these + /// misconfigurations usually lead to the immediate exit or abortion of the running `tail` + /// process. + pub fn verify(&self) -> VerificationResult { + // Mimic GNU's tail for `tail -F` + if self.inputs.iter().any(|i| i.is_stdin()) && self.follow == Some(FollowMode::Name) { + return VerificationResult::CannotFollowStdinByName; + } + + // Mimic GNU's tail for -[nc]0 without -f and exit immediately + if self.follow.is_none() + && matches!( + self.mode, + FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero) + ) + { + return VerificationResult::NoOutput; + } + + VerificationResult::Ok + } } pub fn arg_iterate<'a>( @@ -298,19 +362,6 @@ fn parse_num(src: &str) -> Result { }) } -pub fn stdin_is_pipe_or_fifo() -> bool { - #[cfg(unix)] - { - platform::stdin_is_pipe_or_fifo() - } - #[cfg(windows)] - { - winapi_util::file::typ(winapi_util::HandleRef::stdin()) - .map(|t| t.is_disk() || t.is_pipe()) - .unwrap_or(false) - } -} - pub fn parse_args(args: impl uucore::Args) -> UResult { let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?; Settings::from(&matches) diff --git a/src/uu/tail/src/chunks.rs b/src/uu/tail/src/chunks.rs index acfc69a30..7ad2e153b 100644 --- a/src/uu/tail/src/chunks.rs +++ b/src/uu/tail/src/chunks.rs @@ -7,7 +7,9 @@ //! or at the end of piped stdin with [`LinesChunk`] or [`BytesChunk`]. //! //! Use [`ReverseChunks::new`] to create a new iterator over chunks of bytes from the file. + // spell-checker:ignore (ToDO) filehandle BUFSIZ + use std::collections::VecDeque; use std::fs::File; use std::io::{BufRead, Read, Seek, SeekFrom, Write}; diff --git a/src/uu/tail/src/follow/files.rs b/src/uu/tail/src/follow/files.rs index 556defd1f..8686e73f4 100644 --- a/src/uu/tail/src/follow/files.rs +++ b/src/uu/tail/src/follow/files.rs @@ -13,7 +13,6 @@ use std::collections::hash_map::Keys; use std::collections::HashMap; use std::fs::{File, Metadata}; use std::io::{stdout, BufRead, BufReader, BufWriter}; - use std::path::{Path, PathBuf}; use uucore::error::UResult; diff --git a/src/uu/tail/src/follow/mod.rs b/src/uu/tail/src/follow/mod.rs index 4bb2798d1..e31eb54d1 100644 --- a/src/uu/tail/src/follow/mod.rs +++ b/src/uu/tail/src/follow/mod.rs @@ -6,4 +6,4 @@ mod files; mod watch; -pub use watch::{follow, WatcherService}; +pub use watch::{follow, Observer}; diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index dd8728c45..31a6d70cb 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -13,8 +13,7 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind}; use std::collections::VecDeque; use std::io::BufRead; use std::path::{Path, PathBuf}; -use std::sync::mpsc; -use std::sync::mpsc::{channel, Receiver}; +use std::sync::mpsc::{self, channel, Receiver}; use uucore::display::Quotable; use uucore::error::{set_exit_code, UResult, USimpleError}; use uucore::show_error; @@ -81,7 +80,7 @@ impl WatcherRx { } } -pub struct WatcherService { +pub struct Observer { /// Whether --retry was given on the command line pub retry: bool, @@ -92,18 +91,28 @@ pub struct WatcherService { /// platform specific event driven method. Since `use_polling` is subject to /// change during runtime it is moved out of [`Settings`]. pub use_polling: bool, + pub watcher_rx: Option, pub orphans: Vec, pub files: FileHandling, + + pub pid: platform::Pid, } -impl WatcherService { +impl Observer { pub fn new( retry: bool, follow: Option, use_polling: bool, files: FileHandling, + pid: platform::Pid, ) -> Self { + let pid = if platform::supports_pid_checks(pid) { + pid + } else { + 0 + }; + Self { retry, follow, @@ -111,6 +120,7 @@ impl WatcherService { watcher_rx: None, orphans: Vec::new(), files, + pid, } } @@ -120,6 +130,7 @@ impl WatcherService { settings.follow, settings.use_polling, FileHandling::from(settings), + settings.pid, ) } @@ -460,14 +471,12 @@ impl WatcherService { } } -pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResult<()> { - if watcher_service.files.no_files_remaining(settings) - && !watcher_service.files.only_stdin_remaining() - { +pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { + if observer.files.no_files_remaining(settings) && !observer.files.only_stdin_remaining() { return Err(USimpleError::new(1, text::NO_FILES_REMAINING.to_string())); } - let mut process = platform::ProcessChecker::new(settings.pid); + let mut process = platform::ProcessChecker::new(observer.pid); let mut _event_counter = 0; let mut _timeout_counter = 0; @@ -478,7 +487,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // If `--pid=p`, tail checks whether process p // is alive at least every `--sleep-interval=N` seconds - if settings.follow.is_some() && settings.pid != 0 && process.is_dead() { + if settings.follow.is_some() && observer.pid != 0 && process.is_dead() { // p is dead, tail will also terminate break; } @@ -487,22 +496,20 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // If a path becomes an orphan during runtime, it will be added to orphans. // To be able to differentiate between the cases of test_retry8 and test_retry9, // here paths will not be removed from orphans if the path becomes available. - if watcher_service.follow_name_retry() { - for new_path in &watcher_service.orphans { + if observer.follow_name_retry() { + for new_path in &observer.orphans { if new_path.exists() { - let pd = watcher_service.files.get(new_path); + let pd = observer.files.get(new_path); let md = new_path.metadata().unwrap(); if md.is_tailable() && pd.reader.is_none() { show_error!( "{} has appeared; following new file", pd.display_name.quote() ); - watcher_service.files.update_metadata(new_path, Some(md)); - watcher_service.files.update_reader(new_path)?; - _read_some = watcher_service - .files - .tail_file(new_path, settings.verbose)?; - watcher_service + observer.files.update_metadata(new_path, Some(md)); + observer.files.update_reader(new_path)?; + _read_some = observer.files.tail_file(new_path, settings.verbose)?; + observer .watcher_rx .as_mut() .unwrap() @@ -514,7 +521,7 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu // With -f, sleep for approximately N seconds (default 1.0) between iterations; // We wake up if Notify sends an Event or if we wait more than `sleep_sec`. - let rx_result = watcher_service + let rx_result = observer .watcher_rx .as_mut() .unwrap() @@ -529,9 +536,9 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu match rx_result { Ok(Ok(event)) => { if let Some(event_path) = event.paths.first() { - if watcher_service.files.contains_key(event_path) { + if observer.files.contains_key(event_path) { // Handle Event if it is about a path that we are monitoring - paths = watcher_service.handle_event(&event, settings)?; + paths = observer.handle_event(&event, settings)?; } } } @@ -540,8 +547,8 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu paths, })) if e.kind() == std::io::ErrorKind::NotFound => { if let Some(event_path) = paths.first() { - if watcher_service.files.contains_key(event_path) { - let _ = watcher_service + if observer.files.contains_key(event_path) { + let _ = observer .watcher_rx .as_mut() .unwrap() @@ -566,16 +573,16 @@ pub fn follow(mut watcher_service: WatcherService, settings: &Settings) -> UResu Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {}", e))), } - if watcher_service.use_polling && settings.follow.is_some() { + if observer.use_polling && settings.follow.is_some() { // Consider all files to potentially have new content. // This is a workaround because `Notify::PollWatcher` // does not recognize the "renaming" of files. - paths = watcher_service.files.keys().cloned().collect::>(); + paths = observer.files.keys().cloned().collect::>(); } // main print loop for path in &paths { - _read_some = watcher_service.files.tail_file(path, settings.verbose)?; + _read_some = observer.files.tail_file(path, settings.verbose)?; } if _timeout_counter == settings.max_unchanged_stats { diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 203a23817..03656d036 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -5,24 +5,14 @@ // spell-checker:ignore tailable seekable stdlib (stdlib) -#[cfg(unix)] -use std::os::unix::fs::{FileTypeExt, MetadataExt}; - -use std::collections::VecDeque; +use crate::text; use std::fs::{File, Metadata}; use std::io::{Seek, SeekFrom}; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; - use uucore::error::UResult; -use crate::args::Settings; -use crate::text; - -// * This file is part of the uutils coreutils package. -// * -// * For the full copyright and license information, please view the LICENSE -// * file that was distributed with this source code. - #[derive(Debug, Clone)] pub enum InputKind { File(PathBuf), @@ -36,6 +26,7 @@ pub struct Input { } impl Input { + // TODO: from &str may be the better choice pub fn from(string: String) -> Self { let kind = if string == text::DASH { InputKind::Stdin @@ -132,44 +123,6 @@ impl HeaderPrinter { } } } - -#[derive(Debug, Clone)] -pub struct InputService { - pub inputs: VecDeque, - pub presume_input_pipe: bool, - pub header_printer: HeaderPrinter, -} - -impl InputService { - pub fn new(verbose: bool, presume_input_pipe: bool, inputs: VecDeque) -> Self { - Self { - inputs, - presume_input_pipe, - header_printer: HeaderPrinter::new(verbose, true), - } - } - - pub fn from(settings: &Settings) -> Self { - Self::new( - settings.verbose, - settings.presume_input_pipe, - settings.inputs.clone(), - ) - } - - pub fn has_stdin(&mut self) -> bool { - self.inputs.iter().any(|input| input.is_stdin()) - } - - pub fn has_only_stdin(&self) -> bool { - self.inputs.iter().all(|input| input.is_stdin()) - } - - pub fn print_header(&mut self, input: &Input) { - self.header_printer.print_input(input); - } -} - pub trait FileExtTail { #[allow(clippy::wrong_self_convention)] fn is_seekable(&mut self, current_offset: u64) -> bool; @@ -228,9 +181,11 @@ impl MetadataExtTail for Metadata { } #[cfg(windows)] { + // TODO: `file_index` requires unstable library feature `windows_by_handle` // use std::os::windows::prelude::*; // if let Some(self_id) = self.file_index() { // if let Some(other_id) = other.file_index() { + // // TODO: not sure this is the equivalent of comparing inode numbers // // return self_id.eq(&other_id); // } diff --git a/src/uu/tail/src/platform/mod.rs b/src/uu/tail/src/platform/mod.rs index 17660731d..e5ae8b8d8 100644 --- a/src/uu/tail/src/platform/mod.rs +++ b/src/uu/tail/src/platform/mod.rs @@ -11,7 +11,6 @@ #[cfg(unix)] pub use self::unix::{ //stdin_is_bad_fd, stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker, - stdin_is_pipe_or_fifo, supports_pid_checks, Pid, ProcessChecker, diff --git a/src/uu/tail/src/platform/unix.rs b/src/uu/tail/src/platform/unix.rs index 2627cc252..ed34b2cf9 100644 --- a/src/uu/tail/src/platform/unix.rs +++ b/src/uu/tail/src/platform/unix.rs @@ -11,8 +11,6 @@ // spell-checker:ignore (ToDO) stdlib, ISCHR, GETFD // spell-checker:ignore (options) EPERM, ENOSYS -use libc::S_IFCHR; -use nix::sys::stat::fstat; use std::io::Error; pub type Pid = libc::pid_t; @@ -45,13 +43,6 @@ pub fn supports_pid_checks(pid: self::Pid) -> bool { fn get_errno() -> i32 { Error::last_os_error().raw_os_error().unwrap() } -#[inline] -pub fn stdin_is_pipe_or_fifo() -> bool { - // IFCHR means the file (stdin) is a character input device, which is the case of a terminal. - // We just need to check if stdin is not a character device here, because we are not interested - // in the type of stdin itself. - fstat(libc::STDIN_FILENO).map_or(false, |file| file.st_mode as libc::mode_t & S_IFCHR == 0) -} //pub fn stdin_is_bad_fd() -> bool { // FIXME: Detect a closed file descriptor, e.g.: `tail <&-` diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index af56c9813..2a5a94352 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -24,58 +24,57 @@ mod paths; mod platform; pub mod text; +pub use args::uu_app; +use args::{parse_args, FilterMode, Settings, Signum}; +use chunks::ReverseChunks; +use follow::Observer; +use paths::{FileExtTail, HeaderPrinter, Input, InputKind, MetadataExtTail}; use same_file::Handle; use std::cmp::Ordering; use std::fs::File; use std::io::{self, stdin, stdout, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; -use uucore::{show, show_error, show_warning}; - use uucore::display::Quotable; use uucore::error::{get_exit_code, set_exit_code, FromIo, UError, UResult, USimpleError}; - -pub use args::uu_app; -use args::{parse_args, FilterMode, Settings, Signum}; -use chunks::ReverseChunks; -use follow::WatcherService; -use paths::{FileExtTail, Input, InputKind, InputService, MetadataExtTail}; +use uucore::{show, show_error}; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = parse_args(args)?; + + settings.check_warnings(); + + match settings.verify() { + args::VerificationResult::CannotFollowStdinByName => { + return Err(USimpleError::new( + 1, + format!("cannot follow {} by name", text::DASH.quote()), + )) + } + // Exit early if we do not output anything. Note, that this may break a pipe + // when tail is on the receiving side. + args::VerificationResult::NoOutput => return Ok(()), + args::VerificationResult::Ok => {} + } + uu_tail(&settings) } fn uu_tail(settings: &Settings) -> UResult<()> { - // Mimic GNU's tail for `tail -F` and exit immediately - let mut input_service = InputService::from(settings); - let mut watcher_service = WatcherService::from(settings); + let mut printer = HeaderPrinter::new(settings.verbose, true); + let mut observer = Observer::from(settings); - if input_service.has_stdin() && watcher_service.follow_name() { - return Err(USimpleError::new( - 1, - format!("cannot follow {} by name", text::DASH.quote()), - )); - } - - watcher_service.start(settings)?; + observer.start(settings)?; // Do an initial tail print of each path's content. // Add `path` and `reader` to `files` map if `--follow` is selected. - for input in &input_service.inputs.clone() { + for input in &settings.inputs.clone() { match input.kind() { InputKind::File(path) if cfg!(not(unix)) || path != &PathBuf::from(text::DEV_STDIN) => { - tail_file( - settings, - &mut input_service, - input, - path, - &mut watcher_service, - 0, - )?; + tail_file(settings, &mut printer, input, path, &mut observer, 0)?; } // File points to /dev/stdin here InputKind::File(_) | InputKind::Stdin => { - tail_stdin(settings, &mut input_service, input, &mut watcher_service)?; + tail_stdin(settings, &mut printer, input, &mut observer)?; } } } @@ -90,9 +89,8 @@ fn uu_tail(settings: &Settings) -> UResult<()> { the input file is not a FIFO, pipe, or regular file, it is unspecified whether or not the -f option shall be ignored. */ - - if !input_service.has_only_stdin() { - follow::follow(watcher_service, settings)?; + if !settings.has_only_stdin() { + follow::follow(observer, settings)?; } } @@ -105,16 +103,12 @@ fn uu_tail(settings: &Settings) -> UResult<()> { fn tail_file( settings: &Settings, - input_service: &mut InputService, + header_printer: &mut HeaderPrinter, input: &Input, path: &Path, - watcher_service: &mut WatcherService, + observer: &mut Observer, offset: u64, ) -> UResult<()> { - if watcher_service.follow_descriptor_retry() { - show_warning!("--retry only effective for the initial open"); - } - if !path.exists() { set_exit_code(1); show_error!( @@ -122,11 +116,11 @@ fn tail_file( input.display_name, text::NO_SUCH_FILE ); - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } else if path.is_dir() { set_exit_code(1); - input_service.print_header(input); + header_printer.print_input(input); let err_msg = "Is a directory".to_string(); show_error!("error reading '{}': {}", input.display_name, err_msg); @@ -142,16 +136,16 @@ fn tail_file( msg ); } - if !(watcher_service.follow_name_retry()) { + if !(observer.follow_name_retry()) { // skip directory if not retry return Ok(()); } - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } else if input.is_tailable() { let metadata = path.metadata().ok(); match File::open(path) { Ok(mut file) => { - input_service.print_header(input); + header_printer.print_input(input); let mut reader; if !settings.presume_input_pipe && file.is_seekable(if input.is_stdin() { offset } else { 0 }) @@ -163,7 +157,7 @@ fn tail_file( reader = BufReader::new(file); unbounded_tail(&mut reader, settings)?; } - watcher_service.add_path( + observer.add_path( path, input.display_name.as_str(), Some(Box::new(reader)), @@ -171,20 +165,20 @@ fn tail_file( )?; } Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; show!(e.map_err_context(|| { format!("cannot open '{}' for reading", input.display_name) })); } Err(e) => { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; return Err(e.map_err_context(|| { format!("cannot open '{}' for reading", input.display_name) })); } } } else { - watcher_service.add_bad_path(path, input.display_name.as_str(), false)?; + observer.add_bad_path(path, input.display_name.as_str(), false)?; } Ok(()) @@ -192,9 +186,9 @@ fn tail_file( fn tail_stdin( settings: &Settings, - input_service: &mut InputService, + header_printer: &mut HeaderPrinter, input: &Input, - watcher_service: &mut WatcherService, + observer: &mut Observer, ) -> UResult<()> { match input.resolve() { // fifo @@ -211,24 +205,20 @@ fn tail_stdin( } tail_file( settings, - input_service, + header_printer, input, &path, - watcher_service, + observer, stdin_offset, )?; } // pipe None => { - input_service.print_header(input); + header_printer.print_input(input); if !paths::stdin_is_bad_fd() { let mut reader = BufReader::new(stdin()); unbounded_tail(&mut reader, settings)?; - watcher_service.add_stdin( - input.display_name.as_str(), - Some(Box::new(reader)), - true, - )?; + observer.add_stdin(input.display_name.as_str(), Some(Box::new(reader)), true)?; } else { set_exit_code(1); show_error!( @@ -417,7 +407,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR FilterMode::Lines(Signum::Negative(count), sep) => { let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count); chunks.fill(reader)?; - chunks.print(writer)?; + chunks.print(&mut writer)?; } FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => { io::copy(reader, &mut writer)?; @@ -441,7 +431,7 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR FilterMode::Bytes(Signum::Negative(count)) => { let mut chunks = chunks::BytesChunkBuffer::new(*count); chunks.fill(reader)?; - chunks.print(writer)?; + chunks.print(&mut writer)?; } FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => { io::copy(reader, &mut writer)?; diff --git a/src/uu/tail/src/text.rs b/src/uu/tail/src/text.rs index 62ad9cf70..0e4a26f39 100644 --- a/src/uu/tail/src/text.rs +++ b/src/uu/tail/src/text.rs @@ -19,3 +19,6 @@ pub static BACKEND: &str = "kqueue"; #[cfg(target_os = "windows")] pub static BACKEND: &str = "ReadDirectoryChanges"; pub static FD0: &str = "/dev/fd/0"; +pub static IS_A_DIRECTORY: &str = "Is a directory"; +pub static DEV_TTY: &str = "/dev/tty"; +pub static DEV_PTMX: &str = "/dev/ptmx"; diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 8f7c13d20..24be78c06 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -396,7 +396,7 @@ pub fn canonicalize>( read_dir(parent)?; } } - _ => {} + MissingHandling::Missing => {} } Ok(result) } diff --git a/src/uucore/src/lib/mods/quoting_style.rs b/src/uucore/src/lib/mods/quoting_style.rs index 38401a169..b07154139 100644 --- a/src/uucore/src/lib/mods/quoting_style.rs +++ b/src/uucore/src/lib/mods/quoting_style.rs @@ -278,7 +278,7 @@ pub fn escape_name(name: &OsStr, style: &QuotingStyle) -> String { match quotes { Quotes::Single => format!("'{}'", escaped_str), Quotes::Double => format!("\"{}\"", escaped_str), - _ => escaped_str, + Quotes::None => escaped_str, } } QuotingStyle::Shell { diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index e59dcf3ea..6ea075541 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -5,17 +5,33 @@ // spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile file siette ocho nueve diez MULT // spell-checker:ignore (libs) kqueue -// spell-checker:ignore (jargon) tailable untailable +// spell-checker:ignore (jargon) tailable untailable datasame runneradmin tmpi extern crate tail; use crate::common::random::*; use crate::common::util::*; +use pretty_assertions::assert_eq; use rand::distributions::Alphanumeric; use std::char::from_digit; +use std::fs::File; use std::io::Write; +#[cfg(not(target_vendor = "apple"))] +use std::io::{Seek, SeekFrom}; +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +use std::path::Path; use std::process::Stdio; use tail::chunks::BUFFER_SIZE as CHUNK_BUFFER_SIZE; +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +use tail::text; static FOOBAR_TXT: &str = "foobar.txt"; static FOOBAR_2_TXT: &str = "foobar2.txt"; @@ -73,12 +89,12 @@ fn test_stdin_redirect_file() { at.write("f", "foo"); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .run() .stdout_is("foo") .succeeded(); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .arg("-v") .run() .no_stderr() @@ -88,7 +104,7 @@ fn test_stdin_redirect_file() { let mut p = ts .ucmd() .arg("-f") - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .run_no_wait(); p.make_assertion_with_delay(500).is_alive(); @@ -102,13 +118,12 @@ fn test_stdin_redirect_file() { #[cfg(not(target_vendor = "apple"))] // FIXME: for currently not working platforms fn test_stdin_redirect_offset() { // inspired by: "gnu/tests/tail-2/start-middle.sh" - use std::io::{Seek, SeekFrom}; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; at.write("k", "1\n2\n"); - let mut fh = std::fs::File::open(at.plus("k")).unwrap(); + let mut fh = File::open(at.plus("k")).unwrap(); fh.seek(SeekFrom::Start(2)).unwrap(); ts.ucmd() @@ -127,7 +142,6 @@ fn test_stdin_redirect_offset2() { // expected: ==> standard input <== // like test_stdin_redirect_offset but with multiple files - use std::io::{Seek, SeekFrom}; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; @@ -135,7 +149,7 @@ fn test_stdin_redirect_offset2() { at.write("k", "1\n2\n"); at.write("l", "3\n4\n"); at.write("m", "5\n6\n"); - let mut fh = std::fs::File::open(at.plus("k")).unwrap(); + let mut fh = File::open(at.plus("k")).unwrap(); fh.seek(SeekFrom::Start(2)).unwrap(); ts.ucmd() @@ -257,7 +271,7 @@ fn test_follow_redirect_stdin_name_retry() { let mut args = vec!["-F", "-"]; for _ in 0..2 { ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("f")).unwrap()) + .set_stdin(File::open(at.plus("f")).unwrap()) .args(&args) .fails() .no_stdout() @@ -284,13 +298,13 @@ fn test_stdin_redirect_dir() { at.mkdir("dir"); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("dir")).unwrap()) + .set_stdin(File::open(at.plus("dir")).unwrap()) .fails() .no_stdout() .stderr_is("tail: error reading 'standard input': Is a directory") .code_is(1); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("dir")).unwrap()) + .set_stdin(File::open(at.plus("dir")).unwrap()) .arg("-") .fails() .no_stdout() @@ -317,13 +331,13 @@ fn test_stdin_redirect_dir_when_target_os_is_macos() { at.mkdir("dir"); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("dir")).unwrap()) + .set_stdin(File::open(at.plus("dir")).unwrap()) .fails() .no_stdout() .stderr_is("tail: cannot open 'standard input' for reading: No such file or directory") .code_is(1); ts.ucmd() - .set_stdin(std::fs::File::open(at.plus("dir")).unwrap()) + .set_stdin(File::open(at.plus("dir")).unwrap()) .arg("-") .fails() .no_stdout() @@ -365,61 +379,6 @@ fn test_follow_stdin_name_retry() { } } -#[test] -#[cfg(target_os = "linux")] -#[cfg(disable_until_fixed)] -fn test_follow_stdin_explicit_indefinitely() { - // inspired by: "gnu/tests/tail-2/follow-stdin.sh" - // tail -f - /dev/null standard input <== - - let ts = TestScenario::new(util_name!()); - - let mut p = ts - .ucmd() - .set_stdin(Stdio::null()) - .args(&["-f", "-", "/dev/null"]) - .run_no_wait(); - - p.make_assertion_with_delay(500).is_alive(); - p.kill() - .make_assertion() - .with_all_output() - .stdout_is("==> standard input <==") - .stderr_is("tail: warning: following standard input indefinitely is ineffective"); - - // Also: - // $ echo bar > foo - // - // $ tail -f - - - // tail: warning: following standard input indefinitely is ineffective - // ==> standard input <== - // - // $ tail -f - foo - // tail: warning: following standard input indefinitely is ineffective - // ==> standard input <== - // - // - // $ tail -f - foo - // tail: warning: following standard input indefinitely is ineffective - // ==> standard input <== - // - // $ tail -f foo - - // tail: warning: following standard input indefinitely is ineffective - // ==> foo <== - // bar - // - // ==> standard input <== - // - - // $ echo f00 | tail -f foo - - // bar - // - - // TODO: Implement the above behavior of GNU's tail for following stdin indefinitely -} - #[test] fn test_follow_bad_fd() { // Provoke a "bad file descriptor" error by closing the fd @@ -688,7 +647,7 @@ fn test_follow_invalid_pid() { not(target_os = "android") ))] // FIXME: for currently not working platforms fn test_follow_with_pid() { - use std::process::{Command, Stdio}; + use std::process::Command; let (at, mut ucmd) = at_and_ucmd!(); @@ -1056,6 +1015,7 @@ fn test_positive_zero_bytes() { ts.ucmd() .args(&["-c", "0"]) .pipe_in("abcde") + .ignore_stdin_write_error() .succeeds() .no_stdout() .no_stderr(); @@ -1539,8 +1499,8 @@ fn test_retry8() { let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let watched_file = std::path::Path::new("watched_file"); - let parent_dir = std::path::Path::new("parent_dir"); + let watched_file = Path::new("watched_file"); + let parent_dir = Path::new("parent_dir"); let user_path = parent_dir.join(watched_file); let parent_dir = parent_dir.to_str().unwrap(); let user_path = user_path.to_str().unwrap(); @@ -1603,12 +1563,12 @@ fn test_retry9() { // Ensure that inotify will switch to polling mode if directory // of the watched file was removed and recreated. - use tail::text::BACKEND; + use text::BACKEND; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; - let watched_file = std::path::Path::new("watched_file"); - let parent_dir = std::path::Path::new("parent_dir"); + let watched_file = Path::new("watched_file"); + let parent_dir = Path::new("parent_dir"); let user_path = parent_dir.join(watched_file); let parent_dir = parent_dir.to_str().unwrap(); let user_path = user_path.to_str().unwrap(); @@ -3496,3 +3456,1070 @@ fn test_args_when_presume_input_pipe_given_input_is_file() { .succeeds() .stdout_only(random_string); } + +#[test] +#[cfg(disable_until_fixed)] +// FIXME: currently missing in the error message is the last line >>tail: no files remaining<< +fn test_when_follow_retry_given_redirected_stdin_from_directory_then_correct_error_message() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkdir("dir"); + + let expected = "tail: warning: --retry only effective for the initial open\n\ + tail: error reading 'standard input': Is a directory\n\ + tail: 'standard input': cannot follow end of this type of file\n\ + tail: no files remaining\n"; + ts.ucmd() + .set_stdin(File::open(at.plus("dir")).unwrap()) + .args(&["-f", "--retry"]) + .fails() + .stderr_only(expected) + .code_is(1); +} + +#[test] +fn test_when_argument_file_is_a_directory() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + at.mkdir("dir"); + + let expected = "tail: error reading 'dir': Is a directory"; + ts.ucmd() + .arg("dir") + .fails() + .stderr_only(expected) + .code_is(1); +} + +// TODO: make this work on windows +#[test] +#[cfg(unix)] +fn test_when_argument_file_is_a_symlink() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let mut file = at.make_file("target"); + + at.symlink_file("target", "link"); + + ts.ucmd() + .args(&["-c", "+0", "link"]) + .succeeds() + .no_stdout() + .no_stderr(); + + let random_string = RandomString::generate(AlphanumericNewline, 100); + let result = file.write_all(random_string.as_bytes()); + assert!(result.is_ok()); + + ts.ucmd() + .args(&["-c", "+0", "link"]) + .succeeds() + .stdout_only(random_string); + + at.mkdir("dir"); + + at.symlink_file("dir", "dir_link"); + + let expected = "tail: error reading 'dir_link': Is a directory"; + ts.ucmd() + .arg("dir_link") + .fails() + .stderr_only(expected) + .code_is(1); +} + +// TODO: make this work on windows +#[test] +#[cfg(unix)] +fn test_when_argument_file_is_a_symlink_to_directory_then_error() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir"); + at.symlink_file("dir", "dir_link"); + + let expected = "tail: error reading 'dir_link': Is a directory"; + ts.ucmd() + .arg("dir_link") + .fails() + .stderr_only(expected) + .code_is(1); +} + +// TODO: make this work on windows +#[test] +#[cfg(unix)] +#[cfg(disabled_until_fixed)] +fn test_when_argument_file_is_a_faulty_symlink_then_error() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.symlink_file("self", "self"); + + #[cfg(all(not(target_env = "musl"), not(target_os = "android")))] + let expected = "tail: cannot open 'self' for reading: Too many levels of symbolic links"; + #[cfg(all(not(target_env = "musl"), target_os = "android"))] + let expected = "tail: cannot open 'self' for reading: Too many symbolic links encountered"; + #[cfg(all(target_env = "musl", not(target_os = "android")))] + let expected = "tail: cannot open 'self' for reading: Symbolic link loop"; + + ts.ucmd() + .arg("self") + .fails() + .stderr_only(expected) + .code_is(1); + + at.symlink_file("missing", "broken"); + + let expected = "tail: cannot open 'broken' for reading: No such file or directory"; + ts.ucmd() + .arg("broken") + .fails() + .stderr_only(expected) + .code_is(1); +} + +#[test] +#[cfg(unix)] +#[cfg(disabled_until_fixed)] +fn test_when_argument_file_is_non_existent_unix_socket_address_then_error() { + use std::os::unix::net; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let socket = "socket"; + + // We only bind to create the socket file but do not listen + let result = net::UnixListener::bind(at.plus(socket)); + assert!(result.is_ok()); + + #[cfg(all(not(target_os = "freebsd"), not(target_os = "macos")))] + let expected_stderr = format!( + "tail: cannot open '{}' for reading: No such device or address", + socket + ); + #[cfg(target_os = "freebsd")] + let expected_stderr = format!( + "tail: cannot open '{}' for reading: Operation not supported", + socket + ); + #[cfg(target_os = "macos")] + let expected_stderr = format!( + "tail: cannot open '{}' for reading: Operation not supported on socket", + socket + ); + + ts.ucmd() + .arg(socket) + .fails() + .stderr_only(&expected_stderr) + .code_is(1); + + let path = "file"; + let mut file = at.make_file(path); + + let random_string = RandomString::generate(AlphanumericNewline, 100); + let result = file.write_all(random_string.as_bytes()); + assert!(result.is_ok()); + + let expected_stdout = vec![format!("==> {} <==", path), random_string].join("\n"); + ts.ucmd() + .args(&["-c", "+0", path, socket]) + .fails() + .stdout_is(&expected_stdout) + .stderr_is(&expected_stderr); + + // tail does not stop processing files when having encountered a "No such + // device or address" error. + ts.ucmd() + .args(&["-c", "+0", socket, path]) + .fails() + .stdout_is(&expected_stdout) + .stderr_is(&expected_stderr); +} + +#[test] +#[cfg(disabled_until_fixed)] +fn test_when_argument_files_are_simple_combinations_of_stdin_and_regular_file() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + fixtures.write("empty", ""); + fixtures.write("data", "file data"); + fixtures.write("fifo", "fifo data"); + + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> empty <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "empty"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + \n\ + ==> empty <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "empty"]) + .pipe_in("") + .run() + .success() + .stdout_only(expected); + + let expected = "==> empty <==\n\ + \n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "empty", "-"]) + .pipe_in("") + .run() + .success() + .stdout_only(expected); + + let expected = "==> empty <==\n\ + \n\ + ==> standard input <==\n\ + fifo data"; + scene + .ucmd() + .args(&["-c", "+0", "empty", "-"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + pipe data\n\ + ==> data <==\n\ + file data"; + scene + .ucmd() + .args(&["-c", "+0", "-", "data"]) + .pipe_in("pipe data") + .run() + .success() + .stdout_only(expected); + + let expected = "==> data <==\n\ + file data\n\ + ==> standard input <==\n\ + pipe data"; + scene + .ucmd() + .args(&["-c", "+0", "data", "-"]) + .pipe_in("pipe data") + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + pipe data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "-"]) + .pipe_in("pipe data") + .run() + .success() + .stdout_only(expected); + + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "-"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .success() + .stdout_only(expected); +} + +#[test] +#[cfg(disabled_until_fixed)] +fn test_when_argument_files_are_triple_combinations_of_fifo_pipe_and_regular_file() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + fixtures.write("empty", ""); + fixtures.write("data", "file data"); + fixtures.write("fifo", "fifo data"); + + let expected = "==> standard input <==\n\ + \n\ + ==> empty <==\n\ + \n\ + ==> standard input <==\n"; + + scene + .ucmd() + .args(&["-c", "+0", "-", "empty", "-"]) + .set_stdin(File::open(fixtures.plus("empty")).unwrap()) + .run() + .stdout_only(expected) + .success(); + + let expected = "==> standard input <==\n\ + \n\ + ==> empty <==\n\ + \n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "empty", "-"]) + .pipe_in("") + .stderr_to_stdout() + .run() + .stdout_only(expected) + .success(); + + let expected = "==> standard input <==\n\ + pipe data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "data", "-"]) + .pipe_in("pipe data") + .run() + .stdout_only(expected) + .success(); + + // Correct behavior in a sh shell is to remember the file pointer for the fifo, so we don't + // print the fifo twice. This matches the behavior, if only the pipe is present without fifo + // (See test above). Note that for example a zsh shell prints the pipe data and has therefore + // different output from the sh shell (or cmd shell on windows). + + // windows: tail returns with success although there is an error message present (on some + // windows systems). This error message comes from `echo` (the line ending `\r\n` indicates that + // too) which cannot write to the pipe because tail finished before echo was able to write to + // the pipe. Seems that windows `cmd` (like posix shells) ignores pipes when a fifo is present. + // This is actually the wished behavior and the test therefore succeeds. + #[cfg(windows)] + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n\ + (The process tried to write to a nonexistent pipe.\r\n)?"; + #[cfg(unix)] + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n"; + + #[cfg(windows)] + let cmd = ["cmd", "/C"]; + #[cfg(unix)] + let cmd = ["sh", "-c"]; + + scene + .cmd(cmd[0]) + .arg(cmd[1]) + .arg(format!( + "echo pipe data | {} tail -c +0 - data - < fifo", + scene.bin_path.display(), + )) + .run() + .stdout_only(expected) + .success(); + + let expected = "==> standard input <==\n\ + fifo data\n\ + ==> data <==\n\ + file data\n\ + ==> standard input <==\n"; + scene + .ucmd() + .args(&["-c", "+0", "-", "data", "-"]) + .set_stdin(File::open(fixtures.plus("fifo")).unwrap()) + .run() + .stdout_only(expected) + .success(); +} + +// Bug description: The content of a file is not printed to stdout if the output data does not +// contain newlines and --follow was given as arguments. +// +// This test is only formal on linux, since we currently do not detect this kind of error within the +// test system. However, this behavior shows up on the command line and, at the time of writing this +// description, with this test on macos and windows. +#[test] +#[cfg(disable_until_fixed)] +fn test_when_follow_retry_then_initial_print_of_file_is_written_to_stdout() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let expected_stdout = "file data"; + fixtures.write("data", expected_stdout); + + let mut child = scene + .ucmd() + .args(&["--follow=name", "--retry", "data"]) + .run_no_wait(); + + child + .delay(1500) + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +// TODO: Add test for the warning `--pid=PID is not supported on this system` +#[test] +fn test_args_when_settings_check_warnings_then_shows_warnings() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data\n"; + fixtures.write("data", file_data); + + let expected_stdout = format!( + "tail: warning: --retry ignored; --retry is useful only when following\n\ + {}", + file_data + ); + scene + .ucmd() + .args(&["--retry", "data"]) + .stderr_to_stdout() + .run() + .stdout_only(expected_stdout) + .success(); + + let expected_stdout = format!( + "tail: warning: --retry only effective for the initial open\n\ + {}", + file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "--retry", "data"]) + .stderr_to_stdout() + .run_no_wait(); + + child + .delay(500) + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = format!( + "tail: warning: PID ignored; --pid=PID is useful only when following\n\ + {}", + file_data + ); + scene + .ucmd() + .args(&["--pid=1000", "data"]) + .stderr_to_stdout() + .run() + .stdout_only(expected_stdout) + .success(); + + let expected_stdout = format!( + "tail: warning: --retry ignored; --retry is useful only when following\n\ + tail: warning: PID ignored; --pid=PID is useful only when following\n\ + {}", + file_data + ); + scene + .ucmd() + .args(&["--pid=1000", "--retry", "data"]) + .stderr_to_stdout() + .run() + .stdout_only(expected_stdout) + .success(); +} + +/// TODO: Write similar tests for windows +#[test] +#[cfg(target_os = "linux")] +fn test_args_when_settings_check_warnings_follow_indefinitely_then_warning() { + let scene = TestScenario::new(util_name!()); + + let file_data = "file data\n"; + scene.fixtures.write("data", file_data); + + let expected_stdout = "==> standard input <==\n"; + let expected_stderr = "tail: warning: following standard input indefinitely is ineffective\n"; + + // `tail -f - data` (without any redirect) would also print this warning in a terminal but we're + // not attached to a `tty` in the ci, so it's not possible to setup a test case for this + // particular usage. However, setting stdin to a `tty` behaves equivalently and we're faking an + // attached `tty` that way. + + // testing here that the warning is printed to stderr + // tail -f - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_is(expected_stderr) + .stdout_is(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // same like above but this time the order of the output matters and we're redirecting stderr to + // stdout + // tail -f - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = format!( + "tail: warning: following standard input indefinitely is ineffective\n\ + ==> data <==\n\ + {}\n\ + ==> standard input <==\n", + file_data + ); + // tail -f data - < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "data", "-"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // tail -f - - < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "-"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // tail -f - - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = "tail: warning: following standard input indefinitely is ineffective\n\ + ==> standard input <==\n"; + // tail --pid=100000 -f - data < /dev/ptmx + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "--pid=100000", "-", "data"]) + .set_stdin(File::open(text::DEV_PTMX).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +#[test] +#[cfg(unix)] +fn test_args_when_settings_check_warnings_follow_indefinitely_then_no_warning() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + #[cfg(target_vendor = "apple")] + let delay = 1000; + #[cfg(not(target_vendor = "apple"))] + let delay = 500; + + let file_data = "file data\n"; + let fifo_data = "fifo data\n"; + let fifo_name = "fifo"; + let file_name = "data"; + fixtures.write(file_name, file_data); + fixtures.write(fifo_name, fifo_data); + + let pipe_data = "pipe data"; + let expected_stdout = format!( + "==> standard input <==\n\ + {}\n\ + ==> {} <==\n\ + {}", + pipe_data, file_name, file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", file_name]) + .pipe_in(pipe_data) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(delay).is_alive(); + child + .kill() + .make_assertion_with_delay(delay) + .with_current_output() + .stdout_only(expected_stdout); + + // Test with regular file instead of /dev/tty + // Fails currently on macos with + // Diff < left / right > : + // ==> standard input <== + // >fifo data + // > + // ==> data <== + // file data + #[cfg(not(target_vendor = "apple"))] + { + let expected_stdout = format!( + "==> standard input <==\n\ + {}\n\ + ==> {} <==\n\ + {}", + fifo_data, file_name, file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "-", file_name]) + .set_stdin(File::open(fixtures.plus(fifo_name)).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(delay).is_alive(); + child + .kill() + .make_assertion_with_delay(delay) + .with_current_output() + .stdout_only(expected_stdout); + + let expected_stdout = format!( + "==> standard input <==\n\ + {}\n\ + ==> {} <==\n\ + {}", + fifo_data, file_name, file_data + ); + let mut child = scene + .ucmd() + .args(&["--follow=descriptor", "--pid=0", "-", file_name]) + .set_stdin(File::open(fixtures.plus(fifo_name)).unwrap()) + .stderr_to_stdout() + .run_no_wait(); + + child.make_assertion_with_delay(delay).is_alive(); + child + .kill() + .make_assertion_with_delay(delay) + .with_current_output() + .stdout_only(expected_stdout); + } +} + +/// The expected test outputs come from gnu's tail. +#[test] +#[cfg(disable_until_fixed)] +fn test_follow_when_files_are_pointing_to_same_relative_file_and_data_is_appended() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let relative_path_name = "data"; + + fixtures.write(relative_path_name, file_data); + let absolute_path = fixtures.plus("data").canonicalize().unwrap(); + + // run with relative path first and then the absolute path + let mut child = scene + .ucmd() + .args(&[ + "--follow=name", + relative_path_name, + absolute_path.to_str().unwrap(), + ]) + .run_no_wait(); + + let more_data = "more data"; + child.delay(500); + + fixtures.append(relative_path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}", + relative_path_name, + file_data, + absolute_path.to_str().unwrap(), + more_data + ); + + child + .make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_only(expected_stdout); + + // run with absolute path first and then the relative path + fixtures.write(relative_path_name, file_data); + let mut child = scene + .ucmd() + .args(&[ + "--follow=name", + absolute_path.to_str().unwrap(), + relative_path_name, + ]) + .run_no_wait(); + + child.delay(500); + let more_data = "more data"; + fixtures.append(relative_path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}", + absolute_path.to_str().unwrap(), + file_data, + relative_path_name, + more_data + ); + + child + .make_assertion_with_delay(DEFAULT_SLEEP_INTERVAL_MILLIS) + .is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +/// The expected test outputs come from gnu's tail. +#[test] +#[cfg(disable_until_fixed)] +fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_is_truncated() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let relative_path_name = "data"; + + fixtures.write(relative_path_name, file_data); + let absolute_path = fixtures.plus("data").canonicalize().unwrap(); + + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + relative_path_name, + absolute_path.to_str().unwrap(), + ]) + .stderr_to_stdout() + .run_no_wait(); + + child.delay(500); + let less_data = "less"; + fixtures.write(relative_path_name, "less"); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}{4}: {0}: file truncated\n\ + \n\ + ==> {0} <==\n\ + {3}", + relative_path_name, // 0 + file_data, // 1 + absolute_path.to_str().unwrap(), // 2 + less_data, // 3 + scene.util_name // 4 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +/// The expected test outputs come from gnu's tail. +#[test] +#[cfg(unix)] +#[cfg(disable_until_fixed)] +fn test_follow_when_file_and_symlink_are_pointing_to_same_file_and_append_data() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let path_name = "data"; + let link_name = "link"; + + fixtures.write(path_name, file_data); + fixtures.symlink_file(path_name, link_name); + + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + path_name, + link_name, + ]) + .run_no_wait(); + + child.delay(500); + let more_data = "more data"; + fixtures.append(path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}\n\ + ==> {2} <==\n\ + {3}", + path_name, // 0 + file_data, // 1 + link_name, // 2 + more_data, // 3 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); + + fixtures.write(path_name, file_data); + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + link_name, + path_name, + ]) + .run_no_wait(); + + child.delay(500); + let more_data = "more data"; + fixtures.append(path_name, more_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}\n\ + ==> {0} <==\n\ + {3}\n\ + ==> {2} <==\n\ + {3}", + link_name, // 0 + file_data, // 1 + path_name, // 2 + more_data, // 3 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +// Fails with: +// 'Assertion failed. Expected 'tail' to be running but exited with status=exit status: 1. +// stdout: +// stderr: tail: warning: --retry ignored; --retry is useful only when following +// tail: error reading 'dir': Is a directory +// ' +#[test] +#[cfg(disabled_until_fixed)] +fn test_args_when_directory_given_shorthand_big_f_together_with_retry() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let dirname = "dir"; + fixtures.mkdir(dirname); + let expected_stderr = format!( + "tail: error reading '{0}': Is a directory\n\ + tail: {0}: cannot follow end of this type of file\n", + dirname + ); + + let mut child = scene.ucmd().args(&["-F", "--retry", "dir"]).run_no_wait(); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stderr_only(expected_stderr); +} + +/// Fails on macos sometimes with +/// Diff < left / right > : +/// ==> data <== +/// file data +/// ==> /absolute/path/to/data <== +/// file data +/// +/// Fails on windows with +/// Diff < left / right > : +// ==> data <== +// file data +// ==> \\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpi6lNnX\data <== +// >file data +// < +// +// Fails on freebsd with +// Diff < left / right > : +// ==> data <== +// file data +// ==> /tmp/.tmpZPXPlS/data <== +// >file data +// < +#[test] +#[cfg(all( + not(target_vendor = "apple"), + not(target_os = "windows"), + not(target_os = "freebsd") +))] +fn test_follow_when_files_are_pointing_to_same_relative_file_and_file_stays_same_size() { + let scene = TestScenario::new(util_name!()); + let fixtures = &scene.fixtures; + + let file_data = "file data"; + let relative_path_name = "data"; + + fixtures.write(relative_path_name, file_data); + let absolute_path = scene.fixtures.plus("data").canonicalize().unwrap(); + + let mut child = scene + .ucmd() + .args(&[ + "--follow=descriptor", + "--max-unchanged-stats=1", + "--sleep-interval=0.1", + relative_path_name, + absolute_path.to_str().unwrap(), + ]) + .run_no_wait(); + + child.delay(500); + let same_data = "same data"; // equal size to file_data + fixtures.write(relative_path_name, same_data); + + let expected_stdout = format!( + "==> {0} <==\n\ + {1}\n\ + ==> {2} <==\n\ + {1}", + relative_path_name, // 0 + file_data, // 1 + absolute_path.to_str().unwrap(), // 2 + ); + + child.make_assertion_with_delay(500).is_alive(); + child + .kill() + .make_assertion() + .with_current_output() + .stdout_only(expected_stdout); +} + +#[test] +#[cfg(disable_until_fixed)] +fn test_args_sleep_interval_when_illegal_argument_then_usage_error() { + let scene = TestScenario::new(util_name!()); + for interval in [ + &format!("{}0", f64::MAX), + &format!("{}0.0", f64::MAX), + "1_000", + ".", + "' '", + "", + " ", + "0,0", + "one.zero", + ".zero", + "one.", + "0..0", + ] { + scene + .ucmd() + .args(&["--sleep-interval", interval]) + .run() + .usage_error(format!("invalid number of seconds: '{}'", interval)) + .code_is(1); + } +} diff --git a/tests/common/macros.rs b/tests/common/macros.rs index 108bc0fb7..4f5965d5a 100644 --- a/tests/common/macros.rs +++ b/tests/common/macros.rs @@ -3,7 +3,7 @@ // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -/// Platform-independent helper for constructing a PathBuf from individual elements +/// Platform-independent helper for constructing a `PathBuf` from individual elements #[macro_export] macro_rules! path_concat { ($e:expr, ..$n:expr) => {{ diff --git a/tests/common/util.rs b/tests/common/util.rs index 85c0650a3..c0d6b7df8 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -147,7 +147,7 @@ impl CmdResult { } /// Returns the program's exit code - /// Panics if not run or has not finished yet for example when run with run_no_wait() + /// Panics if not run or has not finished yet for example when run with `run_no_wait()` pub fn code(&self) -> i32 { self.code .expect("Program must be run first or has not finished, yet") @@ -158,7 +158,7 @@ impl CmdResult { self } - /// Returns the program's TempDir + /// Returns the program's `TempDir` /// Panics if not present pub fn tmpd(&self) -> Rc { match &self.tmpd { @@ -201,7 +201,7 @@ impl CmdResult { } /// asserts that the command resulted in empty (zero-length) stderr stream output - /// generally, it's better to use stdout_only() instead, + /// generally, it's better to use `stdout_only()` instead, /// but you might find yourself using this function if /// 1. you can not know exactly what stdout will be or /// 2. you know that stdout will also be empty @@ -215,8 +215,8 @@ impl CmdResult { } /// asserts that the command resulted in empty (zero-length) stderr stream output - /// unless asserting there was neither stdout or stderr, stderr_only is usually a better choice - /// generally, it's better to use stderr_only() instead, + /// unless asserting there was neither stdout or stderr, `stderr_only` is usually a better choice + /// generally, it's better to use `stderr_only()` instead, /// but you might find yourself using this function if /// 1. you can not know exactly what stderr will be or /// 2. you know that stderr will also be empty @@ -236,7 +236,7 @@ impl CmdResult { /// asserts that the command resulted in stdout stream output that equals the /// passed in value, trailing whitespace are kept to force strict comparison (#1235) - /// stdout_only is a better choice unless stderr may or will be non-empty + /// `stdout_only()` is a better choice unless stderr may or will be non-empty pub fn stdout_is>(&self, msg: T) -> &Self { assert_eq!(self.stdout_str(), String::from(msg.as_ref())); self @@ -244,13 +244,12 @@ impl CmdResult { /// like `stdout_is`, but succeeds if any elements of `expected` matches stdout. pub fn stdout_is_any + std::fmt::Debug>(&self, expected: &[T]) -> &Self { - if !expected.iter().any(|msg| self.stdout_str() == msg.as_ref()) { - panic!( - "stdout was {}\nExpected any of {:#?}", - self.stdout_str(), - expected - ); - } + assert!( + expected.iter().any(|msg| self.stdout_str() == msg.as_ref()), + "stdout was {}\nExpected any of {:#?}", + self.stdout_str(), + expected + ); self } @@ -268,7 +267,7 @@ impl CmdResult { self } - /// like stdout_is(...), but expects the contents of the file at the provided relative path + /// like `stdout_is()`, but expects the contents of the file at the provided relative path pub fn stdout_is_fixture>(&self, file_rel_path: T) -> &Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_is(String::from_utf8(contents).unwrap()) @@ -295,7 +294,7 @@ impl CmdResult { self.stdout_is_bytes(contents) } - /// like stdout_is_fixture(...), but replaces the data in fixture file based on values provided in template_vars + /// like `stdout_is_fixture()`, but replaces the data in fixture file based on values provided in `template_vars` /// command output pub fn stdout_is_templated_fixture>( &self, @@ -329,7 +328,7 @@ impl CmdResult { /// asserts that the command resulted in stderr stream output that equals the /// passed in value, when both are trimmed of trailing whitespace - /// stderr_only is a better choice unless stdout may or will be non-empty + /// `stderr_only` is a better choice unless stdout may or will be non-empty pub fn stderr_is>(&self, msg: T) -> &Self { assert_eq!( self.stderr_str().trim_end(), @@ -345,7 +344,7 @@ impl CmdResult { self } - /// Like stdout_is_fixture, but for stderr + /// Like `stdout_is_fixture`, but for stderr pub fn stderr_is_fixture>(&self, file_rel_path: T) -> &Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stderr_is(String::from_utf8(contents).unwrap()) @@ -367,7 +366,7 @@ impl CmdResult { self.no_stderr().stdout_is_bytes(msg) } - /// like stdout_only(...), but expects the contents of the file at the provided relative path + /// like `stdout_only()`, but expects the contents of the file at the provided relative path pub fn stdout_only_fixture>(&self, file_rel_path: T) -> &Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.stdout_only_bytes(contents) @@ -399,8 +398,8 @@ impl CmdResult { /// 1. the command resulted in stderr stream output that equals the /// the following format when both are trimmed of trailing whitespace /// `"{util_name}: {msg}\nTry '{bin_path} {util_name} --help' for more information."` - /// This the expected format when a UUsageError is returned or when show_error! is called - /// `msg` should be the same as the one provided to UUsageError::new or show_error! + /// This the expected format when a `UUsageError` is returned or when `show_error!` is called + /// `msg` should be the same as the one provided to `UUsageError::new` or `show_error!` /// /// 2. the command resulted in empty (zero-length) stdout stream output pub fn usage_error>(&self, msg: T) -> &Self { @@ -448,16 +447,20 @@ impl CmdResult { } pub fn stdout_matches(&self, regex: ®ex::Regex) -> &Self { - if !regex.is_match(self.stdout_str().trim()) { - panic!("Stdout does not match regex:\n{}", self.stdout_str()); - } + assert!( + regex.is_match(self.stdout_str().trim()), + "Stdout does not match regex:\n{}", + self.stdout_str() + ); self } pub fn stdout_does_not_match(&self, regex: ®ex::Regex) -> &Self { - if regex.is_match(self.stdout_str().trim()) { - panic!("Stdout matches regex:\n{}", self.stdout_str()); - } + assert!( + !regex.is_match(self.stdout_str().trim()), + "Stdout matches regex:\n{}", + self.stdout_str() + ); self } } @@ -552,7 +555,7 @@ impl AtPath { let mut f = self.open(name); let mut contents = String::new(); f.read_to_string(&mut contents) - .unwrap_or_else(|e| panic!("Couldn't read {}: {}", name, e)); + .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); contents } @@ -560,20 +563,20 @@ impl AtPath { let mut f = self.open(name); let mut contents = Vec::new(); f.read_to_end(&mut contents) - .unwrap_or_else(|e| panic!("Couldn't read {}: {}", name, e)); + .unwrap_or_else(|e| panic!("Couldn't read {name}: {e}")); contents } pub fn write(&self, name: &str, contents: &str) { log_info("write(default)", self.plus_as_string(name)); std::fs::write(self.plus(name), contents) - .unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); + .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); } pub fn write_bytes(&self, name: &str, contents: &[u8]) { log_info("write(default)", self.plus_as_string(name)); std::fs::write(self.plus(name), contents) - .unwrap_or_else(|e| panic!("Couldn't write {}: {}", name, e)); + .unwrap_or_else(|e| panic!("Couldn't write {name}: {e}")); } pub fn append(&self, name: &str, contents: &str) { @@ -585,7 +588,7 @@ impl AtPath { .open(self.plus(name)) .unwrap(); f.write_all(contents.as_bytes()) - .unwrap_or_else(|e| panic!("Couldn't write(append) {}: {}", name, e)); + .unwrap_or_else(|e| panic!("Couldn't write(append) {name}: {e}")); } pub fn append_bytes(&self, name: &str, contents: &[u8]) { @@ -597,7 +600,7 @@ impl AtPath { .open(self.plus(name)) .unwrap(); f.write_all(contents) - .unwrap_or_else(|e| panic!("Couldn't write(append) to {}: {}", name, e)); + .unwrap_or_else(|e| panic!("Couldn't write(append) to {name}: {e}")); } pub fn truncate(&self, name: &str, contents: &str) { @@ -609,30 +612,29 @@ impl AtPath { .open(self.plus(name)) .unwrap(); f.write_all(contents.as_bytes()) - .unwrap_or_else(|e| panic!("Couldn't write(truncate) {}: {}", name, e)); + .unwrap_or_else(|e| panic!("Couldn't write(truncate) {name}: {e}")); } pub fn rename(&self, source: &str, target: &str) { let source = self.plus(source); let target = self.plus(target); - log_info("rename", format!("{:?} {:?}", source, target)); + log_info("rename", format!("{source:?} {target:?}")); std::fs::rename(&source, &target) - .unwrap_or_else(|e| panic!("Couldn't rename {:?} -> {:?}: {}", source, target, e)); + .unwrap_or_else(|e| panic!("Couldn't rename {source:?} -> {target:?}: {e}")); } pub fn remove(&self, source: &str) { let source = self.plus(source); - log_info("remove", format!("{:?}", source)); - std::fs::remove_file(&source) - .unwrap_or_else(|e| panic!("Couldn't remove {:?}: {}", source, e)); + log_info("remove", format!("{source:?}")); + std::fs::remove_file(&source).unwrap_or_else(|e| panic!("Couldn't remove {source:?}: {e}")); } pub fn copy(&self, source: &str, target: &str) { let source = self.plus(source); let target = self.plus(target); - log_info("copy", format!("{:?} {:?}", source, target)); + log_info("copy", format!("{source:?} {target:?}")); std::fs::copy(&source, &target) - .unwrap_or_else(|e| panic!("Couldn't copy {:?} -> {:?}: {}", source, target, e)); + .unwrap_or_else(|e| panic!("Couldn't copy {source:?} -> {target:?}: {e}")); } pub fn rmdir(&self, dir: &str) { @@ -959,7 +961,7 @@ impl UCommand { env_clear: bool, ) -> Self { let bin_path = bin_path.as_ref(); - let util_name = util_name.as_ref().map(|un| un.as_ref()); + let util_name = util_name.as_ref().map(std::convert::AsRef::as_ref); let mut ucmd = Self { tmpd: None, @@ -1079,7 +1081,7 @@ impl UCommand { self } - /// like pipe_in(...), but uses the contents of the file at the provided relative path as the piped in data + /// like `pipe_in()`, but uses the contents of the file at the provided relative path as the piped in data pub fn pipe_in_fixture>(&mut self, file_rel_path: S) -> &mut Self { let contents = read_scenario_fixture(&self.tmpd, file_rel_path); self.pipe_in(contents) @@ -1192,7 +1194,7 @@ impl UCommand { /// Spawns the command, feeding the passed in stdin, waits for the result /// and returns a command result. - /// It is recommended that, instead of this, you use a combination of pipe_in() + /// It is recommended that, instead of this, you use a combination of `pipe_in()` /// with succeeds() or fails() pub fn run_piped_stdin>>(&mut self, input: T) -> CmdResult { self.pipe_in(input).run() @@ -1216,7 +1218,7 @@ impl UCommand { pub fn get_full_fixture_path(&self, file_rel_path: &str) -> String { let tmpdir_path = self.tmpd.as_ref().unwrap().path(); - format!("{}/{}", tmpdir_path.to_str().unwrap(), file_rel_path) + format!("{}/{file_rel_path}", tmpdir_path.to_str().unwrap()) } } @@ -1229,7 +1231,7 @@ struct CapturedOutput { } impl CapturedOutput { - /// Creates a new instance of CapturedOutput + /// Creates a new instance of `CapturedOutput` fn new(output: tempfile::NamedTempFile) -> Self { Self { current_file: output.reopen().unwrap(), @@ -1411,7 +1413,7 @@ impl<'a> UChildAssertion<'a> { self.uchild.stderr_all() ), Ok(None) => {} - Err(error) => panic!("Assertion failed with error '{:?}'", error), + Err(error) => panic!("Assertion failed with error '{error:?}'"), } self @@ -1430,7 +1432,7 @@ impl<'a> UChildAssertion<'a> { self.uchild.stdout_all(), self.uchild.stderr_all()), Ok(_) => {}, - Err(error) => panic!("Assertion failed with error '{:?}'", error), + Err(error) => panic!("Assertion failed with error '{error:?}'"), } self @@ -1780,7 +1782,7 @@ impl UChild { match writer.write_all(&content).and_then(|_| writer.flush()) { Err(error) if !ignore_stdin_write_error => Err(io::Error::new( io::ErrorKind::Other, - format!("failed to write to stdin of child: {}", error), + format!("failed to write to stdin of child: {error}"), )), Ok(_) | Err(_) => Ok(()), } @@ -1832,7 +1834,7 @@ impl UChild { match stdin.write_all(&data.into()).and_then(|_| stdin.flush()) { Err(error) if !self.ignore_stdin_write_error => Err(io::Error::new( io::ErrorKind::Other, - format!("failed to write to stdin of child: {}", error), + format!("failed to write to stdin of child: {error}"), )), Ok(_) | Err(_) => Ok(()), } @@ -1894,7 +1896,7 @@ pub fn whoami() -> String { std::env::var("USER") .or_else(|_| std::env::var("USERNAME")) .unwrap_or_else(|e| { - println!("{}: {}, using \"nobody\" instead", UUTILS_WARNING, e); + println!("{UUTILS_WARNING}: {e}, using \"nobody\" instead"); "nobody".to_string() }) } @@ -1965,33 +1967,33 @@ pub fn check_coreutil_version( // id (GNU coreutils) 8.32.162-4eda let util_name = &host_name_for(util_name); - log_info("run", format!("{} --version", util_name)); + log_info("run", format!("{util_name} --version")); let version_check = match Command::new(util_name.as_ref()) .env("LC_ALL", "C") .arg("--version") .output() { Ok(s) => s, - Err(e) => return Err(format!("{}: '{}' {}", UUTILS_WARNING, util_name, e)), + Err(e) => return Err(format!("{UUTILS_WARNING}: '{util_name}' {e}")), }; std::str::from_utf8(&version_check.stdout).unwrap() .split('\n') .collect::>() .first() .map_or_else( - || Err(format!("{}: unexpected output format for reference coreutil: '{} --version'", UUTILS_WARNING, util_name)), + || Err(format!("{UUTILS_WARNING}: unexpected output format for reference coreutil: '{util_name} --version'")), |s| { - if s.contains(&format!("(GNU coreutils) {}", version_expected)) { - Ok(format!("{}: {}", UUTILS_INFO, s)) + if s.contains(&format!("(GNU coreutils) {version_expected}")) { + Ok(format!("{UUTILS_INFO}: {s}")) } else if s.contains("(GNU coreutils)") { let version_found = parse_coreutil_version(s); let version_expected = version_expected.parse::().unwrap_or_default(); if version_found > version_expected { - Ok(format!("{}: version for the reference coreutil '{}' is higher than expected; expected: {}, found: {}", UUTILS_INFO, util_name, version_expected, version_found)) + Ok(format!("{UUTILS_INFO}: version for the reference coreutil '{util_name}' is higher than expected; expected: {version_expected}, found: {version_found}")) } else { - Err(format!("{}: version for the reference coreutil '{}' does not match; expected: {}, found: {}", UUTILS_WARNING, util_name, version_expected, version_found)) } + Err(format!("{UUTILS_WARNING}: version for the reference coreutil '{util_name}' does not match; expected: {version_expected}, found: {version_found}")) } } else { - Err(format!("{}: no coreutils version string found for reference coreutils '{} --version'", UUTILS_WARNING, util_name)) + Err(format!("{UUTILS_WARNING}: no coreutils version string found for reference coreutils '{util_name} --version'")) } }, ) @@ -2125,10 +2127,10 @@ pub fn run_ucmd_as_root( Err("Cannot run non-interactive sudo".to_string()) } Ok(_output) => Err("\"sudo whoami\" didn't return \"root\"".to_string()), - Err(e) => Err(format!("{}: {}", UUTILS_WARNING, e)), + Err(e) => Err(format!("{UUTILS_WARNING}: {e}")), } } else { - Err(format!("{}: {}", UUTILS_INFO, "cannot run inside CI")) + Err(format!("{UUTILS_INFO}: {}", "cannot run inside CI")) } } @@ -2548,9 +2550,18 @@ mod tests { fn test_uchild_when_run_no_wait_with_a_non_blocking_util() { let ts = TestScenario::new("echo"); let mut child = ts.ucmd().arg("hello world").run_no_wait(); - child.delay(500); - // check `child.is_alive()` is working + // check `child.is_alive()` and `child.delay()` is working + let mut trials = 10; + while child.is_alive() { + if trials <= 0 { + panic!("Assertion failed: child process is still alive.") + } + + child.delay(500); + trials -= 1; + } + assert!(!child.is_alive()); // check `child.is_not_alive()` is working @@ -2599,9 +2610,9 @@ mod tests { at.touch("a/empty"); #[cfg(target_vendor = "apple")] - let delay: u64 = 1000; + let delay: u64 = 2000; #[cfg(not(target_vendor = "apple"))] - let delay: u64 = 500; + let delay: u64 = 1000; let yes = if cfg!(windows) { "y\r\n" } else { "y\n" }; @@ -2641,29 +2652,10 @@ mod tests { .with_exact_output(44, 0) .stdout_only(expected); - #[cfg(windows)] - let expected = "rm: descend into directory 'a'? \ - rm: remove regular empty file 'a\\empty'? \ - removed 'a\\empty'\n\ - rm: remove directory 'a'? \ - removed directory 'a'\n"; - #[cfg(unix)] - let expected = "rm: descend into directory 'a'? \ - rm: remove regular empty file 'a/empty'? \ - removed 'a/empty'\n\ - rm: remove directory 'a'? \ - removed directory 'a'\n"; + let expected = "removed directory 'a'\n"; child.write_in(yes); - child - .delay(delay) - .kill() - .make_assertion() - .is_not_alive() - .with_all_output() - .stdout_only(expected); - - child.wait().unwrap().no_stdout().no_stderr().success(); + child.wait().unwrap().stdout_only(expected).success(); } #[cfg(feature = "tail")]