Skip to content

Commit

Permalink
cli/parse: Refactor --lastmerge into --from/--to
Browse files Browse the repository at this point in the history
* Can now specify command number, and use for both bounds
* Works for all commands, not just `log`
* More ergonomic I feel, but maybe not as discoverable
* No more surprising interaction between `--lastmerge` and `--from`
* The effective bounds are still a bit counterintuitive

I hoped to save an `open()` syscall, but gzip doesn't `impl Seek`.
  • Loading branch information
vincentdephily committed Nov 25, 2024
1 parent 4ac3beb commit 463a906
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 82 deletions.
7 changes: 1 addition & 6 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ use std::{collections::{BTreeMap, HashMap, HashSet},
/// Straightforward display of merge events
///
/// We store the start times in a hashmap to compute/print the duration when we reach a stop event.
pub fn cmd_log(mut gc: Conf, sc: ConfLog) -> Result<bool, Error> {
if sc.lastmerge {
if let Some(t) = parse::get_cmd_times(&gc.logfile, gc.from, gc.to)?.last() {
gc.from = Some(*t);
}
}
pub fn cmd_log(gc: Conf, sc: ConfLog) -> Result<bool, Error> {
let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?;
let mut merges: HashMap<String, i64> = HashMap::new();
let mut unmerges: HashMap<String, i64> = HashMap::new();
Expand Down
17 changes: 8 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ pub struct Conf {
pub date_fmt: DateStyle,
pub out: OutStyle,
pub logfile: String,
pub from: Option<i64>,
pub to: Option<i64>,
pub from: TimeBound,
pub to: TimeBound,
}
pub struct ConfLog {
pub show: Show,
Expand All @@ -47,7 +47,6 @@ pub struct ConfLog {
pub starttime: bool,
pub first: usize,
pub last: usize,
pub lastmerge: bool,
}
pub struct ConfPred {
pub show: Show,
Expand Down Expand Up @@ -154,10 +153,11 @@ impl Conf {
let outdef = if isterm { OutStyle::Columns } else { OutStyle::Tab };
let offset = get_offset(sel!(cli, toml, utc, (), false)?);
Ok(Self { logfile: sel!(cli, toml, logfile, (), String::from("/var/log/emerge.log"))?,
from: cli.get_one("from")
.map(|d| i64::parse(d, offset, "--from"))
.transpose()?,
to: cli.get_one("to").map(|d| i64::parse(d, offset, "--to")).transpose()?,
from:
cli.get_one("from")
.map_or(Ok(TimeBound::None), |d| TimeBound::parse(d, offset, "--from"))?,
to: cli.get_one("to")
.map_or(Ok(TimeBound::None), |d| TimeBound::parse(d, offset, "--to"))?,
pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }),
merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }),
unmerge: AnsiStr::from(if color { "\x1B[1;31m" } else { "<<< " }),
Expand Down Expand Up @@ -187,8 +187,7 @@ impl ConfLog {
exact: cli.get_flag("exact"),
starttime: sel!(cli, toml, log, starttime, (), false)?,
first: *cli.get_one("first").unwrap_or(&usize::MAX),
last: *cli.get_one("last").unwrap_or(&usize::MAX),
lastmerge: cli.get_flag("lastmerge") })
last: *cli.get_one("last").unwrap_or(&usize::MAX) })
}
}

Expand Down
16 changes: 6 additions & 10 deletions src/config/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ pub fn build_cli() -> Command {
m: Package merges\n \
t: Totals\n \
a: All of the above");
let h = "Only parse log entries after <date>\n \
let h = "Only parse log entries after <date/command>\n \
2018-03-04|2018-03-04 12:34:56|2018-03-04T12:34: Absolute ISO date\n \
123456789: Absolute unix timestamp\n \
1 year, 2 months|10d: Relative date";
1 year, 2 months|10d: Relative date\n \
1c|2 commands Emerge command";
let from = Arg::new("from").short('f')
.long("from")
.value_name("date")
Expand All @@ -90,10 +91,11 @@ pub fn build_cli() -> Command {
.help_heading("Filter")
.help(h.split_once('\n').unwrap().0)
.long_help(h);
let h = "Only parse log entries before <date>\n \
let h = "Only parse log entries before <date/command>\n \
2018-03-04|2018-03-04 12:34:56|2018-03-04T12:34: Absolute ISO date\n \
123456789: Absolute unix timestamp\n \
1 year, 2 months|10d: Relative date";
1 year, 2 months|10d: Relative date\n \
1c|2 commands Emerge command";
let to = Arg::new("to").short('t')
.long("to")
.value_name("date")
Expand Down Expand Up @@ -127,11 +129,6 @@ pub fn build_cli() -> Command {
.long_help("Show only the last <num> entries\n \
(empty)|1: last entry\n \
5: last 5 entries\n");
let lastmerge = Arg::new("lastmerge").long("lastmerge")
.action(SetTrue)
.display_order(8)
.help_heading("Filter")
.help("Show only the last merge");
let h = "Use main, backup, either, or no portage resume list\n\
This is ignored if STDIN is a piped `emerge -p` output\n \
(default)|auto|a: Use main or backup resume list, if currently emerging\n \
Expand Down Expand Up @@ -342,7 +339,6 @@ pub fn build_cli() -> Command {
.arg(starttime)
.arg(&first)
.arg(&last)
.arg(lastmerge)
.arg(show_l)
.arg(&exact)
.arg(&pkg);
Expand Down
68 changes: 49 additions & 19 deletions src/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,32 +74,59 @@ pub fn epoch_now() -> i64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64
}

#[cfg_attr(test, derive(PartialEq, Debug))]
#[derive(Clone, Copy)]
pub enum TimeBound {
/// Unbounded
None,
/// Bound by unix timestamp
Unix(i64),
/// Bound by time of nth fist/last emerge command
Cmd(usize),
}

/// Parse datetime in various formats, returning unix timestamp
impl ArgParse<String, UtcOffset> for i64 {
impl ArgParse<String, UtcOffset> for TimeBound {
fn parse(val: &String, offset: UtcOffset, src: &'static str) -> Result<Self, ArgError> {
let s = val.trim();
let et = match i64::from_str(s) {
Ok(i) => return Ok(i),
Ok(i) => return Ok(Self::Unix(i)),
Err(et) => et,
};
let ea = match parse_date_yyyymmdd(s, offset) {
Ok(i) => return Ok(i),
Ok(i) => return Ok(Self::Unix(i)),
Err(ea) => ea,
};
let ec = match parse_command_num(s) {
Ok(i) => return Ok(Self::Cmd(i)),
Err(ea) => ea,
};
match parse_date_ago(s) {
Ok(i) => Ok(i),
Ok(i) => Ok(Self::Unix(i)),
Err(er) => {
let m = format!("Not a unix timestamp ({et}), absolute date ({ea}), or relative date ({er})");
let m = format!("Not a unix timestamp ({et}), absolute date ({ea}), relative date ({er}), or command ({ec})");
Err(ArgError::new(val, src).msg(m))
},
}
}
}

/// Parse a command index
fn parse_command_num(s: &str) -> Result<usize, Error> {
use atoi::FromRadix10;
match usize::from_radix_10(s.as_bytes()) {
(num, pos) if pos != 0 => match s[pos..].trim() {
"c" | "command" | "commands" => Ok(num),
_ => bail!("bad span {:?}", &s[pos..]),
},
_ => bail!("not a number"),
}
}

/// Parse a number of day/years/hours/etc in the past, relative to current time
fn parse_date_ago(s: &str) -> Result<i64, Error> {
if !s.chars().all(|c| c.is_alphanumeric() || c == ' ' || c == ',') {
bail!("Illegal char");
bail!("illegal char");
}
let mut now = OffsetDateTime::now_utc();
let re = Regex::new("([0-9]+|[a-z]+)").expect("Bad date span regex");
Expand Down Expand Up @@ -138,7 +165,7 @@ fn parse_date_ago(s: &str) -> Result<i64, Error> {
}

if !at_least_one {
bail!("No token found");
bail!("no token found");
}
Ok(now.unix_timestamp())
}
Expand Down Expand Up @@ -180,7 +207,7 @@ fn parse_date_yyyymmdd(s: &str, offset: UtcOffset) -> Result<i64, Error> {
]))
])?;
if !rest.is_empty() {
bail!("Junk at end")
bail!("junk at end")
}
Ok(OffsetDateTime::try_from(p)?.unix_timestamp())
}
Expand Down Expand Up @@ -299,8 +326,8 @@ mod test {
fn parse_3339(s: &str) -> OffsetDateTime {
OffsetDateTime::parse(s, &Rfc3339).expect(s)
}
fn parse_date(s: &str, o: UtcOffset) -> Result<i64, ArgError> {
i64::parse(&String::from(s), o, "")
fn parse_date(s: &str, o: UtcOffset) -> Result<TimeBound, ArgError> {
TimeBound::parse(&String::from(s), o, "")
}
fn ts(t: OffsetDateTime) -> i64 {
t.unix_timestamp()
Expand All @@ -313,24 +340,27 @@ mod test {
let now = epoch_now();

Check warning on line 340 in src/datetime.rs

View workflow job for this annotation

GitHub Actions / Rustfmt

Diff in /home/runner/work/emlop/emlop/src/datetime.rs
let (day, hour, min) = (60 * 60 * 24, 60 * 60, 60);
let tz_utc = UtcOffset::UTC;

// Absolute dates
assert_eq!(Ok(then), parse_date(" 1522713600 ", tz_utc));
assert_eq!(Ok(then), parse_date(" 2018-04-03 ", tz_utc));
assert_eq!(Ok(then + hour + min), parse_date("2018-04-03 01:01", tz_utc));
assert_eq!(Ok(then + hour + min + 1), parse_date("2018-04-03 01:01:01", tz_utc));
assert_eq!(Ok(then + hour + min + 1), parse_date("2018-04-03T01:01:01", tz_utc));
assert_eq!(Ok(TimeBound::Unix(then)), parse_date(" 1522713600 ", tz_utc));
assert_eq!(Ok(TimeBound::Unix(then)), parse_date(" 2018-04-03 ", tz_utc));
assert_eq!(Ok(TimeBound::Unix(then + hour + min)), parse_date("2018-04-03 01:01", tz_utc));
assert_eq!(Ok(TimeBound::Unix(then + hour + min + 1)),
parse_date("2018-04-03 01:01:01", tz_utc));
assert_eq!(Ok(TimeBound::Unix(then + hour + min + 1)),
parse_date("2018-04-03T01:01:01", tz_utc));

// Different timezone (not calling `get_utcoffset()` because tests are threaded, which makes
// `UtcOffset::current_local_offset()` error out)
for secs in [hour, -1 * hour, 90 * min, -90 * min] {
let offset = dbg!(UtcOffset::from_whole_seconds(secs.try_into().unwrap()).unwrap());
assert_eq!(Ok(then - secs), parse_date("2018-04-03T00:00", offset));
assert_eq!(Ok(TimeBound::Unix(then - secs)), parse_date("2018-04-03T00:00", offset));
}

// Relative dates
assert_eq!(Ok(now - hour - 3 * day - 45), parse_date("1 hour, 3 days 45sec", tz_utc));
assert_eq!(Ok(now - 5 * 7 * day), parse_date("5 weeks", tz_utc));
assert_eq!(Ok(TimeBound::Unix(now - hour - 3 * day - 45)),
parse_date("1 hour, 3 days 45sec", tz_utc));
assert_eq!(Ok(TimeBound::Unix(now - 5 * 7 * day)), parse_date("5 weeks", tz_utc));

// Failure cases
assert!(parse_date("", tz_utc).is_err());
Expand Down
2 changes: 1 addition & 1 deletion src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod proces;

pub use ansi::{Ansi, AnsiStr};
pub use current::{get_buildlog, get_emerge, get_pretend, get_resume, Pkg};
pub use history::{get_cmd_times, get_hist, Hist};
pub use history::{get_hist, Hist};
#[cfg(test)]
pub use proces::tests::procs;
pub use proces::{get_all_proc, FmtProc, ProcKind, ProcList};
76 changes: 39 additions & 37 deletions src/parse/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Use `new_hist()` to start parsing and retrieve `Hist` enums.
use crate::{datetime::fmt_utctime, Show};
use crate::{datetime::fmt_utctime, Show, TimeBound};
use anyhow::{bail, ensure, Context, Error};
use crossbeam_channel::{bounded, Receiver, Sender};
use flate2::read::GzDecoder;
Expand Down Expand Up @@ -88,17 +88,17 @@ fn open_any_buffered(name: &str) -> Result<BufReader<Box<dyn std::io::Read + Sen

/// Parse emerge log into a channel of `Parsed` enums.
pub fn get_hist(file: &str,
min_ts: Option<i64>,
max_ts: Option<i64>,
min: TimeBound,
max: TimeBound,
show: Show,
search_terms: &Vec<String>,
search_exact: bool)
-> Result<Receiver<Hist>, Error> {
debug!("File: {file}");
debug!("Show: {show}");
let mut buf = open_any_buffered(file)?;
let (ts_min, ts_max) = filter_ts(min_ts, max_ts)?;
let (ts_min, ts_max) = filter_ts(file, min, max)?;
let filter = FilterStr::try_new(search_terms, search_exact)?;
let mut buf = open_any_buffered(file)?;
let (tx, rx): (Sender<Hist>, Receiver<Hist>) = bounded(256);
thread::spawn(move || {
let show_merge = show.merge || show.pkg || show.tot;
Expand Down Expand Up @@ -161,39 +161,41 @@ pub fn get_hist(file: &str,
Ok(rx)
}

/// Parse emerge log into a Vec of emerge command starts
///
/// This is a specialized version of get_hist(), about 20% faster for this usecase
pub fn get_cmd_times(file: &str,
min_ts: Option<i64>,
max_ts: Option<i64>)
-> Result<Vec<i64>, Error> {
let mut buf = open_any_buffered(file)?;
let (ts_min, ts_max) = filter_ts(min_ts, max_ts)?;
let mut line = Vec::with_capacity(255);
let mut res = vec![];
loop {
match buf.read_until(b'\n', &mut line) {
// End of file
Ok(0) => break,
// Got a line, see if one of the funs match it
Ok(_) => {
if let Some((t, s)) = parse_ts(&line, ts_min, ts_max) {
if s.starts_with(b"*** emerge") {
res.push(t)
/// Return min/max timestamp depending on options.
fn filter_ts(file: &str, min: TimeBound, max: TimeBound) -> Result<(i64, i64), Error> {
// Parse emerge log into a Vec of emerge command starts
// This is a specialized version of get_hist(), about 20% faster for this usecase
let mut cmds = vec![];
if matches!(min, TimeBound::Cmd(_)) || matches!(max, TimeBound::Cmd(_)) {
let mut buf = open_any_buffered(file)?;
let mut line = Vec::with_capacity(255);
loop {
match buf.read_until(b'\n', &mut line) {
Ok(0) => break,
Ok(_) => {
if let Some((t, s)) = parse_ts(&line, i64::MIN, i64::MAX) {
if s.starts_with(b"*** emerge") {
cmds.push(t)
}
}
}
},
// Could be invalid UTF8, system read error...
Err(_) => (),
},
Err(_) => (),
}
line.clear();
}
line.clear();
}
Ok(res)
}

/// Return min/max timestamp depending on options.
fn filter_ts(min: Option<i64>, max: Option<i64>) -> Result<(i64, i64), Error> {
// Convert to Option<int>
let min = match min {
TimeBound::Cmd(n) => cmds.iter().rev().nth(n).copied(),
TimeBound::Unix(n) => Some(n),
TimeBound::None => None,
};
let max = match max {
TimeBound::Cmd(n) => cmds.get(n).copied(),
TimeBound::Unix(n) => Some(n),
TimeBound::None => None,
};
// Check and log bounds, return result
match (min, max) {
(None, None) => debug!("Date: None"),
(Some(a), None) => debug!("Date: after {}", fmt_utctime(a)),
Expand Down Expand Up @@ -390,8 +392,8 @@ mod tests {
o => unimplemented!("Unknown test log file {:?}", o),
};
let hist = get_hist(&format!("tests/emerge.{}.log", file),
filter_mints,
filter_maxts,
filter_mints.map_or(TimeBound::None, |n| TimeBound::Unix(n)),
filter_maxts.map_or(TimeBound::None, |n| TimeBound::Unix(n)),
Show::parse(&String::from(show), "cptsmue", "test").unwrap(),
&filter_terms,
exact).unwrap();
Expand Down

0 comments on commit 463a906

Please sign in to comment.