From b5d6d188bf4c9e39c118c47d9694eb86ac6627fe Mon Sep 17 00:00:00 2001 From: Sebastian Bentmar Holgersson Date: Fri, 1 Aug 2025 16:12:39 +0200 Subject: [PATCH] feat(last): add --since and --until (#342) * feat(last): add --since and --until * clean up clap slightly * fix clippy * test(last): add tests for --since and --until * refactor(last): resolve review comments * test(last): set cfg for since and until to unix * remove unnecessary comment * move parse_time_value below uumain * minor cleanup * fix ci issues --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/uu/last/Cargo.toml | 1 + src/uu/last/src/last.rs | 18 +++++++++++++ src/uu/last/src/platform/unix.rs | 44 +++++++++++++++++++++++++++++++- tests/by-util/test_last.rs | 32 +++++++++++++++++++++++ 6 files changed, 98 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 991f23e..5ca553f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1201,6 +1201,7 @@ dependencies = [ "dns-lookup", "libc", "nix", + "parse_datetime", "phf", "phf_codegen", "pretty_assertions", @@ -1294,6 +1295,7 @@ version = "0.0.1" dependencies = [ "clap", "dns-lookup", + "parse_datetime", "uucore", ] diff --git a/Cargo.toml b/Cargo.toml index 5377608..f8b4d7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ uucore = "0.1.0" uuid = { version = "1.16.0", features = ["rng-rand"] } windows = { version = "0.61.1" } xattr = "1.3.1" +parse_datetime = "0.10.0" [dependencies] clap = { workspace = true } @@ -84,6 +85,7 @@ serde = { workspace = true } serde_json = { workspace = true } textwrap = { workspace = true } uucore = { workspace = true } +parse_datetime = {workspace = true} # blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" } diff --git a/src/uu/last/Cargo.toml b/src/uu/last/Cargo.toml index 077080b..f3ed321 100644 --- a/src/uu/last/Cargo.toml +++ b/src/uu/last/Cargo.toml @@ -10,3 +10,4 @@ path = "src/last.rs" uucore = { workspace = true, features = ["utmpx"] } clap = { workspace = true} dns-lookup = { workspace = true } +parse_datetime = { workspace = true } diff --git a/src/uu/last/src/last.rs b/src/uu/last/src/last.rs index bb77bde..7f1569c 100644 --- a/src/uu/last/src/last.rs +++ b/src/uu/last/src/last.rs @@ -15,6 +15,8 @@ mod options { pub const LIMIT: &str = "limit"; pub const DNS: &str = "dns"; pub const TIME_FORMAT: &str = "time-format"; + pub const SINCE: &str = "since"; + pub const UNTIL: &str = "until"; pub const USER_TTY: &str = "username"; pub const FILE: &str = "file"; } @@ -90,5 +92,21 @@ pub fn uu_app() -> Command { .help("show timestamps in the specified : notime|short|full|iso") .default_value("short"), ) + .arg( + Arg::new(options::SINCE) + .short('s') + .long(options::SINCE) + .action(ArgAction::Set) + .required(false) + .help("display the lines since the specified time"), + ) + .arg( + Arg::new(options::UNTIL) + .short('t') + .long(options::UNTIL) + .action(ArgAction::Set) + .required(false) + .help("display the lines until the specified time"), + ) .arg(Arg::new(options::USER_TTY).action(ArgAction::Append)) } diff --git a/src/uu/last/src/platform/unix.rs b/src/uu/last/src/platform/unix.rs index 189e9fb..492d61a 100644 --- a/src/uu/last/src/platform/unix.rs +++ b/src/uu/last/src/platform/unix.rs @@ -10,7 +10,7 @@ use uucore::error::UIoError; use uucore::error::UResult; use uucore::error::USimpleError; -use uucore::utmpx::time::OffsetDateTime; +use uucore::utmpx::time::{OffsetDateTime, UtcOffset}; use uucore::utmpx::{time, Utmpx}; use std::fmt::Write; @@ -23,6 +23,8 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; +use parse_datetime::parse_datetime; + fn get_long_usage() -> String { format!("If FILE is not specified, use {WTMP_PATH}. /var/log/wtmp as FILE is common.") } @@ -39,6 +41,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let dns = matches.get_flag(options::DNS); let hostlast = matches.get_flag(options::HOSTLAST); let nohost = matches.get_flag(options::NO_HOST); + + let since_default = "0000-01-01 00:00:00".to_string(); + let until_default = "9999-12-31 23:59:59".to_string(); + + let since = parse_time_value( + &matches + .get_one::(options::SINCE) + .cloned() + .unwrap_or(since_default), + )?; + + let until = parse_time_value( + &matches + .get_one::(options::UNTIL) + .cloned() + .unwrap_or(until_default), + )?; + let limit: i32 = if let Some(num) = matches.get_one::(options::LIMIT) { *num } else { @@ -92,11 +112,27 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { file: file.to_string(), users: user, time_format, + since, + until, }; last.exec() } +fn parse_time_value(time_value: &str) -> UResult { + let value = parse_datetime(time_value) + .map_err(|_| USimpleError::new(1, format!("invalid time value \"{time_value}\"")))?; + + let offset = UtcOffset::from_whole_seconds(value.offset().local_minus_utc()) + .map_err(|_| USimpleError::new(2, "failed to extract time zone offset"))?; + + Ok( + OffsetDateTime::from_unix_timestamp(value.naive_local().and_utc().timestamp()) + .expect("Invalid timestamp") + .replace_offset(offset), + ) +} + const RUN_LEVEL_STR: &str = "runlevel"; const REBOOT_STR: &str = "reboot"; const SHUTDOWN_STR: &str = "shutdown"; @@ -113,6 +149,8 @@ struct Last { time_format: String, users: Option>, limit: i32, + since: OffsetDateTime, + until: OffsetDateTime, } fn is_numeric(s: &str) -> bool { @@ -184,6 +222,10 @@ impl Last { let mut counter = 0; let mut first_ut_time = None; while let Some(ut) = ut_stack.pop() { + if ut.login_time() < self.since || ut.login_time() > self.until { + continue; + } + if ut_stack.is_empty() { // By the end of loop we will have the earliest time // (This avoids getting into issues with the compiler) diff --git a/tests/by-util/test_last.rs b/tests/by-util/test_last.rs index b686657..d9d4228 100644 --- a/tests/by-util/test_last.rs +++ b/tests/by-util/test_last.rs @@ -132,3 +132,35 @@ fn test_display_hostname_last_column() { assert_eq!(output_expected, output_result); } + +#[test] +#[cfg(all(unix, not(target_os = "macos"), not(target_os = "openbsd")))] +fn test_since_only_shows_entries_after_time() { + let expected_entry_time = "16:29"; + let unexpected_entry_time = "16:24"; + + new_ucmd!() + .arg("--file") + .arg("last.input.1") + .arg("--since") + .arg("2025-03-08 16:28") + .succeeds() + .stdout_contains(expected_entry_time) + .stdout_does_not_contain(unexpected_entry_time); +} + +#[test] +#[cfg(all(unix, not(target_os = "macos"), not(target_os = "openbsd")))] +fn test_until_only_shows_entries_before_time() { + let expected_entry_time = "16:24"; + let unexpected_entry_time = "16:29"; + + new_ucmd!() + .arg("--file") + .arg("last.input.1") + .arg("--until") + .arg("2025-03-08 16:28") + .succeeds() + .stdout_contains(expected_entry_time) + .stdout_does_not_contain(unexpected_entry_time); +}