From 155588457f8ddcb8bd711abdbaf2110bcac232fb Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 4 Nov 2024 10:26:44 +0000 Subject: [PATCH 1/7] log/parse: Show log of emerge commands This only looks at the start event, as experiments in a different branch showed that matching the end event is too unreliable. --- src/commands.rs | 9 ++++++++- src/config.rs | 2 +- src/config/cli.rs | 24 +++++++++++++----------- src/config/types.rs | 39 +++++++++++++++++++++++++++++++++------ src/parse/history.rs | 19 ++++++++++++++++++- tests/commands.rs | 8 +++++++- 6 files changed, 80 insertions(+), 21 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 9b76ad3..bef4958 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -10,13 +10,19 @@ 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(); - let mut found = 0; let mut sync_start: Option = None; + let mut found = 0; let h = ["Date", "Duration", "Package/Repo"]; let mut tbl = Table::new(&gc).align_left(0).align_left(2).margin(2, " ").last(sc.last).header(h); for p in hist { match p { + Hist::CmdStart { ts, args, .. } => { + found += 1; + if found <= sc.first { + tbl.row([&[&FmtDate(ts)], &[], &[&"Emerge ", &args]]); + } + }, Hist::MergeStart { ts, key, .. } => { // This'll overwrite any previous entry, if a merge started but never finished merges.insert(key, ts); @@ -186,6 +192,7 @@ pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { } } match p { + Hist::CmdStart { .. } => todo!("CmdStart"), Hist::MergeStart { ts, key, .. } => { merge_start.insert(key, ts); }, diff --git a/src/config.rs b/src/config.rs index 31553a4..428716f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -181,7 +181,7 @@ impl Conf { impl ConfLog { fn try_new(cli: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel!(cli, toml, log, show, "musa", Show::m())?, + Ok(Self { show: sel!(cli, toml, log, show, "cmusa", Show::m())?, search: cli.get_many("search").unwrap_or_default().cloned().collect(), exact: cli.get_flag("exact"), starttime: sel!(cli, toml, log, starttime, (), false)?, diff --git a/src/config/cli.rs b/src/config/cli.rs index 0328da7..d419761 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -30,17 +30,19 @@ pub fn build_cli() -> Command { virtual/rust: Matches only `virtual/rust`\n \ RuSt: Matches nothing (case-sensitive)\n \ ru: Matches nothing (whole name only)"); - let show_l = Arg::new("show").short('s') - .long("show") - .value_name("m,u,s,a") - .display_order(3) - .help_heading("Filter") - .help("Show (m)erges, (u)nmerges, (s)yncs, and/or (a)ll") - .long_help("Show (any combination of)\n \ - m: Package merges\n \ - u: Package unmerges\n \ - s: Repository syncs\n \ - a: All of the above"); + let show_l = + Arg::new("show").short('s') + .long("show") + .value_name("c,m,u,s,a") + .display_order(3) + .help_heading("Filter") + .help("Show (c)commands, (m)erges, (u)nmerges, (s)yncs, and/or (a)ll") + .long_help("Show (any combination of)\n \ + c: Emerge command\n \ + m: Package merges\n \ + u: Package unmerges\n \ + s: Repository syncs\n \ + a: All of the above"); let show_s = Arg::new("show").short('s') .long("show") .value_name("p,t,s,a") diff --git a/src/config/types.rs b/src/config/types.rs index 826167e..42382d4 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -184,6 +184,7 @@ impl ArgParse for OutStyle { #[derive(Clone, Copy)] pub struct Show { + pub cmd: bool, pub pkg: bool, pub tot: bool, pub sync: bool, @@ -193,23 +194,48 @@ pub struct Show { } impl Show { pub const fn m() -> Self { - Self { pkg: false, tot: false, sync: false, merge: true, unmerge: false, emerge: false } + Self { cmd: false, + pkg: false, + tot: false, + sync: false, + merge: true, + unmerge: false, + emerge: false } } pub const fn emt() -> Self { - Self { pkg: false, tot: true, sync: false, merge: true, unmerge: false, emerge: true } + Self { cmd: false, + pkg: false, + tot: true, + sync: false, + merge: true, + unmerge: false, + emerge: true } } pub const fn p() -> Self { - Self { pkg: true, tot: false, sync: false, merge: false, unmerge: false, emerge: false } + Self { cmd: false, + pkg: true, + tot: false, + sync: false, + merge: false, + unmerge: false, + emerge: false } } pub const fn mt() -> Self { - Self { pkg: false, tot: true, sync: false, merge: true, unmerge: false, emerge: false } + Self { cmd: false, + pkg: false, + tot: true, + sync: false, + merge: true, + unmerge: false, + emerge: false } } } impl ArgParse for Show { fn parse(show: &String, valid: &'static str, src: &'static str) -> Result { debug_assert!(valid.is_ascii()); // Because we use `chars()` we need to stick to ascii for `valid`. if show.chars().all(|c| valid.contains(c)) { - Ok(Self { pkg: show.contains('p') || show.contains('a'), + Ok(Self { cmd: show.contains('c') || show.contains('a'), + pkg: show.contains('p') || show.contains('a'), tot: show.contains('t') || show.contains('a'), sync: show.contains('s') || show.contains('a'), merge: show.contains('m') || show.contains('a'), @@ -223,7 +249,8 @@ impl ArgParse for Show { impl std::fmt::Display for Show { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut sep = ""; - for (b, s) in [(self.pkg, "pkg"), + for (b, s) in [(self.cmd, "command"), + (self.pkg, "pkg"), (self.tot, "total"), (self.sync, "sync"), (self.merge, "merge"), diff --git a/src/parse/history.rs b/src/parse/history.rs index 81cdab1..1ea379c 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -16,6 +16,9 @@ use std::{fs::File, /// Items sent on the channel returned by `new_hist()`. #[derive(Debug)] pub enum Hist { + /// Command started (might never complete). + // There's no CmdStop, because matching a Stop to the correct Start is too unreliable + CmdStart { ts: i64, args: String }, /// Merge started (might never complete). MergeStart { ts: i64, key: String, pos: usize }, /// Merge completed. @@ -59,6 +62,7 @@ impl Hist { } pub const fn ts(&self) -> i64 { match self { + Self::CmdStart { ts, .. } => *ts, Self::MergeStart { ts, .. } => *ts, Self::MergeStop { ts, .. } => *ts, Self::UnmergeStart { ts, .. } => *ts, @@ -140,6 +144,10 @@ pub fn get_hist(file: &str, if tx.send(found).is_err() { break; } + } else if let Some(found) = parse_cmdstart(show.cmd, t, s) { + if tx.send(found).is_err() { + break; + } } } }, @@ -244,6 +252,13 @@ fn parse_ts(line: &[u8], min: i64, max: i64) -> Option<(i64, &[u8])> { } } +fn parse_cmdstart(enabled: bool, ts: i64, line: &[u8]) -> Option { + if !enabled || !line.starts_with(b"*** emerge") { + return None; + } + Some(Hist::CmdStart { ts, args: from_utf8(&line[11..]).ok()?.trim().to_owned() }) +} + fn parse_mergestart(enabled: bool, ts: i64, line: &[u8], filter: &FilterStr) -> Option { if !enabled || !line.starts_with(b">>> emer") { return None; @@ -347,7 +362,8 @@ mod tests { let hist = get_hist(&format!("tests/emerge.{}.log", file), filter_mints, filter_maxts, - Show { pkg: false, + Show { cmd: false, + pkg: false, tot: false, sync: parse_sync, merge: parse_merge, @@ -361,6 +377,7 @@ mod tests { // Check that all items look valid for p in hist { let (kind, ts, ebuild, version) = match p { + Hist::CmdStart { ts, .. } => ("CStart", ts, "c/e", "1"), Hist::MergeStart { ts, .. } => ("MStart", ts, p.ebuild(), p.version()), Hist::MergeStop { ts, .. } => ("MStop", ts, p.ebuild(), p.version()), Hist::UnmergeStart { ts, .. } => ("UStart", ts, p.ebuild(), p.version()), diff --git a/tests/commands.rs b/tests/commands.rs index 157f907..e3da506 100644 --- a/tests/commands.rs +++ b/tests/commands.rs @@ -100,11 +100,17 @@ fn log() { 2020-06-18 16:21:56 1 Sync moltonel\n"), // Check output of all events ("%F10000.log l --show a --from 2018-03-07T10:42:00 --to 2018-03-07T14:00:00 -oc", - "2018-03-07 10:43:10 14 >>> sys-apps/the_silver_searcher-2.0.0\n\ + "2018-03-07 10:42:51 Emerge --backtrack=100 --quiet-build=y sys-apps/the_silver_searcher\n\ + 2018-03-07 10:43:10 14 >>> sys-apps/the_silver_searcher-2.0.0\n\ + 2018-03-07 11:36:27 Emerge --quiet-build=y --sync\n\ 2018-03-07 11:37:05 38 Sync gentoo\n\ + 2018-03-07 11:38:29 Emerge --deep --backtrack=100 --quiet-build=y --ask --update --jobs=2 --newuse --verbose world\n\ 2018-03-07 12:49:09 2 <<< sys-apps/util-linux-2.30.2\n\ 2018-03-07 12:49:13 1:01 >>> sys-apps/util-linux-2.30.2-r1\n\ + 2018-03-07 13:55:29 Emerge --quiet-build=y --sync\n\ 2018-03-07 13:56:09 40 Sync gentoo\n\ + 2018-03-07 13:57:31 Emerge --update --jobs=2 --backtrack=100 --ask --quiet-build=y --verbose --deep --newuse world\n\ + 2018-03-07 13:58:04 Emerge --update --verbose --newuse --quiet-build=y --deep --backtrack=100 world\n\ 2018-03-07 13:59:38 2 <<< dev-libs/nspr-4.17\n\ 2018-03-07 13:59:41 24 >>> dev-libs/nspr-4.18\n"), // skip first From 8cef71b7914d619e9a6e82889d0ac6e599efb43b Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Thu, 14 Nov 2024 12:55:15 +0000 Subject: [PATCH 2/7] test: Shorter more flexible chk_hist() --- src/parse/history.rs | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/parse/history.rs b/src/parse/history.rs index 1ea379c..d925aec 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -337,13 +337,12 @@ fn parse_syncstop(enabled: bool, ts: i64, line: &[u8], filter: &FilterStr) -> Op #[cfg(test)] mod tests { use super::*; + use crate::ArgParse; use std::collections::HashMap; /// This checks parsing the given emerge.log. fn chk_hist(file: &str, - parse_merge: bool, - parse_unmerge: bool, - parse_sync: bool, + show: &str, filter_mints: Option, filter_maxts: Option, filter_terms: Vec, @@ -362,13 +361,7 @@ mod tests { let hist = get_hist(&format!("tests/emerge.{}.log", file), filter_mints, filter_maxts, - Show { cmd: false, - pkg: false, - tot: false, - sync: parse_sync, - merge: parse_merge, - unmerge: parse_unmerge, - emerge: false }, + Show::parse(&String::from(show), "cptsmue", "test").unwrap(), &filter_terms, exact).unwrap(); let re_atom = Regex::new("^[a-zA-Z0-9-]+/[a-zA-Z0-9_+-]+$").unwrap(); @@ -406,14 +399,14 @@ mod tests { /// Simplified emerge log containing all the ebuilds in all the versions of the current portage tree (see test/generate.sh) fn parse_hist_all() { let t = vec![("MStart", 31467)]; - chk_hist("all", true, false, false, None, None, vec![], false, t); + chk_hist("all", "m", None, None, vec![], false, t); } #[test] /// Emerge log with various invalid data fn parse_hist_nullbytes() { let t = vec![("MStart", 14), ("MStop", 14)]; - chk_hist("nullbytes", true, false, false, None, None, vec![], false, t); + chk_hist("nullbytes", "m", None, None, vec![], false, t); } #[test] @@ -424,7 +417,7 @@ mod tests { ("media-libs/jpeg", 1), //letter in timestamp ("dev-libs/libical", 2), ("media-libs/libpng", 2)]; - chk_hist("badtimestamp", true, false, false, None, None, vec![], false, t); + chk_hist("badtimestamp", "m", None, None, vec![], false, t); } #[test] @@ -435,7 +428,7 @@ mod tests { ("media-libs/jpeg", 2), ("dev-libs/libical", 2), ("media-libs/libpng", 1)]; //missing version - chk_hist("badversion", true, false, false, None, None, vec![], false, t); + chk_hist("badversion", "m", None, None, vec![], false, t); } #[test] @@ -446,7 +439,7 @@ mod tests { ("media-libs/jpeg", 2), ("dev-libs/libical", 1), //missing end of line and spaces in iter ("media-libs/libpng", 2)]; - chk_hist("shortline", true, false, false, None, None, vec![], false, t); + chk_hist("shortline", "m", None, None, vec![], false, t); } #[test] @@ -456,13 +449,17 @@ mod tests { let m = (i & 0b001) == 0; let u = (i & 0b010) == 0; let s = (i & 0b100) == 0; + let show = format!("{}{}{}", + if m { "m" } else { "" }, + if u { "u" } else { "" }, + if s { "s" } else { "" }); let t = vec![("MStart", if m { 889 } else { 0 }), ("MStop", if m { 832 } else { 0 }), ("UStart", if u { 832 } else { 0 }), ("UStop", if u { 832 } else { 0 }), ("SStart", if s { 326 } else { 0 }), ("SStop", if s { 150 } else { 0 })]; - chk_hist("10000", m, u, s, None, None, vec![], false, t); + chk_hist("10000", &show, None, None, vec![], false, t); } } @@ -494,7 +491,7 @@ mod tests { ("SStart", 326), ("SStop", s2)]; let terms = f.split_whitespace().map(str::to_string).collect(); - chk_hist("10000", true, true, true, None, None, terms, e, c); + chk_hist("10000", "mus", None, None, terms, e, c); } } @@ -522,7 +519,7 @@ mod tests { ("UStop", u2), ("SStart", s1), ("SStop", s2)]; - chk_hist("10000", true, true, true, min, max, vec![], true, c); + chk_hist("10000", "mus", min, max, vec![], true, c); } } From 2c431676cde23e363791f4e5e1c8694d0e21edc9 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Thu, 14 Nov 2024 13:19:40 +0000 Subject: [PATCH 3/7] stats: Display basic emerge command stats I played around with more detailed stats, but they didn't seem that useful in the end, so for now I'm just classifying commands as merge/clean/sync. Note that portage doesn't log all commands, so the available data isn't as right as I thought. --- src/commands.rs | 59 ++++++++++++++++++++++++++++++++++++++------ src/config.rs | 2 +- src/config/cli.rs | 24 +++++++++--------- src/parse/history.rs | 17 +++++++------ tests/commands.rs | 6 +++-- 5 files changed, 80 insertions(+), 28 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index bef4958..a29bdd4 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -143,12 +143,37 @@ impl Times { } } +/// Classify emerge commands by looking at their args. +/// +/// Note that some commands don't get logged at all, so this enum is quite limited. +#[derive(PartialEq, Eq, PartialOrd, Ord)] +enum ArgKind { + All, + Merge, + Clean, + Sync, +} +impl ArgKind { + fn new(args: &str) -> Self { + for arg in args.split_ascii_whitespace() { + match arg { + "--deselect" | "--unmerge" | "--clean" | "--depclean" => return Self::Clean, + "--sync" => return Self::Sync, + _ => (), + } + } + Self::Merge + } +} + /// Summary display of merge events /// /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; + let h = [sc.group.name(), "Logged emerges", "Install/Update", "Unmerge/Clean", "Sync"]; + let mut tblc = Table::new(&gc).margin(1, " ").header(h); let h = [sc.group.name(), "Repo", "Syncs", "Total time", "Predict time"]; let mut tbls = Table::new(&gc).align_left(0).align_left(1).margin(1, " ").header(h); let h = [sc.group.name(), @@ -173,6 +198,7 @@ pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { let mut pkg_time: BTreeMap = BTreeMap::new(); let mut sync_start: Option = None; let mut sync_time: BTreeMap = BTreeMap::new(); + let mut cmd_args: BTreeMap = BTreeMap::new(); let mut nextts = 0; let mut curts = 0; for p in hist { @@ -183,16 +209,20 @@ pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { curts = t; } else if t > nextts { let group = sc.group.at(curts, gc.date_offset); - cmd_stats_group(&gc, &sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, - &pkg_time); + cmd_stats_group(&gc, &sc, &mut tblc, &mut tbls, &mut tblp, &mut tblt, group, + &cmd_args, &sync_time, &pkg_time); sync_time.clear(); pkg_time.clear(); + cmd_args.clear(); nextts = sc.group.next(t, gc.date_offset); curts = t; } } match p { - Hist::CmdStart { .. } => todo!("CmdStart"), + Hist::CmdStart { args, .. } => { + *cmd_args.entry(ArgKind::All).or_insert(0) += 1; + *cmd_args.entry(ArgKind::new(&args)).or_insert(0) += 1; + }, Hist::MergeStart { ts, key, .. } => { merge_start.insert(key, ts); }, @@ -228,15 +258,20 @@ pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { } } let group = sc.group.at(curts, gc.date_offset); - cmd_stats_group(&gc, &sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, &pkg_time); + cmd_stats_group(&gc, &sc, &mut tblc, &mut tbls, &mut tblp, &mut tblt, group, &cmd_args, + &sync_time, &pkg_time); // Controlled drop to ensure table order and insert blank lines - let (es, ep, et) = (!tbls.is_empty(), !tblp.is_empty(), !tblt.is_empty()); + let (ec, es, ep, et) = (!tblc.is_empty(), !tbls.is_empty(), !tblp.is_empty(), !tblt.is_empty()); + drop(tblc); + if ec && es { + println!(); + } drop(tbls); - if es && ep { + if (ec || es) && ep { println!(); } drop(tblp); - if (es || ep) && et { + if (ec || es || ep) && et { println!(); } drop(tblt); @@ -247,12 +282,22 @@ pub fn cmd_stats(gc: Conf, sc: ConfStats) -> Result { #[allow(clippy::too_many_arguments)] fn cmd_stats_group(gc: &Conf, sc: &ConfStats, + tblc: &mut Table<5>, tbls: &mut Table<5>, tblp: &mut Table<8>, tblt: &mut Table<7>, group: String, + cmd_args: &BTreeMap, sync_time: &BTreeMap, pkg_time: &BTreeMap) { + // Commands + if sc.show.cmd && !cmd_args.is_empty() { + tblc.row([&[&group], + &[&gc.cnt, cmd_args.get(&ArgKind::All).unwrap_or(&0)], + &[&gc.cnt, cmd_args.get(&ArgKind::Merge).unwrap_or(&0)], + &[&gc.cnt, cmd_args.get(&ArgKind::Clean).unwrap_or(&0)], + &[&gc.cnt, cmd_args.get(&ArgKind::Sync).unwrap_or(&0)]]); + } // Syncs if sc.show.sync && !sync_time.is_empty() { for (repo, time) in sync_time { diff --git a/src/config.rs b/src/config.rs index 428716f..4fd4740 100644 --- a/src/config.rs +++ b/src/config.rs @@ -220,7 +220,7 @@ impl ConfPred { impl ConfStats { fn try_new(cli: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel!(cli, toml, stats, show, "ptsa", Show::p())?, + Ok(Self { show: sel!(cli, toml, stats, show, "cptsa", Show::p())?, search: cli.get_many("search").unwrap_or_default().cloned().collect(), exact: cli.get_flag("exact"), lim: sel!(cli, toml, stats, limit, 1..=65000, 10)? as u16, diff --git a/src/config/cli.rs b/src/config/cli.rs index d419761..ce5bf61 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -43,17 +43,19 @@ pub fn build_cli() -> Command { u: Package unmerges\n \ s: Repository syncs\n \ a: All of the above"); - let show_s = Arg::new("show").short('s') - .long("show") - .value_name("p,t,s,a") - .display_order(3) - .help_heading("Filter") - .help("Show (p)ackages, (t)otals, (s)yncs, and/or (a)ll") - .long_help("Show (any combination of)\n \ - p: Individual package merges/unmerges\n \ - t: Total package merges/unmerges\n \ - s: Repository syncs\n \ - a: All of the above"); + let show_s = + Arg::new("show").short('s') + .long("show") + .value_name("c,p,t,s,a") + .display_order(3) + .help_heading("Filter") + .help("Show (c)commands, (p)ackages, (t)otals, (s)yncs, and/or (a)ll") + .long_help("Show (any combination of)\n \ + c: Emerge commands\n \ + p: Individual package merges/unmerges\n \ + t: Total package merges/unmerges\n \ + s: Repository syncs\n \ + a: All of the above"); let show_p = Arg::new("show").short('s') .long("show") .value_name("e,m,t,a") diff --git a/src/parse/history.rs b/src/parse/history.rs index d925aec..5b039b9 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -443,17 +443,20 @@ mod tests { } #[test] - /// Basic counts, with every combination of merge/unmerge/sync + /// Basic counts, with every combination of command/merge/unmerge/sync fn parse_hist_nofilter() { - for i in 0..8 { - let m = (i & 0b001) == 0; - let u = (i & 0b010) == 0; - let s = (i & 0b100) == 0; - let show = format!("{}{}{}", + for i in 0..16 { + let c = (i & 0b0001) == 0; + let m = (i & 0b0010) == 0; + let u = (i & 0b0100) == 0; + let s = (i & 0b1000) == 0; + let show = format!("{}{}{}{}", + if c { "c" } else { "" }, if m { "m" } else { "" }, if u { "u" } else { "" }, if s { "s" } else { "" }); - let t = vec![("MStart", if m { 889 } else { 0 }), + let t = vec![("CStart", if c { 450 } else { 0 }), + ("MStart", if m { 889 } else { 0 }), ("MStop", if m { 832 } else { 0 }), ("UStart", if u { 832 } else { 0 }), ("UStop", if u { 832 } else { 0 }), diff --git a/tests/commands.rs b/tests/commands.rs index e3da506..daca056 100644 --- a/tests/commands.rs +++ b/tests/commands.rs @@ -292,7 +292,9 @@ fn stats() { 0), ("%F10000.log s client -sst -oc", "11 24:00:24 2:10:56 10 27 2\n", 0), ("%F10000.log s client -sa -oc", - "kde-frameworks/kxmlrpcclient 2 47 23 2 4 2\n\ + "450 267 20 163\n\ + \n\ + kde-frameworks/kxmlrpcclient 2 47 23 2 4 2\n\ mail-client/thunderbird 2 1:23:44 41:52 2 6 3\n\ www-client/chromium 3 21:41:24 7:42:07 3 12 3\n\ www-client/falkon 1 6:02 6:02 0 0 ?\n\ @@ -494,7 +496,7 @@ fn negative_merge_time() { 2019-06-05 10:21:02 ? >>> kde-plasma/kwin-5.15.5\n\ 2019-06-08 21:33:36 3:10 >>> kde-plasma/kwin-5.15.5\n")), // For `stats` the negative merge time is used for count but ignored for tottime/predtime. - ("%Fnegtime.log s -sa -oc", + ("%Fnegtime.log s -sstp -oc", format!("gentoo 2 1:06 1:06\n\ \n\ kde-apps/libktnef 1 26 26 0 0 ?\n\ From 4ac3beb569120d2f9a948f4228befc5cf997e43f Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Thu, 21 Nov 2024 10:09:34 +0000 Subject: [PATCH 4/7] log: Introduce `--lastmerge` option This currently somewhat mirrors what qlop offers, as an MVP to get a feel of the implementation. I initially wrote a single-pass version (droping previous table rows when encountering a command), but the two-pass version is fast enough that `--lastmerge` is about as fast as `--last`, and offers more flexibility. Tentative todo: * Allow `--lastmerge ` * Move into `--from '1 command'` (simpler and more consistent) * Optionaly ignore non-merge commands (qlop always/only does that, but it's a failible heuristic) * Investigate `--to` support * Move code into get_hist (DRY, and optimization opportunities) * Test and document --- src/commands.rs | 7 ++++++- src/config.rs | 4 +++- src/config/cli.rs | 8 +++++++- src/parse.rs | 2 +- src/parse/history.rs | 31 +++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index a29bdd4..437da91 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,7 +6,12 @@ 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(gc: Conf, sc: ConfLog) -> Result { +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); + } + } 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 4fd4740..9733945 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,6 +47,7 @@ pub struct ConfLog { pub starttime: bool, pub first: usize, pub last: usize, + pub lastmerge: bool, } pub struct ConfPred { pub show: Show, @@ -186,7 +187,8 @@ 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) }) + last: *cli.get_one("last").unwrap_or(&usize::MAX), + lastmerge: cli.get_flag("lastmerge") }) } } diff --git a/src/config/cli.rs b/src/config/cli.rs index ce5bf61..1eb45a2 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -127,6 +127,11 @@ 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 \ @@ -140,7 +145,7 @@ pub fn build_cli() -> Command { .hide_possible_values(true) .num_args(..=1) .default_missing_value("either") - .display_order(8) + .display_order(9) .help_heading("Filter") .help(h.split_once('\n').unwrap().0) .long_help(h); @@ -337,6 +342,7 @@ pub fn build_cli() -> Command { .arg(starttime) .arg(&first) .arg(&last) + .arg(lastmerge) .arg(show_l) .arg(&exact) .arg(&pkg); diff --git a/src/parse.rs b/src/parse.rs index fb5fd3b..ad5cbd4 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_hist, Hist}; +pub use history::{get_cmd_times, 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 5b039b9..d7baf62 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -161,6 +161,37 @@ 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) + } + } + }, + // Could be invalid UTF8, system read error... + Err(_) => (), + } + line.clear(); + } + Ok(res) +} + /// Return min/max timestamp depending on options. fn filter_ts(min: Option, max: Option) -> Result<(i64, i64), Error> { match (min, max) { From 4f0de6d8d102ed766c17ef2bf796d957f975aefb Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 22 Nov 2024 14:08:34 +0000 Subject: [PATCH 5/7] 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 | 66 +++++++++++++++++++++++++++----------- src/parse.rs | 2 +- src/parse/history.rs | 76 +++++++++++++++++++++++--------------------- 6 files changed, 103 insertions(+), 81 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..7379603 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() @@ -315,22 +342,25 @@ mod test { 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(); From 828c91cc26bec27f493b2563ee51fcbdb09fcb7a Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 26 Nov 2024 13:57:20 +0000 Subject: [PATCH 6/7] cli: Make `--from=c` 1-based, allow `-fc` as synonym of `-f1c` * Users expect `--from=1c` to start at the last command * Reject `0c`, accept `c` * Add unittest, tweak inline help --- src/config/cli.rs | 4 ++-- src/datetime.rs | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/config/cli.rs b/src/config/cli.rs index 52fbfa1..dc72f3a 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -81,7 +81,7 @@ pub fn build_cli() -> Command { 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\n \ - 1c|2 commands Emerge command"; + 1c|2 commands|c Nth emerge command"; let from = Arg::new("from").short('f') .long("from") .value_name("date") @@ -95,7 +95,7 @@ pub fn build_cli() -> Command { 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\n \ - 1c|2 commands Emerge command"; + 1c|2 commands|c Nth-last emerge command"; let to = Arg::new("to").short('t') .long("to") .value_name("date") diff --git a/src/datetime.rs b/src/datetime.rs index 7379603..3701a8c 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -111,15 +111,14 @@ impl ArgParse for TimeBound { } } -/// Parse a command index +/// Parse a command index (parse as 1-based, return as 0-based) 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"), + let (num, pos) = usize::from_radix_10(s.as_bytes()); + match s[pos..].trim() { + "c" | "command" | "commands" if num > 0 => Ok(num - 1), + "c" | "command" if pos == 0 => Ok(0), + _ => bail!("bad span {:?}", &s[pos..]), } } @@ -371,6 +370,19 @@ mod test { assert!(parse_date("a while ago", tz_utc).is_err()); } + #[test] + fn command_num() { + assert_eq!(parse_command_num("1c").unwrap(), 0); + assert_eq!(parse_command_num("5c").unwrap(), 4); + assert_eq!(parse_command_num("c").unwrap(), 0); + assert_eq!(parse_command_num("1 commands ").unwrap(), 0); + assert!(parse_command_num("").is_err()); + assert!(parse_command_num("0c").is_err()); + assert!(parse_command_num("0").is_err()); + assert!(parse_command_num("1cool").is_err()); + assert!(parse_command_num("-1c").is_err()); + } + #[test] fn timespan_next_() { for t in [// input year month week day From 541c0f39f6caede77028cf69767ce79e61be24ff Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 26 Nov 2024 14:04:57 +0000 Subject: [PATCH 7/7] Update docs, add `--from=1c` benchmark Appended a non-sync emerge command to `benches/emerge.log` so that qlop finds something to display. This is a nice illustration of how unreliable any "detect end of command" heuristics is. It comforts me in the decision to not exactly match qlop's `--lastmerge` behavior (ignoring emerge commands that didn't actually merge something): I value correctness over simplicity. --- CHANGELOG.md | 4 ++++ benches/emerge.log | 14 ++++++++++++++ benches/exec_compare.rs | 2 ++ docs/COMPARISON.md | 20 ++++++++++---------- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cefe92..1d286ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## New features +* `log` and `stat` can now show emerge command start events + - Not end events, as `emerge.log` doesn't provide enough info to make this reliable +* `--from` and `--to` now accept a command index as argument + - `--from=1command` or `-fc` is roughly equivalent to qlop's `--lastmerge` * `predict` now displays emerge proces tree instead of just top proces - Bevahvior configurable with `--pdepth`, `--pwidth` - Format is a bit nicer and more colorful diff --git a/benches/emerge.log b/benches/emerge.log index c274288..6bf6d9f 100644 --- a/benches/emerge.log +++ b/benches/emerge.log @@ -100008,3 +100008,17 @@ 1602189661: >>> Syncing repository 'moltonel' into '/var/db/repos/moltonel'... 1602189662: === Sync completed for moltonel 1602189663: *** terminating. +1602190319: Started emerge on: Mar 10, 2019 15:55:18 +1602190319: *** emerge --newuse --update --backtrack=100 --deep --quiet-build=y --verbose world +1602190389: >>> emerge (1 of 1) www-client/chromium-72.0.3626.121 to / +1602190389: === (1 of 1) Cleaning (www-client/chromium-72.0.3626.121::/usr/portage/www-client/chromium/chromium-72.0.3626.121.ebuild) +1602190391: === (1 of 1) Compiling/Merging (www-client/chromium-72.0.3626.121::/usr/portage/www-client/chromium/chromium-72.0.3626.121.ebuild) +1602190620: === (1 of 1) Merging (www-client/chromium-72.0.3626.121::/usr/portage/www-client/chromium/chromium-72.0.3626.121.ebuild) +1602190624: >>> AUTOCLEAN: www-client/chromium:0 +1602190624: === Unmerging... (www-client/chromium-72.0.3626.96) +1602190626: >>> unmerge success: www-client/chromium-72.0.3626.96 +1602190635: === (1 of 1) Post-Build Cleaning (www-client/chromium-72.0.3626.121::/usr/portage/www-client/chromium/chromium-72.0.3626.121.ebuild) +1602190635: ::: completed emerge (1 of 1) www-client/chromium-72.0.3626.121 to / +1602190635: *** Finished. Cleaning up... +1602190636: *** exiting successfully. +1602190637: *** terminating. diff --git a/benches/exec_compare.rs b/benches/exec_compare.rs index b453a21..3fb32d3 100755 --- a/benches/exec_compare.rs +++ b/benches/exec_compare.rs @@ -61,6 +61,8 @@ fn main() { ("ld2", "genlop", &["-f","{emerge.log}","-l", "--date","2020-10-01","--date","2020-10-31"], None), ("ld2", "qlop", &["-f","{emerge.log}","-mv","--date","2020-10-01","--date","2020-10-31"], None), ("ld2", "emlop", &["-F","{emerge.log}","l", "--from","2020-10-01","--to", "2020-10-31"], None), + ("ldl", "qlop", &["-f","{emerge.log}","-mv","--lastmerge"], None), + ("ldl", "emlop", &["-F","{emerge.log}","l","--from=1c"], None), // Force/prevent color output ("lc", "qlop", &["-f","{emerge.log}","-mv","--color"], None), ("lc", "emlop", &["-F","{emerge.log}","l","--color=y"], None), diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index d04fda1..628b3ce 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -89,16 +89,16 @@ For relative dates, genlop accepts fancy strings like "last month" or "2 weeks a less flexible but less verbose (no "ago" needed), and emlop only accepts a number of days/weeks/etc which can be abbreviated (for example "1 week, 3 days" -> "1w3d"). -| | genlop | qlop | emlop | -|:-----------------------------------------|:-----------:|:-----:|:-----------:| -| Limit log parsing by date | yes | yes | yes | -| Limit log to number fisrt/last n entries | no | no | yes | -| Limit log to last emerge operation | no | yes | no | -| Filter by package categ/name | yes | yes | yes | -| Filter by sync repo | no | no | yes | -| Read filter list from file | no | yes | no | -| Search modes | plain/regex | plain | plain/regex | -| Default search mode | plain | plain | regex | +| | genlop | qlop | emlop | +|:-----------------------------------------|:-----------:|:---------:|:-----------:| +| Limit log parsing by date | yes | yes | yes | +| Limit log to number fisrt/last n entries | no | no | yes | +| Limit log to nth emerge operation | no | last only | yes | +| Filter by package categ/name | yes | yes | yes | +| Filter by sync repo | no | no | yes | +| Read filter list from file | no | yes | no | +| Search modes | plain/regex | plain | plain/regex | +| Default search mode | plain | plain | regex | ## Merge time prediction