From 463a9069f8beb69761266e70c9ef5ffe2d65d6f5 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 22 Nov 2024 14:08:34 +0000 Subject: [PATCH] cli/parse: Refactor `--lastmerge` into `--from`/`--to` * 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`. --- src/commands.rs | 7 +--- src/config.rs | 17 +++++----- src/config/cli.rs | 16 ++++------ src/datetime.rs | 68 ++++++++++++++++++++++++++++----------- src/parse.rs | 2 +- src/parse/history.rs | 76 +++++++++++++++++++++++--------------------- 6 files changed, 104 insertions(+), 82 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 437da91..a29bdd4 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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 { - 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 { let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; let mut merges: HashMap = HashMap::new(); let mut unmerges: HashMap = HashMap::new(); diff --git a/src/config.rs b/src/config.rs index 9733945..689bbca 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,8 +37,8 @@ pub struct Conf { pub date_fmt: DateStyle, pub out: OutStyle, pub logfile: String, - pub from: Option, - pub to: Option, + pub from: TimeBound, + pub to: TimeBound, } pub struct ConfLog { pub show: Show, @@ -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, @@ -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 { "<<< " }), @@ -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) }) } } diff --git a/src/config/cli.rs b/src/config/cli.rs index 1eb45a2..52fbfa1 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -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 \n \ + let h = "Only parse log entries after \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") @@ -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 \n \ + let h = "Only parse log entries before \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") @@ -127,11 +129,6 @@ pub fn build_cli() -> Command { .long_help("Show only the last 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 \ @@ -342,7 +339,6 @@ pub fn build_cli() -> Command { .arg(starttime) .arg(&first) .arg(&last) - .arg(lastmerge) .arg(show_l) .arg(&exact) .arg(&pkg); diff --git a/src/datetime.rs b/src/datetime.rs index 9cf2aa4..214898e 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -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 for i64 { +impl ArgParse for TimeBound { fn parse(val: &String, offset: UtcOffset, src: &'static str) -> Result { 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 { + 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 { 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"); @@ -138,7 +165,7 @@ fn parse_date_ago(s: &str) -> Result { } if !at_least_one { - bail!("No token found"); + bail!("no token found"); } Ok(now.unix_timestamp()) } @@ -180,7 +207,7 @@ fn parse_date_yyyymmdd(s: &str, offset: UtcOffset) -> Result { ])) ])?; if !rest.is_empty() { - bail!("Junk at end") + bail!("junk at end") } Ok(OffsetDateTime::try_from(p)?.unix_timestamp()) } @@ -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::parse(&String::from(s), o, "") + fn parse_date(s: &str, o: UtcOffset) -> Result { + TimeBound::parse(&String::from(s), o, "") } fn ts(t: OffsetDateTime) -> i64 { t.unix_timestamp() @@ -313,24 +340,27 @@ mod test { let now = epoch_now(); 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()); diff --git a/src/parse.rs b/src/parse.rs index ad5cbd4..fb5fd3b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -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}; diff --git a/src/parse/history.rs b/src/parse/history.rs index d7baf62..894c8e1 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -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; @@ -88,17 +88,17 @@ fn open_any_buffered(name: &str) -> Result, - max_ts: Option, + min: TimeBound, + max: TimeBound, show: Show, search_terms: &Vec, search_exact: bool) -> Result, 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, Receiver) = bounded(256); thread::spawn(move || { let show_merge = show.merge || show.pkg || show.tot; @@ -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, - max_ts: Option) - -> Result, 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, max: Option) -> Result<(i64, i64), Error> { + // Convert to Option + 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)), @@ -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();