From ea9f8f88d0a9a6250605515a9a57a9a4e8a6814f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sun, 8 Jun 2025 22:18:10 +0200 Subject: [PATCH] l10n: port tail for translation + add french --- src/uu/tail/locales/en-US.ftl | 68 ++++++- src/uu/tail/locales/fr-FR.ftl | 72 ++++++++ src/uu/tail/src/args.rs | 117 +++++++----- src/uu/tail/src/follow/watch.rs | 307 ++++++++++++++++++++------------ src/uu/tail/src/paths.rs | 7 +- src/uu/tail/src/tail.rs | 94 +++++++--- src/uu/tail/src/text.rs | 14 +- 7 files changed, 490 insertions(+), 189 deletions(-) create mode 100644 src/uu/tail/locales/fr-FR.ftl diff --git a/src/uu/tail/locales/en-US.ftl b/src/uu/tail/locales/en-US.ftl index 8d6394838..d4b670c49 100644 --- a/src/uu/tail/locales/en-US.ftl +++ b/src/uu/tail/locales/en-US.ftl @@ -1,6 +1,72 @@ tail-about = Print the last 10 lines of each FILE to standard output. With more than one FILE, precede each with a header giving the file name. With no FILE, or when FILE is -, read standard input. - Mandatory arguments to long flags are mandatory for short flags too. tail-usage = tail [FLAG]... [FILE]... + +# Help messages +tail-help-bytes = Number of bytes to print +tail-help-follow = Print the file as it grows +tail-help-lines = Number of lines to print +tail-help-pid = With -f, terminate after process ID, PID dies +tail-help-quiet = Never output headers giving file names +tail-help-sleep-interval = Number of seconds to sleep between polling the file when running with -f +tail-help-max-unchanged-stats = Reopen a FILE which has not changed size after N (default 5) iterations to see if it has been unlinked or renamed (this is the usual case of rotated log files); This option is meaningful only when polling (i.e., with --use-polling) and when --follow=name +tail-help-verbose = Always output headers giving file names +tail-help-zero-terminated = Line delimiter is NUL, not newline +tail-help-retry = Keep trying to open a file if it is inaccessible +tail-help-follow-retry = Same as --follow=name --retry +tail-help-polling-linux = Disable 'inotify' support and use polling instead +tail-help-polling-unix = Disable 'kqueue' support and use polling instead +tail-help-polling-windows = Disable 'ReadDirectoryChanges' support and use polling instead + +# Error messages +tail-error-cannot-follow-stdin-by-name = cannot follow { $stdin } by name +tail-error-cannot-open-no-such-file = cannot open '{ $file }' for reading: { $error } +tail-error-reading-file = error reading '{ $file }': { $error } +tail-error-cannot-follow-file-type = { $file }: cannot follow end of this type of file{ $msg } +tail-error-cannot-open-for-reading = cannot open '{ $file }' for reading +tail-error-cannot-fstat = cannot fstat { $file }: { $error } +tail-error-invalid-number-of-bytes = invalid number of bytes: { $arg } +tail-error-invalid-number-of-lines = invalid number of lines: { $arg } +tail-error-invalid-number-of-seconds = invalid number of seconds: '{ $source }' +tail-error-invalid-max-unchanged-stats = invalid maximum number of unchanged stats between opens: { $value } +tail-error-invalid-pid = invalid PID: { $pid } +tail-error-invalid-pid-with-error = invalid PID: { $pid }: { $error } +tail-error-invalid-number-out-of-range = invalid number: { $arg }: Numerical result out of range +tail-error-invalid-number-overflow = invalid number: { $arg } +tail-error-option-used-in-invalid-context = option used in invalid context -- { $option } +tail-error-bad-argument-encoding = bad argument encoding: '{ $arg }' +tail-error-cannot-watch-parent-directory = cannot watch parent directory of { $path } +tail-error-backend-cannot-be-used-too-many-files = { $backend } cannot be used, reverting to polling: Too many open files +tail-error-backend-resources-exhausted = { $backend } resources exhausted +tail-error-notify-error = NotifyError: { $error } +tail-error-recv-timeout-error = RecvTimeoutError: { $error } + +# Warning messages +tail-warning-retry-ignored = --retry ignored; --retry is useful only when following +tail-warning-retry-only-effective = --retry only effective for the initial open +tail-warning-pid-ignored = PID ignored; --pid=PID is useful only when following +tail-warning-pid-not-supported = --pid=PID is not supported on this system +tail-warning-following-stdin-ineffective = following standard input indefinitely is ineffective + +# Status messages +tail-status-has-become-accessible = { $file } has become accessible +tail-status-has-appeared-following-new-file = { $file } has appeared; following new file +tail-status-has-been-replaced-following-new-file = { $file } has been replaced; following new file +tail-status-file-truncated = { $file }: file truncated +tail-status-replaced-with-untailable-file = { $file } has been replaced with an untailable file +tail-status-replaced-with-untailable-file-giving-up = { $file } has been replaced with an untailable file; giving up on this name +tail-status-file-became-inaccessible = { $file } { $become_inaccessible }: { $no_such_file } +tail-status-directory-containing-watched-file-removed = directory containing watched file was removed +tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } cannot be used, reverting to polling +tail-status-file-no-such-file = { $file }: { $no_such_file } + +# Text constants +tail-bad-fd = Bad file descriptor +tail-no-such-file-or-directory = No such file or directory +tail-is-a-directory = Is a directory +tail-giving-up-on-this-name = ; giving up on this name +tail-stdin-header = standard input +tail-no-files-remaining = no files remaining +tail-become-inaccessible = has become inaccessible diff --git a/src/uu/tail/locales/fr-FR.ftl b/src/uu/tail/locales/fr-FR.ftl new file mode 100644 index 000000000..9a12f52eb --- /dev/null +++ b/src/uu/tail/locales/fr-FR.ftl @@ -0,0 +1,72 @@ +tail-about = Afficher les 10 dernières lignes de chaque FICHIER sur la sortie standard. + Avec plus d'un FICHIER, précéder chacun d'un en-tête donnant le nom du fichier. + Sans FICHIER, ou quand FICHIER est -, lire l'entrée standard. + Les arguments obligatoires pour les drapeaux longs sont également obligatoires pour les drapeaux courts. +tail-usage = tail [DRAPEAU]... [FICHIER]... + +# Messages d'aide +tail-help-bytes = Nombre d'octets à afficher +tail-help-follow = Afficher le fichier au fur et à mesure de sa croissance +tail-help-lines = Nombre de lignes à afficher +tail-help-pid = Avec -f, terminer après que l'ID de processus, PID meure +tail-help-quiet = Ne jamais afficher d'en-têtes donnant les noms de fichiers +tail-help-sleep-interval = Nombre de secondes à attendre entre les sondages du fichier lors de l'exécution avec -f +tail-help-max-unchanged-stats = Rouvrir un FICHIER qui n'a pas changé de taille après N (par défaut 5) itérations pour voir s'il a été supprimé ou renommé (c'est le cas habituel des fichiers journaux pivotés) ; Cette option n'a de sens que lors du sondage (c'est-à-dire avec --use-polling) et quand --follow=name +tail-help-verbose = Toujours afficher des en-têtes donnant les noms de fichiers +tail-help-zero-terminated = Le délimiteur de ligne est NUL, pas newline +tail-help-retry = Continuer d'essayer d'ouvrir un fichier s'il est inaccessible +tail-help-follow-retry = Identique à --follow=name --retry +tail-help-polling-linux = Désactiver le support 'inotify' et utiliser le sondage à la place +tail-help-polling-unix = Désactiver le support 'kqueue' et utiliser le sondage à la place +tail-help-polling-windows = Désactiver le support 'ReadDirectoryChanges' et utiliser le sondage à la place + +# Messages d'erreur +tail-error-cannot-follow-stdin-by-name = impossible de suivre { $stdin } par nom +tail-error-cannot-open-no-such-file = impossible d'ouvrir '{ $file }' en lecture : { $error } +tail-error-reading-file = erreur de lecture de '{ $file }' : { $error } +tail-error-cannot-follow-file-type = { $file } : impossible de suivre la fin de ce type de fichier{ $msg } +tail-error-cannot-open-for-reading = impossible d'ouvrir '{ $file }' en lecture +tail-error-cannot-fstat = impossible de faire fstat { $file } : { $error } +tail-error-invalid-number-of-bytes = nombre d'octets invalide : { $arg } +tail-error-invalid-number-of-lines = nombre de lignes invalide : { $arg } +tail-error-invalid-number-of-seconds = nombre de secondes invalide : '{ $source }' +tail-error-invalid-max-unchanged-stats = nombre maximum invalide de statistiques inchangées entre les ouvertures : { $value } +tail-error-invalid-pid = PID invalide : { $pid } +tail-error-invalid-pid-with-error = PID invalide : { $pid } : { $error } +tail-error-invalid-number-out-of-range = nombre invalide : { $arg } : Résultat numérique hors limites +tail-error-invalid-number-overflow = nombre invalide : { $arg } +tail-error-option-used-in-invalid-context = option utilisée dans un contexte invalide -- { $option } +tail-error-bad-argument-encoding = encodage d'argument incorrect : '{ $arg }' +tail-error-cannot-watch-parent-directory = impossible de surveiller le répertoire parent de { $path } +tail-error-backend-cannot-be-used-too-many-files = { $backend } ne peut pas être utilisé, retour au sondage : Trop de fichiers ouverts +tail-error-backend-resources-exhausted = ressources { $backend } épuisées +tail-error-notify-error = Erreur de notification : { $error } +tail-error-recv-timeout-error = Erreur de délai de réception : { $error } + +# Messages d'avertissement +tail-warning-retry-ignored = --retry ignoré ; --retry n'est utile que lors du suivi +tail-warning-retry-only-effective = --retry n'est effectif que pour l'ouverture initiale +tail-warning-pid-ignored = PID ignoré ; --pid=PID n'est utile que lors du suivi +tail-warning-pid-not-supported = --pid=PID n'est pas pris en charge sur ce système +tail-warning-following-stdin-ineffective = suivre l'entrée standard indéfiniment est inefficace + +# Messages de statut +tail-status-has-become-accessible = { $file } est devenu accessible +tail-status-has-appeared-following-new-file = { $file } est apparu ; suivi du nouveau fichier +tail-status-has-been-replaced-following-new-file = { $file } a été remplacé ; suivi du nouveau fichier +tail-status-file-truncated = { $file } : fichier tronqué +tail-status-replaced-with-untailable-file = { $file } a été remplacé par un fichier non suivable +tail-status-replaced-with-untailable-file-giving-up = { $file } a été remplacé par un fichier non suivable ; abandon de ce nom +tail-status-file-became-inaccessible = { $file } { $become_inaccessible } : { $no_such_file } +tail-status-directory-containing-watched-file-removed = le répertoire contenant le fichier surveillé a été supprimé +tail-status-backend-cannot-be-used-reverting-to-polling = { $backend } ne peut pas être utilisé, retour au sondage +tail-status-file-no-such-file = { $file } : { $no_such_file } + +# Constantes de texte +tail-bad-fd = Descripteur de fichier incorrect +tail-no-such-file-or-directory = Aucun fichier ou répertoire de ce type +tail-is-a-directory = Est un répertoire +tail-giving-up-on-this-name = ; abandon de ce nom +tail-stdin-header = entrée standard +tail-no-files-remaining = aucun fichier restant +tail-become-inaccessible = est devenu inaccessible diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 374beffd0..e4e1c4224 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -9,6 +9,7 @@ use crate::paths::Input; use crate::{Quotable, parse, platform}; use clap::{Arg, ArgAction, ArgMatches, Command, value_parser}; use same_file::Handle; +use std::collections::HashMap; use std::ffi::OsString; use std::io::IsTerminal; use std::time::Duration; @@ -18,7 +19,7 @@ use uucore::parser::parse_time; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::{format_usage, show_warning}; -use uucore::locale::get_message; +use uucore::locale::{get_message, get_message_with_args}; pub mod options { pub mod verbosity { @@ -78,7 +79,10 @@ impl FilterMode { Err(e) => { return Err(USimpleError::new( 1, - format!("invalid number of bytes: '{e}'"), + get_message_with_args( + "tail-error-invalid-number-of-bytes", + HashMap::from([("arg".to_string(), format!("'{}'", e))]), + ), )); } } @@ -88,10 +92,13 @@ impl FilterMode { let delimiter = if zero_term { 0 } else { b'\n' }; Self::Lines(signum, delimiter) } - Err(e) => { + Err(_) => { return Err(USimpleError::new( 1, - format!("invalid number of lines: {e}"), + get_message_with_args( + "tail-error-invalid-number-of-lines", + HashMap::from([("arg".to_string(), arg.quote().to_string())]), + ), )); } } @@ -228,7 +235,13 @@ impl Settings { if let Some(source) = matches.get_one::(options::SLEEP_INT) { settings.sleep_sec = parse_time::from_str(source, false).map_err(|_| { - UUsageError::new(1, format!("invalid number of seconds: '{source}'")) + UUsageError::new( + 1, + get_message_with_args( + "tail-error-invalid-number-of-seconds", + HashMap::from([("source".to_string(), source.clone())]), + ), + ) })?; } @@ -238,9 +251,9 @@ impl Settings { Err(_) => { return Err(UUsageError::new( 1, - format!( - "invalid maximum number of unchanged stats between opens: {}", - s.quote() + get_message_with_args( + "tail-error-invalid-max-unchanged-stats", + HashMap::from([("value".to_string(), s.quote().to_string())]), ), )); } @@ -256,7 +269,10 @@ impl Settings { // NOTE: tail only accepts an unsigned pid return Err(USimpleError::new( 1, - format!("invalid PID: {}", pid_str.quote()), + get_message_with_args( + "tail-error-invalid-pid", + HashMap::from([("pid".to_string(), pid_str.quote().to_string())]), + ), )); } @@ -265,7 +281,13 @@ impl Settings { Err(e) => { return Err(USimpleError::new( 1, - format!("invalid PID: {}: {e}", pid_str.quote()), + get_message_with_args( + "tail-error-invalid-pid-with-error", + HashMap::from([ + ("pid".to_string(), pid_str.quote().to_string()), + ("error".to_string(), e.to_string()), + ]), + ), )); } } @@ -300,17 +322,17 @@ impl Settings { pub fn check_warnings(&self) { if self.retry { if self.follow.is_none() { - show_warning!("--retry ignored; --retry is useful only when following"); + show_warning!("{}", get_message("tail-warning-retry-ignored")); } else if self.follow == Some(FollowMode::Descriptor) { - show_warning!("--retry only effective for the initial open"); + show_warning!("{}", get_message("tail-warning-retry-only-effective")); } } if self.pid != 0 { if self.follow.is_none() { - show_warning!("PID ignored; --pid=PID is useful only when following"); + show_warning!("{}", get_message("tail-warning-pid-ignored")); } else if !platform::supports_pid_checks(self.pid) { - show_warning!("--pid=PID is not supported on this system"); + show_warning!("{}", get_message("tail-warning-pid-not-supported")); } } @@ -330,7 +352,10 @@ impl Settings { }); if !blocking_stdin && std::io::stdin().is_terminal() { - show_warning!("following standard input indefinitely is ineffective"); + show_warning!( + "{}", + get_message("tail-warning-following-stdin-ineffective") + ); } } } @@ -368,19 +393,26 @@ pub fn parse_obsolete(arg: &OsString, input: Option<&OsString>) -> UResult format!( - "invalid number: {}: Numerical result out of range", - arg_str.quote() + parse::ParseError::OutOfRange => get_message_with_args( + "tail-error-invalid-number-out-of-range", + HashMap::from([("arg".to_string(), arg_str.quote().to_string())]), + ), + parse::ParseError::Overflow => get_message_with_args( + "tail-error-invalid-number-overflow", + HashMap::from([("arg".to_string(), arg_str.quote().to_string())]), ), - parse::ParseError::Overflow => format!("invalid number: {}", arg_str.quote()), // this ensures compatibility to GNU's error message (as tested in misc/tail) - parse::ParseError::Context => format!( - "option used in invalid context -- {}", - arg_str.chars().nth(1).unwrap_or_default() + parse::ParseError::Context => get_message_with_args( + "tail-error-option-used-in-invalid-context", + HashMap::from([( + "option".to_string(), + arg_str.chars().nth(1).unwrap_or_default().to_string(), + )]), + ), + parse::ParseError::InvalidEncoding => get_message_with_args( + "tail-error-bad-argument-encoding", + HashMap::from([("arg".to_string(), arg_str.to_string())]), ), - parse::ParseError::InvalidEncoding => { - format!("bad argument encoding: '{arg_str}'") - } }, )) } @@ -455,11 +487,11 @@ pub fn parse_args(args: impl uucore::Args) -> UResult { pub fn uu_app() -> Command { #[cfg(target_os = "linux")] - const POLLING_HELP: &str = "Disable 'inotify' support and use polling instead"; + let polling_help = get_message("tail-help-polling-linux"); #[cfg(all(unix, not(target_os = "linux")))] - const POLLING_HELP: &str = "Disable 'kqueue' support and use polling instead"; + let polling_help = get_message("tail-help-polling-unix"); #[cfg(target_os = "windows")] - const POLLING_HELP: &str = "Disable 'ReadDirectoryChanges' support and use polling instead"; + let polling_help = get_message("tail-help-polling-windows"); Command::new(uucore::util_name()) .version(uucore::crate_version!()) @@ -472,7 +504,7 @@ pub fn uu_app() -> Command { .long(options::BYTES) .allow_hyphen_values(true) .overrides_with_all([options::BYTES, options::LINES]) - .help("Number of bytes to print"), + .help(get_message("tail-help-bytes")), ) .arg( Arg::new(options::FOLLOW) @@ -483,7 +515,7 @@ pub fn uu_app() -> Command { .require_equals(true) .value_parser(ShortcutValueParser::new(["descriptor", "name"])) .overrides_with(options::FOLLOW) - .help("Print the file as it grows"), + .help(get_message("tail-help-follow")), ) .arg( Arg::new(options::LINES) @@ -491,13 +523,13 @@ pub fn uu_app() -> Command { .long(options::LINES) .allow_hyphen_values(true) .overrides_with_all([options::BYTES, options::LINES]) - .help("Number of lines to print"), + .help(get_message("tail-help-lines")), ) .arg( Arg::new(options::PID) .long(options::PID) .value_name("PID") - .help("With -f, terminate after process ID, PID dies") + .help(get_message("tail-help-pid")) .overrides_with(options::PID), ) .arg( @@ -506,7 +538,7 @@ pub fn uu_app() -> Command { .long(options::verbosity::QUIET) .visible_alias("silent") .overrides_with_all([options::verbosity::QUIET, options::verbosity::VERBOSE]) - .help("Never output headers giving file names") + .help(get_message("tail-help-quiet")) .action(ArgAction::SetTrue), ) .arg( @@ -514,32 +546,27 @@ pub fn uu_app() -> Command { .short('s') .value_name("N") .long(options::SLEEP_INT) - .help("Number of seconds to sleep between polling the file when running with -f"), + .help(get_message("tail-help-sleep-interval")), ) .arg( Arg::new(options::MAX_UNCHANGED_STATS) .value_name("N") .long(options::MAX_UNCHANGED_STATS) - .help( - "Reopen a FILE which has not changed size after N (default 5) iterations \ - to see if it has been unlinked or renamed (this is the usual case of rotated \ - log files); This option is meaningful only when polling \ - (i.e., with --use-polling) and when --follow=name", - ), + .help(get_message("tail-help-max-unchanged-stats")), ) .arg( Arg::new(options::verbosity::VERBOSE) .short('v') .long(options::verbosity::VERBOSE) .overrides_with_all([options::verbosity::QUIET, options::verbosity::VERBOSE]) - .help("Always output headers giving file names") + .help(get_message("tail-help-verbose")) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::ZERO_TERM) .short('z') .long(options::ZERO_TERM) - .help("Line delimiter is NUL, not newline") + .help(get_message("tail-help-zero-terminated")) .action(ArgAction::SetTrue), ) .arg( @@ -547,20 +574,20 @@ pub fn uu_app() -> Command { .alias(options::DISABLE_INOTIFY_TERM) // NOTE: Used by GNU's test suite .alias("dis") // NOTE: Used by GNU's test suite .long(options::USE_POLLING) - .help(POLLING_HELP) + .help(polling_help) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::RETRY) .long(options::RETRY) - .help("Keep trying to open a file if it is inaccessible") + .help(get_message("tail-help-retry")) .overrides_with(options::RETRY) .action(ArgAction::SetTrue), ) .arg( Arg::new(options::FOLLOW_RETRY) .short('F') - .help("Same as --follow=name --retry") + .help(get_message("tail-help-follow-retry")) .overrides_with(options::FOLLOW_RETRY) .action(ArgAction::SetTrue), ) diff --git a/src/uu/tail/src/follow/watch.rs b/src/uu/tail/src/follow/watch.rs index 212ad63f9..87db5aa02 100644 --- a/src/uu/tail/src/follow/watch.rs +++ b/src/uu/tail/src/follow/watch.rs @@ -10,11 +10,13 @@ use crate::follow::files::{FileHandling, PathData}; use crate::paths::{Input, InputKind, MetadataExtTail, PathExtTail}; use crate::{platform, text}; use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind}; +use std::collections::HashMap; use std::io::BufRead; use std::path::{Path, PathBuf}; use std::sync::mpsc::{self, Receiver, channel}; use uucore::display::Quotable; use uucore::error::{UResult, USimpleError, set_exit_code}; +use uucore::locale::{get_message, get_message_with_args}; use uucore::show_error; pub struct WatcherRx { @@ -56,7 +58,10 @@ impl WatcherRx { } else { return Err(USimpleError::new( 1, - format!("cannot watch parent directory of {}", path.display()), + get_message_with_args( + "tail-error-cannot-watch-parent-directory", + HashMap::from([("path".to_string(), path.display().to_string())]), + ), )); }; } @@ -239,8 +244,11 @@ impl Observer { `sudo sysctl fs.inotify.max_user_instances=64` */ show_error!( - "{} cannot be used, reverting to polling: Too many open files", - text::BACKEND + "{}", + get_message_with_args( + "tail-error-backend-cannot-be-used-too-many-files", + HashMap::from([("backend".to_string(), text::BACKEND.to_string())]) + ) ); set_exit_code(1); self.use_polling = true; @@ -318,120 +326,172 @@ impl Observer { let display_name = self.files.get(event_path).display_name.clone(); match event.kind { - EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any | -MetadataKind::WriteTime) | ModifyKind::Data(DataChange::Any) | -ModifyKind::Name(RenameMode::To)) | -EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => { - if let Ok(new_md) = event_path.metadata() { - - let is_tailable = new_md.is_tailable(); - let pd = self.files.get(event_path); - if let Some(old_md) = &pd.metadata { - if is_tailable { - // We resume tracking from the start of the file, - // assuming it has been truncated to 0. This mimics GNU's `tail` - // behavior and is the usual truncation operation for log self.files. - if !old_md.is_tailable() { - show_error!( "{} has become accessible", display_name.quote()); - self.files.update_reader(event_path)?; - } else if pd.reader.is_none() { - show_error!( "{} has appeared; following new file", display_name.quote()); - self.files.update_reader(event_path)?; - } else if event.kind == EventKind::Modify(ModifyKind::Name(RenameMode::To)) - || (self.use_polling - && !old_md.file_id_eq(&new_md)) { - show_error!( "{} has been replaced; following new file", display_name.quote()); - self.files.update_reader(event_path)?; - } else if old_md.got_truncated(&new_md)? { - show_error!("{display_name}: file truncated"); - self.files.update_reader(event_path)?; - } - paths.push(event_path.clone()); - } else if !is_tailable && old_md.is_tailable() { - if pd.reader.is_some() { - self.files.reset_reader(event_path); - } else { - show_error!( - "{} has been replaced with an untailable file", - display_name.quote() - ); - } - } - } else if is_tailable { - show_error!( "{} has appeared; following new file", display_name.quote()); + EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any | MetadataKind::WriteTime) | ModifyKind::Data(DataChange::Any) | ModifyKind::Name(RenameMode::To)) | + EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => { + if let Ok(new_md) = event_path.metadata() { + let is_tailable = new_md.is_tailable(); + let pd = self.files.get(event_path); + if let Some(old_md) = &pd.metadata { + if is_tailable { + // We resume tracking from the start of the file, + // assuming it has been truncated to 0. This mimics GNU's `tail` + // behavior and is the usual truncation operation for log files. + if !old_md.is_tailable() { + show_error!( + "{}", + get_message_with_args( + "tail-status-has-become-accessible", + HashMap::from([("file".to_string(), display_name.quote().to_string())]) + ) + ); + self.files.update_reader(event_path)?; + } else if pd.reader.is_none() { + show_error!( + "{}", + get_message_with_args( + "tail-status-has-appeared-following-new-file", + HashMap::from([("file".to_string(), display_name.quote().to_string())]) + ) + ); + self.files.update_reader(event_path)?; + } else if event.kind == EventKind::Modify(ModifyKind::Name(RenameMode::To)) + || (self.use_polling && !old_md.file_id_eq(&new_md)) { + show_error!( + "{}", + get_message_with_args( + "tail-status-has-been-replaced-following-new-file", + HashMap::from([("file".to_string(), display_name.quote().to_string())]) + ) + ); + self.files.update_reader(event_path)?; + } else if old_md.got_truncated(&new_md)? { + show_error!( + "{}", + get_message_with_args( + "tail-status-file-truncated", + HashMap::from([("file".to_string(), display_name)]) + ) + ); self.files.update_reader(event_path)?; - paths.push(event_path.clone()); - } else if settings.retry { - if self.follow_descriptor() { - show_error!( - "{} has been replaced with an untailable file; giving up on this name", - display_name.quote() - ); - let _ = self.watcher_rx.as_mut().unwrap().watcher.unwatch(event_path); - self.files.remove(event_path); - if self.files.no_files_remaining(settings) { - return Err(USimpleError::new(1, text::NO_FILES_REMAINING)); - } - } else { - show_error!( - "{} has been replaced with an untailable file", - display_name.quote() - ); - } } - self.files.update_metadata(event_path, Some(new_md)); + paths.push(event_path.clone()); + } else if !is_tailable && old_md.is_tailable() { + if pd.reader.is_some() { + self.files.reset_reader(event_path); + } else { + show_error!( + "{}", + get_message_with_args( + "tail-status-replaced-with-untailable-file", + HashMap::from([("file".to_string(), display_name.quote().to_string())]) + ) + ); + } + } + } else if is_tailable { + show_error!( + "{}", + get_message_with_args( + "tail-status-has-appeared-following-new-file", + HashMap::from([("file".to_string(), display_name.quote().to_string())]) + ) + ); + self.files.update_reader(event_path)?; + paths.push(event_path.clone()); + } else if settings.retry { + if self.follow_descriptor() { + show_error!( + "{}", + get_message_with_args( + "tail-status-replaced-with-untailable-file-giving-up", + HashMap::from([("file".to_string(), display_name.quote().to_string())]) + ) + ); + let _ = self.watcher_rx.as_mut().unwrap().watcher.unwatch(event_path); + self.files.remove(event_path); + if self.files.no_files_remaining(settings) { + return Err(USimpleError::new(1, get_message("tail-no-files-remaining"))); + } + } else { + show_error!( + "{}", + get_message_with_args( + "tail-status-replaced-with-untailable-file", + HashMap::from([("file".to_string(), display_name.quote().to_string())]) + ) + ); + } } + self.files.update_metadata(event_path, Some(new_md)); } + } EventKind::Remove(RemoveKind::File | RemoveKind::Any) // | EventKind::Modify(ModifyKind::Name(RenameMode::Any)) | EventKind::Modify(ModifyKind::Name(RenameMode::From)) => { - if self.follow_name() { - if settings.retry { - if let Some(old_md) = self.files.get_mut_metadata(event_path) { - if old_md.is_tailable() && self.files.get(event_path).reader.is_some() { - show_error!( - "{} {}: {}", - display_name.quote(), - text::BECOME_INACCESSIBLE, - text::NO_SUCH_FILE - ); - } - } - if event_path.is_orphan() && !self.orphans.contains(event_path) { - show_error!("directory containing watched file was removed"); + if self.follow_name() { + if settings.retry { + if let Some(old_md) = self.files.get_mut_metadata(event_path) { + if old_md.is_tailable() && self.files.get(event_path).reader.is_some() { show_error!( - "{} cannot be used, reverting to polling", - text::BACKEND + "{}", + get_message_with_args( + "tail-status-file-became-inaccessible", + HashMap::from([ + ("file".to_string(), display_name.quote().to_string()), + ("become_inaccessible".to_string(), get_message("tail-become-inaccessible")), + ("no_such_file".to_string(), get_message("tail-no-such-file-or-directory")) + ]) + ) ); - self.orphans.push(event_path.clone()); - let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path); - } - } else { - show_error!("{display_name}: {}", text::NO_SUCH_FILE); - if !self.files.files_remaining() && self.use_polling { - // NOTE: GNU's tail exits here for `---disable-inotify` - return Err(USimpleError::new(1, text::NO_FILES_REMAINING)); } } - self.files.reset_reader(event_path); - } else if self.follow_descriptor_retry() { - // --retry only effective for the initial open - let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path); - self.files.remove(event_path); - } else if self.use_polling && event.kind == EventKind::Remove(RemoveKind::Any) { - /* - BUG: The watched file was removed. Since we're using Polling, this - could be a rename. We can't tell because `notify::PollWatcher` doesn't - recognize renames properly. - Ideally we want to call seek to offset 0 on the file handle. - But because we only have access to `PathData::reader` as `BufRead`, - we cannot seek to 0 with `BufReader::seek_relative`. - Also because we don't have the new name, we cannot work around this - by simply reopening the file. - */ + if event_path.is_orphan() && !self.orphans.contains(event_path) { + show_error!("{}", get_message("tail-status-directory-containing-watched-file-removed")); + show_error!( + "{}", + get_message_with_args( + "tail-status-backend-cannot-be-used-reverting-to-polling", + HashMap::from([("backend".to_string(), text::BACKEND.to_string())]) + ) + ); + self.orphans.push(event_path.clone()); + let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path); + } + } else { + show_error!( + "{}", + get_message_with_args( + "tail-status-file-no-such-file", + HashMap::from([ + ("file".to_string(), display_name), + ("no_such_file".to_string(), get_message("tail-no-such-file-or-directory")) + ]) + ) + ); + if !self.files.files_remaining() && self.use_polling { + // NOTE: GNU's tail exits here for `---disable-inotify` + return Err(USimpleError::new(1, get_message("tail-no-files-remaining"))); + } } + self.files.reset_reader(event_path); + } else if self.follow_descriptor_retry() { + // --retry only effective for the initial open + let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path); + self.files.remove(event_path); + } else if self.use_polling && event.kind == EventKind::Remove(RemoveKind::Any) { + /* + BUG: The watched file was removed. Since we're using Polling, this + could be a rename. We can't tell because `notify::PollWatcher` doesn't + recognize renames properly. + Ideally we want to call seek to offset 0 on the file handle. + But because we only have access to `PathData::reader` as `BufRead`, + we cannot seek to 0 with `BufReader::seek_relative`. + Also because we don't have the new name, we cannot work around this + by simply reopening the file. + */ } + } EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { /* NOTE: For `tail -f a`, keep tracking additions to b after `mv a b` @@ -475,7 +535,7 @@ EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => { #[allow(clippy::cognitive_complexity)] 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())); + return Err(USimpleError::new(1, get_message("tail-no-files-remaining"))); } let mut process = platform::ProcessChecker::new(observer.pid); @@ -504,8 +564,14 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { 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() + "{}", + get_message_with_args( + "tail-status-has-appeared-following-new-file", + HashMap::from([( + "file".to_string(), + pd.display_name.quote().to_string() + )]) + ) ); observer.files.update_metadata(new_path, Some(md)); observer.files.update_reader(new_path)?; @@ -528,6 +594,7 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { .unwrap() .receiver .recv_timeout(settings.sleep_sec); + if rx_result.is_ok() { timeout_counter = 0; } @@ -563,14 +630,33 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { })) => { return Err(USimpleError::new( 1, - format!("{} resources exhausted", text::BACKEND), + get_message_with_args( + "tail-error-backend-resources-exhausted", + HashMap::from([("backend".to_string(), text::BACKEND.to_string())]), + ), + )); + } + Ok(Err(e)) => { + return Err(USimpleError::new( + 1, + get_message_with_args( + "tail-error-notify-error", + HashMap::from([("error".to_string(), e.to_string())]), + ), )); } - Ok(Err(e)) => return Err(USimpleError::new(1, format!("NotifyError: {e}"))), Err(mpsc::RecvTimeoutError::Timeout) => { timeout_counter += 1; } - Err(e) => return Err(USimpleError::new(1, format!("RecvTimeoutError: {e}"))), + Err(e) => { + return Err(USimpleError::new( + 1, + get_message_with_args( + "tail-error-recv-timeout-error", + HashMap::from([("error".to_string(), e.to_string())]), + ), + )); + } } if observer.use_polling && settings.follow.is_some() { @@ -588,7 +674,7 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { if timeout_counter == settings.max_unchanged_stats { /* TODO: [2021-10; jhscheer] implement timeout_counter for each file. - ‘--max-unchanged-stats=n’ + '--max-unchanged-stats=n' When tailing a file by name, if there have been n (default n=5) consecutive iterations for which the file has not changed, then open/fstat the file to determine if that file name is still associated with the same device/inode-number pair as before. When @@ -599,5 +685,6 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { */ } } + Ok(()) } diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 5c56ff844..c2fc4f170 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -13,6 +13,7 @@ use std::io::{Seek, SeekFrom}; use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path::{Path, PathBuf}; use uucore::error::UResult; +use uucore::locale::get_message; #[derive(Debug, Clone)] pub enum InputKind { @@ -55,7 +56,7 @@ impl Input { let kind = string.into(); let display_name = match kind { InputKind::File(_) => string.to_string_lossy().to_string(), - InputKind::Stdin => text::STDIN_HEADER.to_string(), + InputKind::Stdin => get_message("tail-stdin-header"), }; Self { kind, display_name } @@ -106,7 +107,7 @@ impl Default for Input { fn default() -> Self { Self { kind: InputKind::Stdin, - display_name: String::from(text::STDIN_HEADER), + display_name: get_message("tail-stdin-header"), } } } @@ -221,7 +222,7 @@ impl PathExtTail for Path { fn is_stdin(&self) -> bool { self.eq(Self::new(text::DASH)) || self.eq(Self::new(text::DEV_STDIN)) - || self.eq(Self::new(text::STDIN_HEADER)) + || self.eq(Self::new(&get_message("tail-stdin-header"))) } /// Return true if `path` does not have an existing parent directory diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 2a6f9eb23..63dd7389b 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -29,11 +29,13 @@ use memchr::{memchr_iter, memrchr_iter}; use paths::{FileExtTail, HeaderPrinter, Input, InputKind, MetadataExtTail}; use same_file::Handle; use std::cmp::Ordering; +use std::collections::HashMap; use std::fs::File; use std::io::{self, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin, stdout}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, get_exit_code, set_exit_code}; +use uucore::locale::{get_message, get_message_with_args}; use uucore::{show, show_error}; #[uucore::main] @@ -46,7 +48,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { args::VerificationResult::CannotFollowStdinByName => { return Err(USimpleError::new( 1, - format!("cannot follow {} by name", text::DASH.quote()), + get_message_with_args( + "tail-error-cannot-follow-stdin-by-name", + HashMap::from([("stdin".to_string(), text::DASH.quote().to_string())]), + ), )); } // Exit early if we do not output anything. Note, that this may break a pipe @@ -95,7 +100,7 @@ fn uu_tail(settings: &Settings) -> UResult<()> { } if get_exit_code() > 0 && paths::stdin_is_bad_fd() { - show_error!("-: {}", text::BAD_FD); + show_error!("{}: {}", text::DASH, get_message("tail-bad-fd")); } Ok(()) @@ -112,27 +117,50 @@ fn tail_file( if !path.exists() { set_exit_code(1); show_error!( - "cannot open '{}' for reading: {}", - input.display_name, - text::NO_SUCH_FILE + "{}", + get_message_with_args( + "tail-error-cannot-open-no-such-file", + HashMap::from([ + ("file".to_string(), input.display_name.clone()), + ( + "error".to_string(), + get_message("tail-no-such-file-or-directory") + ) + ]) + ) ); observer.add_bad_path(path, input.display_name.as_str(), false)?; } else if path.is_dir() { set_exit_code(1); header_printer.print_input(input); - let err_msg = "Is a directory".to_string(); + let err_msg = get_message("tail-is-a-directory"); - show_error!("error reading '{}': {err_msg}", input.display_name); + show_error!( + "{}", + get_message_with_args( + "tail-error-reading-file", + HashMap::from([ + ("file".to_string(), input.display_name.clone()), + ("error".to_string(), err_msg) + ]) + ) + ); if settings.follow.is_some() { let msg = if settings.retry { "" } else { - "; giving up on this name" + &get_message("tail-giving-up-on-this-name") }; show_error!( - "{}: cannot follow end of this type of file{msg}", - input.display_name, + "{}", + get_message_with_args( + "tail-error-cannot-follow-file-type", + HashMap::from([ + ("file".to_string(), input.display_name.clone()), + ("msg".to_string(), msg.to_string()) + ]) + ) ); } if !observer.follow_name_retry() { @@ -166,13 +194,19 @@ fn tail_file( Err(e) if e.kind() == ErrorKind::PermissionDenied => { observer.add_bad_path(path, input.display_name.as_str(), false)?; show!(e.map_err_context(|| { - format!("cannot open '{}' for reading", input.display_name) + get_message_with_args( + "tail-error-cannot-open-for-reading", + HashMap::from([("file".to_string(), input.display_name.clone())]), + ) })); } Err(e) => { 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) + get_message_with_args( + "tail-error-cannot-open-for-reading", + HashMap::from([("file".to_string(), input.display_name.clone())]), + ) })); } } @@ -202,9 +236,17 @@ fn tail_stdin( if meta.file_type().is_dir() { set_exit_code(1); show_error!( - "cannot open '{}' for reading: {}", - input.display_name, - text::NO_SUCH_FILE + "{}", + get_message_with_args( + "tail-error-cannot-open-no-such-file", + HashMap::from([ + ("file".to_string(), input.display_name.clone()), + ( + "error".to_string(), + get_message("tail-no-such-file-or-directory") + ) + ]) + ) ); return Ok(()); } @@ -240,15 +282,25 @@ fn tail_stdin( if paths::stdin_is_bad_fd() { set_exit_code(1); show_error!( - "cannot fstat {}: {}", - text::STDIN_HEADER.quote(), - text::BAD_FD + "{}", + get_message_with_args( + "tail-error-cannot-fstat", + HashMap::from([ + ("file".to_string(), get_message("tail-stdin-header")), + ("error".to_string(), get_message("tail-bad-fd")) + ]) + ) ); if settings.follow.is_some() { show_error!( - "error reading {}: {}", - text::STDIN_HEADER.quote(), - text::BAD_FD + "{}", + get_message_with_args( + "tail-error-reading-file", + HashMap::from([ + ("file".to_string(), get_message("tail-stdin-header")), + ("error".to_string(), get_message("tail-bad-fd")) + ]) + ) ); } } else { diff --git a/src/uu/tail/src/text.rs b/src/uu/tail/src/text.rs index e7aa6c253..0c4972345 100644 --- a/src/uu/tail/src/text.rs +++ b/src/uu/tail/src/text.rs @@ -5,20 +5,16 @@ // spell-checker:ignore (ToDO) kqueue +// Non-localized constants (system paths and technical identifiers) pub const DASH: &str = "-"; pub const DEV_STDIN: &str = "/dev/stdin"; -pub const STDIN_HEADER: &str = "standard input"; -pub const NO_FILES_REMAINING: &str = "no files remaining"; -pub const NO_SUCH_FILE: &str = "No such file or directory"; -pub const BECOME_INACCESSIBLE: &str = "has become inaccessible"; -pub const BAD_FD: &str = "Bad file descriptor"; +pub const FD0: &str = "/dev/fd/0"; +pub const DEV_TTY: &str = "/dev/tty"; +pub const DEV_PTMX: &str = "/dev/ptmx"; + #[cfg(target_os = "linux")] pub const BACKEND: &str = "inotify"; #[cfg(all(unix, not(target_os = "linux")))] pub const BACKEND: &str = "kqueue"; #[cfg(target_os = "windows")] pub const BACKEND: &str = "ReadDirectoryChanges"; -pub const FD0: &str = "/dev/fd/0"; -pub const IS_A_DIRECTORY: &str = "Is a directory"; -pub const DEV_TTY: &str = "/dev/tty"; -pub const DEV_PTMX: &str = "/dev/ptmx";