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

Merge pull request #8124 from sylvestre/l10n-tail

l10n: port tail for translation + add french
This commit is contained in:
Daniel Hofstetter 2025-06-09 14:48:31 +02:00 committed by GitHub
commit 07a95aaf50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 490 additions and 189 deletions

View file

@ -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

View file

@ -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

View file

@ -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::<String>(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<Optio
Err(USimpleError::new(
1,
match e {
parse::ParseError::OutOfRange => 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<Settings> {
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),
)

View file

@ -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(())
}

View file

@ -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

View file

@ -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 {

View file

@ -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";