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",
|
"dns-lookup",
|
||||||
"libc",
|
"libc",
|
||||||
"nix",
|
"nix",
|
||||||
|
"parse_datetime",
|
||||||
"phf",
|
"phf",
|
||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
@@ -1294,6 +1295,7 @@ version = "0.0.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"dns-lookup",
|
"dns-lookup",
|
||||||
|
"parse_datetime",
|
||||||
"uucore",
|
"uucore",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ uucore = "0.1.0"
|
|||||||
uuid = { version = "1.16.0", features = ["rng-rand"] }
|
uuid = { version = "1.16.0", features = ["rng-rand"] }
|
||||||
windows = { version = "0.61.1" }
|
windows = { version = "0.61.1" }
|
||||||
xattr = "1.3.1"
|
xattr = "1.3.1"
|
||||||
|
parse_datetime = "0.10.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
@@ -84,6 +85,7 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
textwrap = { workspace = true }
|
textwrap = { workspace = true }
|
||||||
uucore = { workspace = true }
|
uucore = { workspace = true }
|
||||||
|
parse_datetime = {workspace = true}
|
||||||
|
|
||||||
#
|
#
|
||||||
blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" }
|
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"] }
|
uucore = { workspace = true, features = ["utmpx"] }
|
||||||
clap = { workspace = true}
|
clap = { workspace = true}
|
||||||
dns-lookup = { workspace = true }
|
dns-lookup = { workspace = true }
|
||||||
|
parse_datetime = { workspace = true }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ mod options {
|
|||||||
pub const LIMIT: &str = "limit";
|
pub const LIMIT: &str = "limit";
|
||||||
pub const DNS: &str = "dns";
|
pub const DNS: &str = "dns";
|
||||||
pub const TIME_FORMAT: &str = "time-format";
|
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 USER_TTY: &str = "username";
|
||||||
pub const FILE: &str = "file";
|
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")
|
.help("show timestamps in the specified <format>: notime|short|full|iso")
|
||||||
.default_value("short"),
|
.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))
|
.arg(Arg::new(options::USER_TTY).action(ArgAction::Append))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use uucore::error::UIoError;
|
|||||||
use uucore::error::UResult;
|
use uucore::error::UResult;
|
||||||
|
|
||||||
use uucore::error::USimpleError;
|
use uucore::error::USimpleError;
|
||||||
use uucore::utmpx::time::OffsetDateTime;
|
use uucore::utmpx::time::{OffsetDateTime, UtcOffset};
|
||||||
use uucore::utmpx::{time, Utmpx};
|
use uucore::utmpx::{time, Utmpx};
|
||||||
|
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
@@ -23,6 +23,8 @@ use std::path::PathBuf;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use parse_datetime::parse_datetime;
|
||||||
|
|
||||||
fn get_long_usage() -> String {
|
fn get_long_usage() -> String {
|
||||||
format!("If FILE is not specified, use {WTMP_PATH}. /var/log/wtmp as FILE is common.")
|
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 dns = matches.get_flag(options::DNS);
|
||||||
let hostlast = matches.get_flag(options::HOSTLAST);
|
let hostlast = matches.get_flag(options::HOSTLAST);
|
||||||
let nohost = matches.get_flag(options::NO_HOST);
|
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) {
|
let limit: i32 = if let Some(num) = matches.get_one::<i32>(options::LIMIT) {
|
||||||
*num
|
*num
|
||||||
} else {
|
} else {
|
||||||
@@ -92,11 +112,27 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||||||
file: file.to_string(),
|
file: file.to_string(),
|
||||||
users: user,
|
users: user,
|
||||||
time_format,
|
time_format,
|
||||||
|
since,
|
||||||
|
until,
|
||||||
};
|
};
|
||||||
|
|
||||||
last.exec()
|
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 RUN_LEVEL_STR: &str = "runlevel";
|
||||||
const REBOOT_STR: &str = "reboot";
|
const REBOOT_STR: &str = "reboot";
|
||||||
const SHUTDOWN_STR: &str = "shutdown";
|
const SHUTDOWN_STR: &str = "shutdown";
|
||||||
@@ -113,6 +149,8 @@ struct Last {
|
|||||||
time_format: String,
|
time_format: String,
|
||||||
users: Option<Vec<String>>,
|
users: Option<Vec<String>>,
|
||||||
limit: i32,
|
limit: i32,
|
||||||
|
since: OffsetDateTime,
|
||||||
|
until: OffsetDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_numeric(s: &str) -> bool {
|
fn is_numeric(s: &str) -> bool {
|
||||||
@@ -184,6 +222,10 @@ impl Last {
|
|||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
let mut first_ut_time = None;
|
let mut first_ut_time = None;
|
||||||
while let Some(ut) = ut_stack.pop() {
|
while let Some(ut) = ut_stack.pop() {
|
||||||
|
if ut.login_time() < self.since || ut.login_time() > self.until {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if ut_stack.is_empty() {
|
if ut_stack.is_empty() {
|
||||||
// By the end of loop we will have the earliest time
|
// By the end of loop we will have the earliest time
|
||||||
// (This avoids getting into issues with the compiler)
|
// (This avoids getting into issues with the compiler)
|
||||||
|
|||||||
@@ -132,3 +132,35 @@ fn test_display_hostname_last_column() {
|
|||||||
|
|
||||||
assert_eq!(output_expected, output_result);
|
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