From 069ef48eafe0e936381b3725a874d8eb1951ce64 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Sat, 26 Jul 2025 01:28:09 +0200 Subject: [PATCH 01/10] feat(last): add --since and --until --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/uu/last/Cargo.toml | 1 + src/uu/last/src/last.rs | 22 ++++++++++++++++++ src/uu/last/src/platform/unix.rs | 38 +++++++++++++++++++++++++++++++- 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 06cc17d4..df7e59c3 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 5377608d..f8b4d7bf 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 077080b2..f3ed3212 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 bb77bde7..c7773dd5 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,25 @@ 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("since") + .action(ArgAction::Set) + .required(false) + .help("display the lines since the specified time") + .value_name("time") + .default_value("0000-01-01 00:00:00"), + ) + .arg( + Arg::new(options::UNTIL) + .short('t') + .long("until") + .action(ArgAction::Set) + .required(false) + .help("display the lines until the specified time") + .value_name("time") + .default_value("9999-12-31 23:59:59"), + ) .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 189e9fbd..fa2ccf5c 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.") } @@ -30,6 +32,30 @@ fn get_long_usage() -> String { const WTMP_PATH: &str = "/var/log/wtmp"; static TIME_FORMAT_STR: [&str; 4] = ["notime", "short", "full", "iso"]; +fn parse_time_value(time_value: &str) -> UResult { + parse_datetime(time_value).map_or_else( + |_| { + Err(USimpleError::new( + 1, + format!("invalid time value \"{}\"", time_value), + )) + }, + |dt| { + UtcOffset::from_whole_seconds(dt.offset().local_minus_utc()).map_or_else( + |_| Err(USimpleError::new(2, "failed to extract time zone offset")), + |offset| { + let naive = dt.naive_local(); + Ok( + OffsetDateTime::from_unix_timestamp(naive.and_utc().timestamp()) + .expect("Invalid timestamp") + .replace_offset(offset), + ) + }, + ) + }, + ) +} + pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(get_long_usage()) @@ -39,6 +65,8 @@ 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 until = parse_time_value(matches.get_one::(options::UNTIL).unwrap())?; + let since = parse_time_value(matches.get_one::(options::SINCE).unwrap())?; let limit: i32 = if let Some(num) = matches.get_one::(options::LIMIT) { *num } else { @@ -92,6 +120,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { file: file.to_string(), users: user, time_format, + since, + until, }; last.exec() @@ -113,6 +143,8 @@ struct Last { time_format: String, users: Option>, limit: i32, + since: OffsetDateTime, + until: OffsetDateTime, } fn is_numeric(s: &str) -> bool { @@ -184,6 +216,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) From c0db32b6d2a9de12f0184787b7eadc98b9567238 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Sat, 26 Jul 2025 02:01:55 +0200 Subject: [PATCH 02/10] clean up clap slightly --- src/uu/last/src/last.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/uu/last/src/last.rs b/src/uu/last/src/last.rs index c7773dd5..7d10ec02 100644 --- a/src/uu/last/src/last.rs +++ b/src/uu/last/src/last.rs @@ -95,21 +95,19 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::SINCE) .short('s') - .long("since") + .long(options::SINCE) .action(ArgAction::Set) .required(false) .help("display the lines since the specified time") - .value_name("time") .default_value("0000-01-01 00:00:00"), ) .arg( Arg::new(options::UNTIL) .short('t') - .long("until") + .long(options::UNTIL) .action(ArgAction::Set) .required(false) .help("display the lines until the specified time") - .value_name("time") .default_value("9999-12-31 23:59:59"), ) .arg(Arg::new(options::USER_TTY).action(ArgAction::Append)) From a603f93fe1e76a6b2735574e329a76278cee0fb4 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Sat, 26 Jul 2025 11:17:13 +0200 Subject: [PATCH 03/10] fix clippy --- src/uu/last/src/platform/unix.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/last/src/platform/unix.rs b/src/uu/last/src/platform/unix.rs index fa2ccf5c..81c59c54 100644 --- a/src/uu/last/src/platform/unix.rs +++ b/src/uu/last/src/platform/unix.rs @@ -37,7 +37,7 @@ fn parse_time_value(time_value: &str) -> UResult { |_| { Err(USimpleError::new( 1, - format!("invalid time value \"{}\"", time_value), + format!("invalid time value \"{time_value}\""), )) }, |dt| { From 55ba53b11cfb9da2b5a475df070ec4b0e6742270 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Fri, 1 Aug 2025 00:28:30 +0200 Subject: [PATCH 04/10] test(last): add tests for --since and --until --- tests/by-util/test_last.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/by-util/test_last.rs b/tests/by-util/test_last.rs index b6866570..9f2ccf61 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))] +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))] +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); +} From 905f833c46e3df5fee07ccd7587d27add5076c6f Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Fri, 1 Aug 2025 00:28:47 +0200 Subject: [PATCH 05/10] refactor(last): resolve review comments --- src/uu/last/src/last.rs | 6 ++-- src/uu/last/src/platform/unix.rs | 51 ++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/uu/last/src/last.rs b/src/uu/last/src/last.rs index 7d10ec02..7f1569c0 100644 --- a/src/uu/last/src/last.rs +++ b/src/uu/last/src/last.rs @@ -98,8 +98,7 @@ pub fn uu_app() -> Command { .long(options::SINCE) .action(ArgAction::Set) .required(false) - .help("display the lines since the specified time") - .default_value("0000-01-01 00:00:00"), + .help("display the lines since the specified time"), ) .arg( Arg::new(options::UNTIL) @@ -107,8 +106,7 @@ pub fn uu_app() -> Command { .long(options::UNTIL) .action(ArgAction::Set) .required(false) - .help("display the lines until the specified time") - .default_value("9999-12-31 23:59:59"), + .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 81c59c54..8cd84e59 100644 --- a/src/uu/last/src/platform/unix.rs +++ b/src/uu/last/src/platform/unix.rs @@ -33,26 +33,16 @@ const WTMP_PATH: &str = "/var/log/wtmp"; static TIME_FORMAT_STR: [&str; 4] = ["notime", "short", "full", "iso"]; fn parse_time_value(time_value: &str) -> UResult { - parse_datetime(time_value).map_or_else( - |_| { - Err(USimpleError::new( - 1, - format!("invalid time value \"{time_value}\""), - )) - }, - |dt| { - UtcOffset::from_whole_seconds(dt.offset().local_minus_utc()).map_or_else( - |_| Err(USimpleError::new(2, "failed to extract time zone offset")), - |offset| { - let naive = dt.naive_local(); - Ok( - OffsetDateTime::from_unix_timestamp(naive.and_utc().timestamp()) - .expect("Invalid timestamp") - .replace_offset(offset), - ) - }, - ) - }, + 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), ) } @@ -65,8 +55,25 @@ 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 until = parse_time_value(matches.get_one::(options::UNTIL).unwrap())?; - let since = parse_time_value(matches.get_one::(options::SINCE).unwrap())?; + + /* + default_value 9999-12-31 23:59:59") + */ + + let since = parse_time_value( + &matches + .get_one::(options::SINCE) + .cloned() + .unwrap_or_else(|| "0000-01-01 00:00:00".to_string()), + )?; + + let until = parse_time_value( + &matches + .get_one::(options::UNTIL) + .cloned() + .unwrap_or_else(|| "9999-12-31 23:59:59".to_string()), + )?; + let limit: i32 = if let Some(num) = matches.get_one::(options::LIMIT) { *num } else { From daebae6771746467a989e1469bd976b9927f8ef8 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Fri, 1 Aug 2025 00:32:12 +0200 Subject: [PATCH 06/10] test(last): set cfg for since and until to unix --- tests/by-util/test_last.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/by-util/test_last.rs b/tests/by-util/test_last.rs index 9f2ccf61..fe3c75d6 100644 --- a/tests/by-util/test_last.rs +++ b/tests/by-util/test_last.rs @@ -134,7 +134,7 @@ fn test_display_hostname_last_column() { } #[test] -#[cfg(all(unix))] +#[cfg(unix)] fn test_since_only_shows_entries_after_time() { let expected_entry_time = "16:29"; let unexpected_entry_time = "16:24"; @@ -150,7 +150,7 @@ fn test_since_only_shows_entries_after_time() { } #[test] -#[cfg(all(unix))] +#[cfg(unix)] fn test_until_only_shows_entries_before_time() { let expected_entry_time = "16:24"; let unexpected_entry_time = "16:29"; From 69d9961406be7ed9e6400950c9ad1a2a7ee97804 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Fri, 1 Aug 2025 00:36:52 +0200 Subject: [PATCH 07/10] remove unnecessary comment --- src/uu/last/src/platform/unix.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/uu/last/src/platform/unix.rs b/src/uu/last/src/platform/unix.rs index 8cd84e59..05906a1d 100644 --- a/src/uu/last/src/platform/unix.rs +++ b/src/uu/last/src/platform/unix.rs @@ -56,10 +56,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let hostlast = matches.get_flag(options::HOSTLAST); let nohost = matches.get_flag(options::NO_HOST); - /* - default_value 9999-12-31 23:59:59") - */ - let since = parse_time_value( &matches .get_one::(options::SINCE) From 4ab62603fa3560a91114d96f1482e82c70edf9fe Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Fri, 1 Aug 2025 00:38:49 +0200 Subject: [PATCH 08/10] move parse_time_value below uumain --- src/uu/last/src/platform/unix.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/uu/last/src/platform/unix.rs b/src/uu/last/src/platform/unix.rs index 05906a1d..2c0a5e2e 100644 --- a/src/uu/last/src/platform/unix.rs +++ b/src/uu/last/src/platform/unix.rs @@ -32,20 +32,6 @@ fn get_long_usage() -> String { const WTMP_PATH: &str = "/var/log/wtmp"; static TIME_FORMAT_STR: [&str; 4] = ["notime", "short", "full", "iso"]; -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), - ) -} - pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app() .after_help(get_long_usage()) @@ -130,6 +116,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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"; From 608988249f3ec94ac444b689cd4ae1226c4308f3 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Fri, 1 Aug 2025 00:41:57 +0200 Subject: [PATCH 09/10] minor cleanup --- src/uu/last/src/platform/unix.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/uu/last/src/platform/unix.rs b/src/uu/last/src/platform/unix.rs index 2c0a5e2e..e89c63f4 100644 --- a/src/uu/last/src/platform/unix.rs +++ b/src/uu/last/src/platform/unix.rs @@ -42,18 +42,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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_else(|| "0000-01-01 00:00:00".to_string()), + .unwrap_or_else(|| since_default), )?; let until = parse_time_value( &matches .get_one::(options::UNTIL) .cloned() - .unwrap_or_else(|| "9999-12-31 23:59:59".to_string()), + .unwrap_or_else(|| until_default), )?; let limit: i32 = if let Some(num) = matches.get_one::(options::LIMIT) { From 3f81edd437fc2625c604f9c1f7463b47a8fa3998 Mon Sep 17 00:00:00 2001 From: Sebastian Holgersson Date: Fri, 1 Aug 2025 15:59:08 +0200 Subject: [PATCH 10/10] fix ci issues --- src/uu/last/src/platform/unix.rs | 4 ++-- tests/by-util/test_last.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uu/last/src/platform/unix.rs b/src/uu/last/src/platform/unix.rs index e89c63f4..492d61a4 100644 --- a/src/uu/last/src/platform/unix.rs +++ b/src/uu/last/src/platform/unix.rs @@ -49,14 +49,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &matches .get_one::(options::SINCE) .cloned() - .unwrap_or_else(|| since_default), + .unwrap_or(since_default), )?; let until = parse_time_value( &matches .get_one::(options::UNTIL) .cloned() - .unwrap_or_else(|| until_default), + .unwrap_or(until_default), )?; let limit: i32 = if let Some(num) = matches.get_one::(options::LIMIT) { diff --git a/tests/by-util/test_last.rs b/tests/by-util/test_last.rs index fe3c75d6..d9d42287 100644 --- a/tests/by-util/test_last.rs +++ b/tests/by-util/test_last.rs @@ -134,7 +134,7 @@ fn test_display_hostname_last_column() { } #[test] -#[cfg(unix)] +#[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"; @@ -150,7 +150,7 @@ fn test_since_only_shows_entries_after_time() { } #[test] -#[cfg(unix)] +#[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";