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
This commit is contained in:
Sebastian Bentmar Holgersson
2025-08-01 16:12:39 +02:00
committed by GitHub
parent 76565e287d
commit b5d6d188bf
6 changed files with 98 additions and 1 deletions

2
Cargo.lock generated
View File

@@ -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",
]

View File

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

View File

@@ -10,3 +10,4 @@ path = "src/last.rs"
uucore = { workspace = true, features = ["utmpx"] }
clap = { workspace = true}
dns-lookup = { workspace = true }
parse_datetime = { workspace = true }

View File

@@ -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 <format>: 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))
}

View File

@@ -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::<String>(options::SINCE)
.cloned()
.unwrap_or(since_default),
)?;
let until = parse_time_value(
&matches
.get_one::<String>(options::UNTIL)
.cloned()
.unwrap_or(until_default),
)?;
let limit: i32 = if let Some(num) = matches.get_one::<i32>(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<OffsetDateTime> {
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<Vec<String>>,
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)

View File

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