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

l10n: port tail for translation + add french

This commit is contained in:
Sylvestre Ledru 2025-06-08 22:18:10 +02:00
parent 4128efad0f
commit ea9f8f88d0
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. 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 more than one FILE, precede each with a header giving the file name.
With no FILE, or when FILE is -, read standard input. With no FILE, or when FILE is -, read standard input.
Mandatory arguments to long flags are mandatory for short flags too. Mandatory arguments to long flags are mandatory for short flags too.
tail-usage = tail [FLAG]... [FILE]... 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 crate::{Quotable, parse, platform};
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser}; use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
use same_file::Handle; use same_file::Handle;
use std::collections::HashMap;
use std::ffi::OsString; use std::ffi::OsString;
use std::io::IsTerminal; use std::io::IsTerminal;
use std::time::Duration; use std::time::Duration;
@ -18,7 +19,7 @@ use uucore::parser::parse_time;
use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::parser::shortcut_value_parser::ShortcutValueParser;
use uucore::{format_usage, show_warning}; 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 options {
pub mod verbosity { pub mod verbosity {
@ -78,7 +79,10 @@ impl FilterMode {
Err(e) => { Err(e) => {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 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' }; let delimiter = if zero_term { 0 } else { b'\n' };
Self::Lines(signum, delimiter) Self::Lines(signum, delimiter)
} }
Err(e) => { Err(_) => {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 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) { if let Some(source) = matches.get_one::<String>(options::SLEEP_INT) {
settings.sleep_sec = parse_time::from_str(source, false).map_err(|_| { 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(_) => { Err(_) => {
return Err(UUsageError::new( return Err(UUsageError::new(
1, 1,
format!( get_message_with_args(
"invalid maximum number of unchanged stats between opens: {}", "tail-error-invalid-max-unchanged-stats",
s.quote() HashMap::from([("value".to_string(), s.quote().to_string())]),
), ),
)); ));
} }
@ -256,7 +269,10 @@ impl Settings {
// NOTE: tail only accepts an unsigned pid // NOTE: tail only accepts an unsigned pid
return Err(USimpleError::new( return Err(USimpleError::new(
1, 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) => { Err(e) => {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 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) { pub fn check_warnings(&self) {
if self.retry { if self.retry {
if self.follow.is_none() { 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) { } 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.pid != 0 {
if self.follow.is_none() { 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) { } 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() { 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( Err(USimpleError::new(
1, 1,
match e { match e {
parse::ParseError::OutOfRange => format!( parse::ParseError::OutOfRange => get_message_with_args(
"invalid number: {}: Numerical result out of range", "tail-error-invalid-number-out-of-range",
arg_str.quote() 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) // this ensures compatibility to GNU's error message (as tested in misc/tail)
parse::ParseError::Context => format!( parse::ParseError::Context => get_message_with_args(
"option used in invalid context -- {}", "tail-error-option-used-in-invalid-context",
arg_str.chars().nth(1).unwrap_or_default() 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 { pub fn uu_app() -> Command {
#[cfg(target_os = "linux")] #[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")))] #[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")] #[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()) Command::new(uucore::util_name())
.version(uucore::crate_version!()) .version(uucore::crate_version!())
@ -472,7 +504,7 @@ pub fn uu_app() -> Command {
.long(options::BYTES) .long(options::BYTES)
.allow_hyphen_values(true) .allow_hyphen_values(true)
.overrides_with_all([options::BYTES, options::LINES]) .overrides_with_all([options::BYTES, options::LINES])
.help("Number of bytes to print"), .help(get_message("tail-help-bytes")),
) )
.arg( .arg(
Arg::new(options::FOLLOW) Arg::new(options::FOLLOW)
@ -483,7 +515,7 @@ pub fn uu_app() -> Command {
.require_equals(true) .require_equals(true)
.value_parser(ShortcutValueParser::new(["descriptor", "name"])) .value_parser(ShortcutValueParser::new(["descriptor", "name"]))
.overrides_with(options::FOLLOW) .overrides_with(options::FOLLOW)
.help("Print the file as it grows"), .help(get_message("tail-help-follow")),
) )
.arg( .arg(
Arg::new(options::LINES) Arg::new(options::LINES)
@ -491,13 +523,13 @@ pub fn uu_app() -> Command {
.long(options::LINES) .long(options::LINES)
.allow_hyphen_values(true) .allow_hyphen_values(true)
.overrides_with_all([options::BYTES, options::LINES]) .overrides_with_all([options::BYTES, options::LINES])
.help("Number of lines to print"), .help(get_message("tail-help-lines")),
) )
.arg( .arg(
Arg::new(options::PID) Arg::new(options::PID)
.long(options::PID) .long(options::PID)
.value_name("PID") .value_name("PID")
.help("With -f, terminate after process ID, PID dies") .help(get_message("tail-help-pid"))
.overrides_with(options::PID), .overrides_with(options::PID),
) )
.arg( .arg(
@ -506,7 +538,7 @@ pub fn uu_app() -> Command {
.long(options::verbosity::QUIET) .long(options::verbosity::QUIET)
.visible_alias("silent") .visible_alias("silent")
.overrides_with_all([options::verbosity::QUIET, options::verbosity::VERBOSE]) .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), .action(ArgAction::SetTrue),
) )
.arg( .arg(
@ -514,32 +546,27 @@ pub fn uu_app() -> Command {
.short('s') .short('s')
.value_name("N") .value_name("N")
.long(options::SLEEP_INT) .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(
Arg::new(options::MAX_UNCHANGED_STATS) Arg::new(options::MAX_UNCHANGED_STATS)
.value_name("N") .value_name("N")
.long(options::MAX_UNCHANGED_STATS) .long(options::MAX_UNCHANGED_STATS)
.help( .help(get_message("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",
),
) )
.arg( .arg(
Arg::new(options::verbosity::VERBOSE) Arg::new(options::verbosity::VERBOSE)
.short('v') .short('v')
.long(options::verbosity::VERBOSE) .long(options::verbosity::VERBOSE)
.overrides_with_all([options::verbosity::QUIET, 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), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::ZERO_TERM) Arg::new(options::ZERO_TERM)
.short('z') .short('z')
.long(options::ZERO_TERM) .long(options::ZERO_TERM)
.help("Line delimiter is NUL, not newline") .help(get_message("tail-help-zero-terminated"))
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
@ -547,20 +574,20 @@ pub fn uu_app() -> Command {
.alias(options::DISABLE_INOTIFY_TERM) // NOTE: Used by GNU's test suite .alias(options::DISABLE_INOTIFY_TERM) // NOTE: Used by GNU's test suite
.alias("dis") // NOTE: Used by GNU's test suite .alias("dis") // NOTE: Used by GNU's test suite
.long(options::USE_POLLING) .long(options::USE_POLLING)
.help(POLLING_HELP) .help(polling_help)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::RETRY) Arg::new(options::RETRY)
.long(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) .overrides_with(options::RETRY)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
Arg::new(options::FOLLOW_RETRY) Arg::new(options::FOLLOW_RETRY)
.short('F') .short('F')
.help("Same as --follow=name --retry") .help(get_message("tail-help-follow-retry"))
.overrides_with(options::FOLLOW_RETRY) .overrides_with(options::FOLLOW_RETRY)
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )

View file

@ -10,11 +10,13 @@ use crate::follow::files::{FileHandling, PathData};
use crate::paths::{Input, InputKind, MetadataExtTail, PathExtTail}; use crate::paths::{Input, InputKind, MetadataExtTail, PathExtTail};
use crate::{platform, text}; use crate::{platform, text};
use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind}; use notify::{RecommendedWatcher, RecursiveMode, Watcher, WatcherKind};
use std::collections::HashMap;
use std::io::BufRead; use std::io::BufRead;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, channel}; use std::sync::mpsc::{self, Receiver, channel};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError, set_exit_code}; use uucore::error::{UResult, USimpleError, set_exit_code};
use uucore::locale::{get_message, get_message_with_args};
use uucore::show_error; use uucore::show_error;
pub struct WatcherRx { pub struct WatcherRx {
@ -56,7 +58,10 @@ impl WatcherRx {
} else { } else {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 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` `sudo sysctl fs.inotify.max_user_instances=64`
*/ */
show_error!( 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); set_exit_code(1);
self.use_polling = true; self.use_polling = true;
@ -318,32 +326,52 @@ impl Observer {
let display_name = self.files.get(event_path).display_name.clone(); let display_name = self.files.get(event_path).display_name.clone();
match event.kind { match event.kind {
EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any | EventKind::Modify(ModifyKind::Metadata(MetadataKind::Any | MetadataKind::WriteTime) | ModifyKind::Data(DataChange::Any) | ModifyKind::Name(RenameMode::To)) |
MetadataKind::WriteTime) | ModifyKind::Data(DataChange::Any) |
ModifyKind::Name(RenameMode::To)) |
EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => { EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => {
if let Ok(new_md) = event_path.metadata() { if let Ok(new_md) = event_path.metadata() {
let is_tailable = new_md.is_tailable(); let is_tailable = new_md.is_tailable();
let pd = self.files.get(event_path); let pd = self.files.get(event_path);
if let Some(old_md) = &pd.metadata { if let Some(old_md) = &pd.metadata {
if is_tailable { if is_tailable {
// We resume tracking from the start of the file, // We resume tracking from the start of the file,
// assuming it has been truncated to 0. This mimics GNU's `tail` // assuming it has been truncated to 0. This mimics GNU's `tail`
// behavior and is the usual truncation operation for log self.files. // behavior and is the usual truncation operation for log files.
if !old_md.is_tailable() { if !old_md.is_tailable() {
show_error!( "{} has become accessible", display_name.quote()); 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)?; self.files.update_reader(event_path)?;
} else if pd.reader.is_none() { } else if pd.reader.is_none() {
show_error!( "{} has appeared; following new file", display_name.quote()); 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)?; self.files.update_reader(event_path)?;
} else if event.kind == EventKind::Modify(ModifyKind::Name(RenameMode::To)) } else if event.kind == EventKind::Modify(ModifyKind::Name(RenameMode::To))
|| (self.use_polling || (self.use_polling && !old_md.file_id_eq(&new_md)) {
&& !old_md.file_id_eq(&new_md)) { show_error!(
show_error!( "{} has been replaced; following new file", display_name.quote()); "{}",
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)?; self.files.update_reader(event_path)?;
} else if old_md.got_truncated(&new_md)? { } else if old_md.got_truncated(&new_md)? {
show_error!("{display_name}: file truncated"); show_error!(
"{}",
get_message_with_args(
"tail-status-file-truncated",
HashMap::from([("file".to_string(), display_name)])
)
);
self.files.update_reader(event_path)?; self.files.update_reader(event_path)?;
} }
paths.push(event_path.clone()); paths.push(event_path.clone());
@ -352,30 +380,45 @@ EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => {
self.files.reset_reader(event_path); self.files.reset_reader(event_path);
} else { } else {
show_error!( show_error!(
"{} has been replaced with an untailable file", "{}",
display_name.quote() get_message_with_args(
"tail-status-replaced-with-untailable-file",
HashMap::from([("file".to_string(), display_name.quote().to_string())])
)
); );
} }
} }
} else if is_tailable { } else if is_tailable {
show_error!( "{} has appeared; following new file", display_name.quote()); 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)?; self.files.update_reader(event_path)?;
paths.push(event_path.clone()); paths.push(event_path.clone());
} else if settings.retry { } else if settings.retry {
if self.follow_descriptor() { if self.follow_descriptor() {
show_error!( show_error!(
"{} has been replaced with an untailable file; giving up on this name", "{}",
display_name.quote() 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); let _ = self.watcher_rx.as_mut().unwrap().watcher.unwatch(event_path);
self.files.remove(event_path); self.files.remove(event_path);
if self.files.no_files_remaining(settings) { if self.files.no_files_remaining(settings) {
return Err(USimpleError::new(1, text::NO_FILES_REMAINING)); return Err(USimpleError::new(1, get_message("tail-no-files-remaining")));
} }
} else { } else {
show_error!( show_error!(
"{} has been replaced with an untailable file", "{}",
display_name.quote() get_message_with_args(
"tail-status-replaced-with-untailable-file",
HashMap::from([("file".to_string(), display_name.quote().to_string())])
)
); );
} }
} }
@ -391,27 +434,44 @@ EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => {
if let Some(old_md) = self.files.get_mut_metadata(event_path) { 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() { if old_md.is_tailable() && self.files.get(event_path).reader.is_some() {
show_error!( show_error!(
"{} {}: {}", "{}",
display_name.quote(), get_message_with_args(
text::BECOME_INACCESSIBLE, "tail-status-file-became-inaccessible",
text::NO_SUCH_FILE 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"))
])
)
); );
} }
} }
if event_path.is_orphan() && !self.orphans.contains(event_path) { if event_path.is_orphan() && !self.orphans.contains(event_path) {
show_error!("directory containing watched file was removed"); show_error!("{}", get_message("tail-status-directory-containing-watched-file-removed"));
show_error!( show_error!(
"{} cannot be used, reverting to polling", "{}",
text::BACKEND 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()); self.orphans.push(event_path.clone());
let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path); let _ = self.watcher_rx.as_mut().unwrap().unwatch(event_path);
} }
} else { } else {
show_error!("{display_name}: {}", text::NO_SUCH_FILE); 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 { if !self.files.files_remaining() && self.use_polling {
// NOTE: GNU's tail exits here for `---disable-inotify` // NOTE: GNU's tail exits here for `---disable-inotify`
return Err(USimpleError::new(1, text::NO_FILES_REMAINING)); return Err(USimpleError::new(1, get_message("tail-no-files-remaining")));
} }
} }
self.files.reset_reader(event_path); self.files.reset_reader(event_path);
@ -475,7 +535,7 @@ EventKind::Create(CreateKind::File | CreateKind::Folder | CreateKind::Any) => {
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> { pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> {
if observer.files.no_files_remaining(settings) && !observer.files.only_stdin_remaining() { 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); 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(); let md = new_path.metadata().unwrap();
if md.is_tailable() && pd.reader.is_none() { if md.is_tailable() && pd.reader.is_none() {
show_error!( 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_metadata(new_path, Some(md));
observer.files.update_reader(new_path)?; observer.files.update_reader(new_path)?;
@ -528,6 +594,7 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> {
.unwrap() .unwrap()
.receiver .receiver
.recv_timeout(settings.sleep_sec); .recv_timeout(settings.sleep_sec);
if rx_result.is_ok() { if rx_result.is_ok() {
timeout_counter = 0; timeout_counter = 0;
} }
@ -563,14 +630,33 @@ pub fn follow(mut observer: Observer, settings: &Settings) -> UResult<()> {
})) => { })) => {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 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) => { Err(mpsc::RecvTimeoutError::Timeout) => {
timeout_counter += 1; 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() { 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 { if timeout_counter == settings.max_unchanged_stats {
/* /*
TODO: [2021-10; jhscheer] implement timeout_counter for each file. 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 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 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 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(()) Ok(())
} }

View file

@ -13,6 +13,7 @@ use std::io::{Seek, SeekFrom};
use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::unix::fs::{FileTypeExt, MetadataExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::error::UResult; use uucore::error::UResult;
use uucore::locale::get_message;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum InputKind { pub enum InputKind {
@ -55,7 +56,7 @@ impl Input {
let kind = string.into(); let kind = string.into();
let display_name = match kind { let display_name = match kind {
InputKind::File(_) => string.to_string_lossy().to_string(), 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 } Self { kind, display_name }
@ -106,7 +107,7 @@ impl Default for Input {
fn default() -> Self { fn default() -> Self {
Self { Self {
kind: InputKind::Stdin, 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 { fn is_stdin(&self) -> bool {
self.eq(Self::new(text::DASH)) self.eq(Self::new(text::DASH))
|| self.eq(Self::new(text::DEV_STDIN)) || 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 /// 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 paths::{FileExtTail, HeaderPrinter, Input, InputKind, MetadataExtTail};
use same_file::Handle; use same_file::Handle;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io::{self, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin, stdout}; use std::io::{self, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin, stdout};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError, get_exit_code, set_exit_code}; 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}; use uucore::{show, show_error};
#[uucore::main] #[uucore::main]
@ -46,7 +48,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
args::VerificationResult::CannotFollowStdinByName => { args::VerificationResult::CannotFollowStdinByName => {
return Err(USimpleError::new( return Err(USimpleError::new(
1, 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 // 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() { 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(()) Ok(())
@ -112,27 +117,50 @@ fn tail_file(
if !path.exists() { if !path.exists() {
set_exit_code(1); set_exit_code(1);
show_error!( show_error!(
"cannot open '{}' for reading: {}", "{}",
input.display_name, get_message_with_args(
text::NO_SUCH_FILE "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)?; observer.add_bad_path(path, input.display_name.as_str(), false)?;
} else if path.is_dir() { } else if path.is_dir() {
set_exit_code(1); set_exit_code(1);
header_printer.print_input(input); 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() { if settings.follow.is_some() {
let msg = if settings.retry { let msg = if settings.retry {
"" ""
} else { } else {
"; giving up on this name" &get_message("tail-giving-up-on-this-name")
}; };
show_error!( 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() { if !observer.follow_name_retry() {
@ -166,13 +194,19 @@ fn tail_file(
Err(e) if e.kind() == ErrorKind::PermissionDenied => { Err(e) if e.kind() == ErrorKind::PermissionDenied => {
observer.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(|| { 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) => { Err(e) => {
observer.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(|| { 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() { if meta.file_type().is_dir() {
set_exit_code(1); set_exit_code(1);
show_error!( show_error!(
"cannot open '{}' for reading: {}", "{}",
input.display_name, get_message_with_args(
text::NO_SUCH_FILE "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(()); return Ok(());
} }
@ -240,15 +282,25 @@ fn tail_stdin(
if paths::stdin_is_bad_fd() { if paths::stdin_is_bad_fd() {
set_exit_code(1); set_exit_code(1);
show_error!( show_error!(
"cannot fstat {}: {}", "{}",
text::STDIN_HEADER.quote(), get_message_with_args(
text::BAD_FD "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() { if settings.follow.is_some() {
show_error!( show_error!(
"error reading {}: {}", "{}",
text::STDIN_HEADER.quote(), get_message_with_args(
text::BAD_FD "tail-error-reading-file",
HashMap::from([
("file".to_string(), get_message("tail-stdin-header")),
("error".to_string(), get_message("tail-bad-fd"))
])
)
); );
} }
} else { } else {

View file

@ -5,20 +5,16 @@
// spell-checker:ignore (ToDO) kqueue // spell-checker:ignore (ToDO) kqueue
// Non-localized constants (system paths and technical identifiers)
pub const DASH: &str = "-"; pub const DASH: &str = "-";
pub const DEV_STDIN: &str = "/dev/stdin"; pub const DEV_STDIN: &str = "/dev/stdin";
pub const STDIN_HEADER: &str = "standard input"; pub const FD0: &str = "/dev/fd/0";
pub const NO_FILES_REMAINING: &str = "no files remaining"; pub const DEV_TTY: &str = "/dev/tty";
pub const NO_SUCH_FILE: &str = "No such file or directory"; pub const DEV_PTMX: &str = "/dev/ptmx";
pub const BECOME_INACCESSIBLE: &str = "has become inaccessible";
pub const BAD_FD: &str = "Bad file descriptor";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub const BACKEND: &str = "inotify"; pub const BACKEND: &str = "inotify";
#[cfg(all(unix, not(target_os = "linux")))] #[cfg(all(unix, not(target_os = "linux")))]
pub const BACKEND: &str = "kqueue"; pub const BACKEND: &str = "kqueue";
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub const BACKEND: &str = "ReadDirectoryChanges"; 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";