Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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" }
Expand Down
1 change: 1 addition & 0 deletions src/uu/last/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ path = "src/last.rs"
uucore = { workspace = true, features = ["utmpx"] }
clap = { workspace = true}
dns-lookup = { workspace = true }
parse_datetime = { workspace = true }
18 changes: 18 additions & 0 deletions src/uu/last/src/last.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down Expand Up @@ -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))
}
44 changes: 43 additions & 1 deletion src/uu/last/src/platform/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.")
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 32 additions & 0 deletions tests/by-util/test_last.rs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tests :)

Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading