Skip to content

Commit b5d6d18

Browse files
authored
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
1 parent 76565e2 commit b5d6d18

6 files changed

Lines changed: 98 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ uucore = "0.1.0"
7373
uuid = { version = "1.16.0", features = ["rng-rand"] }
7474
windows = { version = "0.61.1" }
7575
xattr = "1.3.1"
76+
parse_datetime = "0.10.0"
7677

7778
[dependencies]
7879
clap = { workspace = true }
@@ -84,6 +85,7 @@ serde = { workspace = true }
8485
serde_json = { workspace = true }
8586
textwrap = { workspace = true }
8687
uucore = { workspace = true }
88+
parse_datetime = {workspace = true}
8789

8890
#
8991
blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" }

src/uu/last/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ path = "src/last.rs"
1010
uucore = { workspace = true, features = ["utmpx"] }
1111
clap = { workspace = true}
1212
dns-lookup = { workspace = true }
13+
parse_datetime = { workspace = true }

src/uu/last/src/last.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ mod options {
1515
pub const LIMIT: &str = "limit";
1616
pub const DNS: &str = "dns";
1717
pub const TIME_FORMAT: &str = "time-format";
18+
pub const SINCE: &str = "since";
19+
pub const UNTIL: &str = "until";
1820
pub const USER_TTY: &str = "username";
1921
pub const FILE: &str = "file";
2022
}
@@ -90,5 +92,21 @@ pub fn uu_app() -> Command {
9092
.help("show timestamps in the specified <format>: notime|short|full|iso")
9193
.default_value("short"),
9294
)
95+
.arg(
96+
Arg::new(options::SINCE)
97+
.short('s')
98+
.long(options::SINCE)
99+
.action(ArgAction::Set)
100+
.required(false)
101+
.help("display the lines since the specified time"),
102+
)
103+
.arg(
104+
Arg::new(options::UNTIL)
105+
.short('t')
106+
.long(options::UNTIL)
107+
.action(ArgAction::Set)
108+
.required(false)
109+
.help("display the lines until the specified time"),
110+
)
93111
.arg(Arg::new(options::USER_TTY).action(ArgAction::Append))
94112
}

src/uu/last/src/platform/unix.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use uucore::error::UIoError;
1010
use uucore::error::UResult;
1111

1212
use uucore::error::USimpleError;
13-
use uucore::utmpx::time::OffsetDateTime;
13+
use uucore::utmpx::time::{OffsetDateTime, UtcOffset};
1414
use uucore::utmpx::{time, Utmpx};
1515

1616
use std::fmt::Write;
@@ -23,6 +23,8 @@ use std::path::PathBuf;
2323
use std::str::FromStr;
2424
use std::time::Duration;
2525

26+
use parse_datetime::parse_datetime;
27+
2628
fn get_long_usage() -> String {
2729
format!("If FILE is not specified, use {WTMP_PATH}. /var/log/wtmp as FILE is common.")
2830
}
@@ -39,6 +41,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
3941
let dns = matches.get_flag(options::DNS);
4042
let hostlast = matches.get_flag(options::HOSTLAST);
4143
let nohost = matches.get_flag(options::NO_HOST);
44+
45+
let since_default = "0000-01-01 00:00:00".to_string();
46+
let until_default = "9999-12-31 23:59:59".to_string();
47+
48+
let since = parse_time_value(
49+
&matches
50+
.get_one::<String>(options::SINCE)
51+
.cloned()
52+
.unwrap_or(since_default),
53+
)?;
54+
55+
let until = parse_time_value(
56+
&matches
57+
.get_one::<String>(options::UNTIL)
58+
.cloned()
59+
.unwrap_or(until_default),
60+
)?;
61+
4262
let limit: i32 = if let Some(num) = matches.get_one::<i32>(options::LIMIT) {
4363
*num
4464
} else {
@@ -92,11 +112,27 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
92112
file: file.to_string(),
93113
users: user,
94114
time_format,
115+
since,
116+
until,
95117
};
96118

97119
last.exec()
98120
}
99121

122+
fn parse_time_value(time_value: &str) -> UResult<OffsetDateTime> {
123+
let value = parse_datetime(time_value)
124+
.map_err(|_| USimpleError::new(1, format!("invalid time value \"{time_value}\"")))?;
125+
126+
let offset = UtcOffset::from_whole_seconds(value.offset().local_minus_utc())
127+
.map_err(|_| USimpleError::new(2, "failed to extract time zone offset"))?;
128+
129+
Ok(
130+
OffsetDateTime::from_unix_timestamp(value.naive_local().and_utc().timestamp())
131+
.expect("Invalid timestamp")
132+
.replace_offset(offset),
133+
)
134+
}
135+
100136
const RUN_LEVEL_STR: &str = "runlevel";
101137
const REBOOT_STR: &str = "reboot";
102138
const SHUTDOWN_STR: &str = "shutdown";
@@ -113,6 +149,8 @@ struct Last {
113149
time_format: String,
114150
users: Option<Vec<String>>,
115151
limit: i32,
152+
since: OffsetDateTime,
153+
until: OffsetDateTime,
116154
}
117155

118156
fn is_numeric(s: &str) -> bool {
@@ -184,6 +222,10 @@ impl Last {
184222
let mut counter = 0;
185223
let mut first_ut_time = None;
186224
while let Some(ut) = ut_stack.pop() {
225+
if ut.login_time() < self.since || ut.login_time() > self.until {
226+
continue;
227+
}
228+
187229
if ut_stack.is_empty() {
188230
// By the end of loop we will have the earliest time
189231
// (This avoids getting into issues with the compiler)

tests/by-util/test_last.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,35 @@ fn test_display_hostname_last_column() {
132132

133133
assert_eq!(output_expected, output_result);
134134
}
135+
136+
#[test]
137+
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "openbsd")))]
138+
fn test_since_only_shows_entries_after_time() {
139+
let expected_entry_time = "16:29";
140+
let unexpected_entry_time = "16:24";
141+
142+
new_ucmd!()
143+
.arg("--file")
144+
.arg("last.input.1")
145+
.arg("--since")
146+
.arg("2025-03-08 16:28")
147+
.succeeds()
148+
.stdout_contains(expected_entry_time)
149+
.stdout_does_not_contain(unexpected_entry_time);
150+
}
151+
152+
#[test]
153+
#[cfg(all(unix, not(target_os = "macos"), not(target_os = "openbsd")))]
154+
fn test_until_only_shows_entries_before_time() {
155+
let expected_entry_time = "16:24";
156+
let unexpected_entry_time = "16:29";
157+
158+
new_ucmd!()
159+
.arg("--file")
160+
.arg("last.input.1")
161+
.arg("--until")
162+
.arg("2025-03-08 16:28")
163+
.succeeds()
164+
.stdout_contains(expected_entry_time)
165+
.stdout_does_not_contain(unexpected_entry_time);
166+
}

0 commit comments

Comments
 (0)