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:
committed by
GitHub
parent
76565e287d
commit
b5d6d188bf
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -10,3 +10,4 @@ path = "src/last.rs"
|
||||
uucore = { workspace = true, features = ["utmpx"] }
|
||||
clap = { workspace = true}
|
||||
dns-lookup = { workspace = true }
|
||||
parse_datetime = { workspace = true }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user