use crate::paths::Input;
use crate::{Quotable, parse, platform};
use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
use same_file::Handle;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::time::Duration;
use uucore::error::{UResult, USimpleError, UUsageError};
use uucore::parser::parse_signed_num::{SignPrefix, parse_signed_num_max};
use uucore::parser::parse_size::ParseSizeError;
use uucore::parser::parse_time;
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
use uucore::translate;
use uucore::{format_usage, show_warning};
pub mod options {
pub mod verbosity {
pub const QUIET: &str = "quiet";
pub const VERBOSE: &str = "verbose";
}
pub const BYTES: &str = "bytes";
pub const FOLLOW: &str = "follow";
pub const LINES: &str = "lines";
pub const PID: &str = "pid";
pub const SLEEP_INT: &str = "sleep-interval";
pub const ZERO_TERM: &str = "zero-terminated";
pub const DISABLE_INOTIFY_TERM: &str = "-disable-inotify"; pub const USE_POLLING: &str = "use-polling";
pub const RETRY: &str = "retry";
pub const FOLLOW_RETRY: &str = "F";
pub const MAX_UNCHANGED_STATS: &str = "max-unchanged-stats";
pub const ARG_FILES: &str = "files";
pub const PRESUME_INPUT_PIPE: &str = "-presume-input-pipe"; pub const DEBUG: &str = "debug";
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Signum {
Negative(u64),
Positive(u64),
PlusZero,
MinusZero,
}
#[derive(Debug, PartialEq, Eq)]
pub enum FilterMode {
Bytes(Signum),
Lines(Signum, u8),
}
impl FilterMode {
fn from_obsolete_args(args: &parse::ObsoleteArgs) -> Self {
let signum = if args.plus {
Signum::Positive(args.num)
} else {
Signum::Negative(args.num)
};
if args.lines {
Self::Lines(signum, b'\n')
} else {
Self::Bytes(signum)
}
}
fn from(matches: &ArgMatches) -> UResult<Self> {
let zero_term = matches.get_flag(options::ZERO_TERM);
let mode = if let Some(arg) = matches.get_one::<String>(options::BYTES) {
match parse_num(arg) {
Ok(signum) => Self::Bytes(signum),
Err(e) => {
return Err(USimpleError::new(
1,
translate!("tail-error-invalid-number-of-bytes", "arg" => e.to_string()),
));
}
}
} else if let Some(arg) = matches.get_one::<String>(options::LINES) {
match parse_num(arg) {
Ok(signum) => {
let delimiter = if zero_term { 0 } else { b'\n' };
Self::Lines(signum, delimiter)
}
Err(_) => {
return Err(USimpleError::new(
1,
translate!("tail-error-invalid-number-of-lines", "arg" => arg.quote()),
));
}
}
} else if zero_term {
Self::default_zero()
} else {
Self::default()
};
Ok(mode)
}
fn default_zero() -> Self {
Self::Lines(Signum::Negative(10), 0)
}
}
impl Default for FilterMode {
fn default() -> Self {
Self::Lines(Signum::Negative(10), b'\n')
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum FollowMode {
Descriptor,
Name,
}
#[derive(Debug)]
pub enum VerificationResult {
Ok,
CannotFollowStdinByName,
NoOutput,
}
#[derive(Debug)]
pub struct Settings {
pub follow: Option<FollowMode>,
pub max_unchanged_stats: u32,
pub mode: FilterMode,
pub pid: platform::Pid,
pub retry: bool,
pub sleep_sec: Duration,
pub use_polling: bool,
pub verbose: bool,
pub presume_input_pipe: bool,
pub debug: bool,
pub inputs: Vec<Input>,
}
impl Default for Settings {
fn default() -> Self {
Self {
max_unchanged_stats: 5,
sleep_sec: Duration::from_secs_f32(1.0),
follow: Option::default(),
mode: FilterMode::default(),
pid: Default::default(),
retry: Default::default(),
use_polling: Default::default(),
verbose: Default::default(),
presume_input_pipe: Default::default(),
debug: Default::default(),
inputs: Vec::default(),
}
}
}
impl Settings {
pub fn from_obsolete_args(args: &parse::ObsoleteArgs, name: Option<&OsString>) -> Self {
let mut settings = Self::default();
if args.follow {
settings.follow = if name.is_some() {
Some(FollowMode::Name)
} else {
Some(FollowMode::Descriptor)
};
}
settings.mode = FilterMode::from_obsolete_args(args);
let input = if let Some(name) = name {
Input::from(name)
} else {
Input::default()
};
settings.inputs.push(input);
settings
}
pub fn from(matches: &ArgMatches) -> UResult<Self> {
let follow_retry = matches.get_flag(options::FOLLOW_RETRY);
let retry = follow_retry || matches.get_flag(options::RETRY);
let follow = match (
follow_retry,
matches
.get_one::<String>(options::FOLLOW)
.map(String::as_str),
) {
(true, Some(_))
if matches.index_of(options::FOLLOW_RETRY) > matches.index_of(options::FOLLOW) =>
{
Some(FollowMode::Name)
}
(_, Some("name")) | (true, None) => Some(FollowMode::Name),
(_, Some(_)) => Some(FollowMode::Descriptor),
(false, None) => None,
};
let mut settings: Self = Self {
follow,
retry,
use_polling: matches.get_flag(options::USE_POLLING),
mode: FilterMode::from(matches)?,
verbose: matches.get_flag(options::verbosity::VERBOSE),
presume_input_pipe: matches.get_flag(options::PRESUME_INPUT_PIPE),
debug: matches.get_flag(options::DEBUG),
..Default::default()
};
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,
translate!("tail-error-invalid-number-of-seconds", "source" => source.clone()),
)
})?;
}
if let Some(s) = matches.get_one::<String>(options::MAX_UNCHANGED_STATS) {
settings.max_unchanged_stats = match s.parse::<u32>() {
Ok(s) => s,
Err(_) => {
return Err(UUsageError::new(
1,
translate!("tail-error-invalid-max-unchanged-stats", "value" => s.quote()),
));
}
}
}
if let Some(pid_str) = matches.get_one::<String>(options::PID) {
match pid_str.parse() {
Ok(pid) => {
#[cfg(unix)]
if pid < 0 {
return Err(USimpleError::new(
1,
translate!("tail-error-invalid-pid", "pid" => pid_str.quote()),
));
}
settings.pid = pid;
}
Err(e) => {
return Err(USimpleError::new(
1,
translate!("tail-error-invalid-pid-with-error", "pid" => pid_str.quote(), "error" => e),
));
}
}
}
settings.inputs = matches
.get_many::<OsString>(options::ARG_FILES)
.map_or_else(|| vec![Input::default()], |v| v.map(Input::from).collect());
settings.verbose = (matches.get_flag(options::verbosity::VERBOSE)
|| settings.inputs.len() > 1)
&& !matches.get_flag(options::verbosity::QUIET);
Ok(settings)
}
pub fn has_only_stdin(&self) -> bool {
self.inputs.iter().all(Input::is_stdin)
}
pub fn has_stdin(&self) -> bool {
self.inputs.iter().any(Input::is_stdin)
}
pub fn num_inputs(&self) -> usize {
self.inputs.len()
}
pub fn check_warnings(&self) {
if self.retry {
if self.follow.is_none() {
show_warning!("{}", translate!("tail-warning-retry-ignored"));
} else if self.follow == Some(FollowMode::Descriptor) {
show_warning!("{}", translate!("tail-warning-retry-only-effective"));
}
}
if self.pid != 0 {
if self.follow.is_none() {
show_warning!("{}", translate!("tail-warning-pid-ignored"));
} else if !platform::supports_pid_checks(self.pid) {
show_warning!("{}", translate!("tail-warning-pid-not-supported"));
}
}
if self.follow.is_some() && self.has_stdin() {
let blocking_stdin = self.pid == 0
&& self.follow == Some(FollowMode::Descriptor)
&& self.num_inputs() == 1
&& Handle::stdin().is_ok_and(|handle| {
handle
.as_file()
.metadata()
.is_ok_and(|meta| !meta.is_file())
});
if !blocking_stdin && std::io::stdin().is_terminal() {
show_warning!("{}", translate!("tail-warning-following-stdin-ineffective"));
}
}
}
pub fn verify(&self) -> VerificationResult {
if self.inputs.iter().any(Input::is_stdin) && self.follow == Some(FollowMode::Name) {
return VerificationResult::CannotFollowStdinByName;
}
if self.follow.is_none()
&& matches!(
self.mode,
FilterMode::Lines(Signum::MinusZero, _) | FilterMode::Bytes(Signum::MinusZero)
)
{
return VerificationResult::NoOutput;
}
VerificationResult::Ok
}
}
pub fn parse_obsolete(arg: &OsString, input: Option<&OsString>) -> UResult<Option<Settings>> {
match parse::parse_obsolete(arg) {
Some(Ok(args)) => Ok(Some(Settings::from_obsolete_args(&args, input))),
None => Ok(None),
Some(Err(e)) => {
Err(USimpleError::new(
1,
match e {
parse::ParseError::Context => {
translate!(
"tail-error-option-used-in-invalid-context",
"option" => arg.to_string_lossy().chars().nth(1).unwrap_or_default(),
)
}
parse::ParseError::InvalidEncoding => {
translate!("tail-error-bad-argument-encoding", "arg" => arg.quote())
}
},
))
}
}
}
fn parse_num(src: &str) -> Result<Signum, ParseSizeError> {
let result = parse_signed_num_max(src)?;
let is_plus = result.sign == Some(SignPrefix::Plus);
match (result.value, is_plus) {
(0, true) => Ok(Signum::PlusZero),
(0, false) => Ok(Signum::MinusZero),
(n, true) => Ok(Signum::Positive(n)),
(n, false) => Ok(Signum::Negative(n)),
}
}
pub fn parse_args(args: impl uucore::Args) -> UResult<Settings> {
let args_vec: Vec<OsString> = args.collect();
let clap_args = uu_app().try_get_matches_from(args_vec.clone());
let clap_result = match clap_args {
Ok(matches) => Ok(Settings::from(&matches)?),
Err(err) => Err(err.into()),
};
if args_vec.len() != 2 && args_vec.len() != 3 {
return clap_result;
}
let possible_obsolete_args = &args_vec[1];
if clap_result.is_ok() && !possible_obsolete_args.to_string_lossy().starts_with('+') {
return clap_result;
}
match parse_obsolete(possible_obsolete_args, args_vec.get(2))? {
Some(settings) => Ok(settings),
None => clap_result,
}
}
pub fn uu_app() -> Command {
#[cfg(target_os = "linux")]
let polling_help = translate!("tail-help-polling-linux");
#[cfg(all(unix, not(target_os = "linux")))]
let polling_help = translate!("tail-help-polling-unix");
#[cfg(target_os = "windows")]
let polling_help = translate!("tail-help-polling-windows");
#[cfg(not(any(unix, target_os = "windows")))]
let polling_help = translate!("tail-help-polling-unix");
Command::new("tail")
.version(uucore::crate_version!())
.help_template(uucore::localized_help_template("tail"))
.about(translate!("tail-about"))
.override_usage(format_usage(&translate!("tail-usage")))
.infer_long_args(true)
.arg(
Arg::new(options::BYTES)
.short('c')
.long(options::BYTES)
.allow_hyphen_values(true)
.overrides_with_all([options::BYTES, options::LINES])
.help(translate!("tail-help-bytes")),
)
.arg(
Arg::new(options::FOLLOW)
.short('f')
.long(options::FOLLOW)
.default_missing_value("descriptor")
.num_args(0..=1)
.require_equals(true)
.value_parser(ShortcutValueParser::new(["descriptor", "name"]))
.overrides_with(options::FOLLOW)
.help(translate!("tail-help-follow")),
)
.arg(
Arg::new(options::LINES)
.short('n')
.long(options::LINES)
.allow_hyphen_values(true)
.overrides_with_all([options::BYTES, options::LINES])
.help(translate!("tail-help-lines")),
)
.arg(
Arg::new(options::PID)
.long(options::PID)
.value_name("PID")
.help(translate!("tail-help-pid"))
.overrides_with(options::PID),
)
.arg(
Arg::new(options::verbosity::QUIET)
.short('q')
.long(options::verbosity::QUIET)
.visible_alias("silent")
.overrides_with_all([options::verbosity::QUIET, options::verbosity::VERBOSE])
.help(translate!("tail-help-quiet"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SLEEP_INT)
.short('s')
.value_name("N")
.long(options::SLEEP_INT)
.help(translate!("tail-help-sleep-interval")),
)
.arg(
Arg::new(options::MAX_UNCHANGED_STATS)
.value_name("N")
.long(options::MAX_UNCHANGED_STATS)
.help(translate!("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(translate!("tail-help-verbose"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ZERO_TERM)
.short('z')
.long(options::ZERO_TERM)
.help(translate!("tail-help-zero-terminated"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::USE_POLLING)
.alias(options::DISABLE_INOTIFY_TERM) .alias("dis") .long(options::USE_POLLING)
.help(polling_help)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::RETRY)
.long(options::RETRY)
.help(translate!("tail-help-retry"))
.overrides_with(options::RETRY)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::FOLLOW_RETRY)
.short('F')
.help(translate!("tail-help-follow-retry"))
.overrides_with(options::FOLLOW_RETRY)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::DEBUG)
.long(options::DEBUG)
.help(translate!("tail-help-debug"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::PRESUME_INPUT_PIPE)
.long("presume-input-pipe")
.alias(options::PRESUME_INPUT_PIPE)
.hide(true)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::ARG_FILES)
.action(ArgAction::Append)
.num_args(1..)
.value_parser(value_parser!(OsString))
.value_hint(clap::ValueHint::FilePath),
)
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use crate::parse::ObsoleteArgs;
use super::*;
#[test]
fn test_parse_num_when_sign_is_given() {
let result = parse_num("+0");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Signum::PlusZero);
let result = parse_num("+1");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Signum::Positive(1));
let result = parse_num("-0");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Signum::MinusZero);
let result = parse_num("-1");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Signum::Negative(1));
}
#[test]
fn test_parse_num_when_no_sign_is_given() {
let result = parse_num("0");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Signum::MinusZero);
let result = parse_num("1");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Signum::Negative(1));
}
#[test]
fn test_parse_obsolete_settings_f() {
let args = ObsoleteArgs {
follow: true,
..Default::default()
};
let result = Settings::from_obsolete_args(&args, None);
assert_eq!(result.follow, Some(FollowMode::Descriptor));
let result = Settings::from_obsolete_args(&args, Some(&"file".into()));
assert_eq!(result.follow, Some(FollowMode::Name));
}
#[rstest]
#[case::default(vec![], None, false)]
#[case::retry(vec!["--retry"], None, true)]
#[case::multiple_retry(vec!["--retry", "--retry"], None, true)]
#[case::follow_long(vec!["--follow"], Some(FollowMode::Descriptor), false)]
#[case::follow_short(vec!["-f"], Some(FollowMode::Descriptor), false)]
#[case::follow_long_with_retry(vec!["--follow", "--retry"], Some(FollowMode::Descriptor), true)]
#[case::follow_short_with_retry(vec!["-f", "--retry"], Some(FollowMode::Descriptor), true)]
#[case::follow_overwrites_previous_selection_1(vec!["--follow=name", "--follow=descriptor"], Some(FollowMode::Descriptor), false)]
#[case::follow_overwrites_previous_selection_2(vec!["--follow=descriptor", "--follow=name"], Some(FollowMode::Name), false)]
#[case::big_f(vec!["-F"], Some(FollowMode::Name), true)]
#[case::multiple_big_f(vec!["-F", "-F"], Some(FollowMode::Name), true)]
#[case::big_f_with_retry_then_does_not_change(vec!["-F", "--retry"], Some(FollowMode::Name), true)]
#[case::big_f_with_follow_descriptor_then_change(vec!["-F", "--follow=descriptor"], Some(FollowMode::Descriptor), true)]
#[case::multiple_big_f_with_follow_descriptor_then_no_change(vec!["-F", "--follow=descriptor", "-F"], Some(FollowMode::Name), true)]
#[case::big_f_with_follow_short_then_change(vec!["-F", "-f"], Some(FollowMode::Descriptor), true)]
#[case::follow_descriptor_with_big_f_then_change(vec!["--follow=descriptor", "-F"], Some(FollowMode::Name), true)]
#[case::follow_short_with_big_f_then_change(vec!["-f", "-F"], Some(FollowMode::Name), true)]
#[case::big_f_with_follow_name_then_not_change(vec!["-F", "--follow=name"], Some(FollowMode::Name), true)]
#[case::follow_name_with_big_f_then_not_change(vec!["--follow=name", "-F"], Some(FollowMode::Name), true)]
#[case::big_f_with_multiple_long_follow(vec!["--follow=name", "-F", "--follow=descriptor"], Some(FollowMode::Descriptor), true)]
#[case::big_f_with_multiple_long_follow_name(vec!["--follow=name", "-F", "--follow=name"], Some(FollowMode::Name), true)]
#[case::big_f_with_multiple_short_follow(vec!["-f", "-F", "-f"], Some(FollowMode::Descriptor), true)]
#[case::multiple_big_f_with_multiple_short_follow(vec!["-f", "-F", "-f", "-F"], Some(FollowMode::Name), true)]
fn test_parse_settings_follow_mode_and_retry(
#[case] args: Vec<&str>,
#[case] expected_follow_mode: Option<FollowMode>,
#[case] expected_retry: bool,
) {
let settings =
Settings::from(&uu_app().no_binary_name(true).get_matches_from(args)).unwrap();
assert_eq!(settings.follow, expected_follow_mode);
assert_eq!(settings.retry, expected_retry);
}
}