feat(history): Add new flag to allow custom output format (#662)

* feat(history): Add new flag to allow custom output format

* more efficient formatting

* add user and host

* docs

Co-authored-by: Conrad Ludgate <conrad.ludgate@truelayer.com>
This commit is contained in:
Baptiste 2023-01-26 11:57:52 +01:00 committed by GitHub
parent a8ed8f1325
commit 893a395f12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 36 deletions

18
Cargo.lock generated
View file

@ -91,6 +91,7 @@ dependencies = [
"itertools", "itertools",
"log", "log",
"rpassword", "rpassword",
"runtime-format",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@ -1647,6 +1648,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "runtime-format"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b035308411b1af4683acc4fc928366443f08b893bb73e235c85de4c2be572495"
dependencies = [
"tinyvec",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.36.5" version = "0.36.5"
@ -2104,18 +2114,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.34" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.34" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -72,6 +72,7 @@ fs-err = "2.7"
whoami = "1.1.2" whoami = "1.1.2"
rpassword = "7.0" rpassword = "7.0"
semver = "1.0.14" semver = "1.0.14"
runtime-format = "0.1.2"
[dependencies.tracing-subscriber] [dependencies.tracing-subscriber]
version = "0.3" version = "0.3"

View file

@ -1,11 +1,13 @@
use std::{ use std::{
env, env,
fmt::{self, Display},
io::{StdoutLock, Write}, io::{StdoutLock, Write},
time::Duration, time::Duration,
}; };
use clap::Subcommand; use clap::Subcommand;
use eyre::Result; use eyre::Result;
use runtime_format::{FormatKey, FormatKeyError, ParsedFmt};
use atuin_client::{ use atuin_client::{
database::{current_context, Database}, database::{current_context, Database},
@ -17,7 +19,7 @@ use atuin_client::{
use atuin_client::sync; use atuin_client::sync;
use log::debug; use log::debug;
use super::search::format_duration; use super::search::format_duration_into;
#[derive(Subcommand)] #[derive(Subcommand)]
#[command(infer_subcommands = true)] #[command(infer_subcommands = true)]
@ -46,6 +48,11 @@ pub enum Cmd {
/// Show only the text of the command /// Show only the text of the command
#[arg(long)] #[arg(long)]
cmd_only: bool, cmd_only: bool,
/// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
#[arg(long, short)]
format: Option<String>,
}, },
/// Get the last command ran /// Get the last command ran
@ -56,6 +63,11 @@ pub enum Cmd {
/// Show only the text of the command /// Show only the text of the command
#[arg(long)] #[arg(long)]
cmd_only: bool, cmd_only: bool,
/// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
#[arg(long, short)]
format: Option<String>,
}, },
} }
@ -79,43 +91,76 @@ impl ListMode {
} }
#[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_sign_loss)]
pub fn print_list(h: &[History], list_mode: ListMode) { pub fn print_list(h: &[History], list_mode: ListMode, format: Option<&str>) {
let w = std::io::stdout(); let w = std::io::stdout();
let mut w = w.lock(); let mut w = w.lock();
match list_mode { match list_mode {
ListMode::Human => print_human_list(&mut w, h), ListMode::Human => print_human_list(&mut w, h, format),
ListMode::CmdOnly => print_cmd_only(&mut w, h), ListMode::CmdOnly => print_cmd_only(&mut w, h),
ListMode::Regular => print_regular(&mut w, h), ListMode::Regular => print_regular(&mut w, h, format),
} }
w.flush().expect("failed to flush history"); w.flush().expect("failed to flush history");
} }
#[allow(clippy::cast_sign_loss)] /// type wrapper around `History` so we can implement traits
pub fn print_human_list(w: &mut StdoutLock, h: &[History]) { struct FmtHistory<'a>(&'a History);
for h in h.iter().rev() {
let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64));
let time = h.timestamp.format("%Y-%m-%d %H:%M:%S"); /// defines how to format the history
let cmd = h.command.trim(); impl FormatKey for FmtHistory<'_> {
#[allow(clippy::cast_sign_loss)]
writeln!(w, "{time} · {duration}\t{cmd}").expect("failed to write history"); fn fmt(&self, key: &str, f: &mut fmt::Formatter<'_>) -> Result<(), FormatKeyError> {
match key {
"command" => f.write_str(self.0.command.trim())?,
"directory" => f.write_str(self.0.cwd.trim())?,
"duration" => {
let dur = Duration::from_nanos(std::cmp::max(self.0.duration, 0) as u64);
format_duration_into(dur, f)?;
}
"time" => self.0.timestamp.format("%Y-%m-%d %H:%M:%S").fmt(f)?,
"host" => f.write_str(
self.0
.hostname
.split_once(':')
.map_or(&self.0.hostname, |(host, _)| host),
)?,
"user" => f.write_str(self.0.hostname.split_once(':').map_or("", |(_, user)| user))?,
_ => return Err(FormatKeyError::UnknownKey),
}
Ok(())
} }
} }
#[allow(clippy::cast_sign_loss)] fn print_list_with(w: &mut StdoutLock, h: &[History], format: &str) {
pub fn print_regular(w: &mut StdoutLock, h: &[History]) { let fmt = match ParsedFmt::new(format) {
Ok(fmt) => fmt,
Err(err) => {
eprintln!("ERROR: History formatting failed with the following error: {err}");
println!("If your formatting string contains curly braces (eg: {{var}}) you need to escape them this way: {{{{var}}.");
std::process::exit(1)
}
};
for h in h.iter().rev() { for h in h.iter().rev() {
let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64)); writeln!(w, "{}", fmt.with_args(&FmtHistory(h))).expect("failed to write history");
let time = h.timestamp.format("%Y-%m-%d %H:%M:%S");
let cmd = h.command.trim();
writeln!(w, "{time}\t{cmd}\t{duration}").expect("failed to write history");
} }
} }
pub fn print_human_list(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
let format = format
.unwrap_or("{time} · {duration}\t{command}")
.replace("\\t", "\t");
print_list_with(w, h, &format);
}
pub fn print_regular(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
let format = format
.unwrap_or("{time}\t{command}\t{duration}")
.replace("\\t", "\t");
print_list_with(w, h, &format);
}
pub fn print_cmd_only(w: &mut StdoutLock, h: &[History]) { pub fn print_cmd_only(w: &mut StdoutLock, h: &[History]) {
for h in h.iter().rev() { for h in h.iter().rev() {
writeln!(w, "{}", h.command.trim()).expect("failed to write history"); writeln!(w, "{}", h.command.trim()).expect("failed to write history");
@ -187,6 +232,7 @@ impl Cmd {
cwd, cwd,
human, human,
cmd_only, cmd_only,
format,
} => { } => {
let session = if *session { let session = if *session {
Some(env::var("ATUIN_SESSION")?) Some(env::var("ATUIN_SESSION")?)
@ -218,14 +264,26 @@ impl Cmd {
} }
}; };
print_list(&history, ListMode::from_flags(*human, *cmd_only)); print_list(
&history,
ListMode::from_flags(*human, *cmd_only),
format.as_deref(),
);
Ok(()) Ok(())
} }
Self::Last { human, cmd_only } => { Self::Last {
human,
cmd_only,
format,
} => {
let last = db.last().await?; let last = db.last().await?;
print_list(&[last], ListMode::from_flags(*human, *cmd_only)); print_list(
&[last],
ListMode::from_flags(*human, *cmd_only),
format.as_deref(),
);
Ok(()) Ok(())
} }

View file

@ -16,7 +16,7 @@ mod duration;
mod event; mod event;
mod history_list; mod history_list;
mod interactive; mod interactive;
pub use duration::format_duration; pub use duration::{format_duration, format_duration_into};
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[derive(Parser)] #[derive(Parser)]
@ -74,6 +74,11 @@ pub struct Cmd {
/// Show only the text of the command /// Show only the text of the command
#[arg(long)] #[arg(long)]
cmd_only: bool, cmd_only: bool,
/// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
#[arg(long, short)]
format: Option<String>,
} }
impl Cmd { impl Cmd {
@ -97,6 +102,7 @@ impl Cmd {
self.exit, self.exit,
self.exclude_exit, self.exclude_exit,
self.exclude_cwd, self.exclude_cwd,
self.format,
self.before, self.before,
self.after, self.after,
self.limit, self.limit,
@ -122,6 +128,7 @@ async fn run_non_interactive(
exit: Option<i64>, exit: Option<i64>,
exclude_exit: Option<i64>, exclude_exit: Option<i64>,
exclude_cwd: Option<String>, exclude_cwd: Option<String>,
format: Option<String>,
before: Option<String>, before: Option<String>,
after: Option<String>, after: Option<String>,
limit: Option<i64>, limit: Option<i64>,
@ -202,6 +209,6 @@ async fn run_non_interactive(
.map(std::borrow::ToOwned::to_owned) .map(std::borrow::ToOwned::to_owned)
.collect(); .collect();
super::history::print_list(&results, list_mode); super::history::print_list(&results, list_mode, format.as_deref());
Ok(results.len()) Ok(results.len())
} }

View file

@ -1,10 +1,11 @@
use core::fmt;
use std::{ops::ControlFlow, time::Duration}; use std::{ops::ControlFlow, time::Duration};
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub fn format_duration(f: Duration) -> String { pub fn format_duration_into(dur: Duration, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn item(name: &str, value: u64) -> ControlFlow<String> { fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> {
if value > 0 { if value > 0 {
ControlFlow::Break(format!("{value}{name}")) ControlFlow::Break((unit, value))
} else { } else {
ControlFlow::Continue(()) ControlFlow::Continue(())
} }
@ -13,7 +14,7 @@ pub fn format_duration(f: Duration) -> String {
// impl taken and modified from // impl taken and modified from
// https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331 // https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331
// Copyright (c) 2016 The humantime Developers // Copyright (c) 2016 The humantime Developers
fn fmt(f: Duration) -> ControlFlow<String, ()> { fn fmt(f: Duration) -> ControlFlow<(&'static str, u64), ()> {
let secs = f.as_secs(); let secs = f.as_secs();
let nanos = f.subsec_nanos(); let nanos = f.subsec_nanos();
@ -43,8 +44,19 @@ pub fn format_duration(f: Duration) -> String {
ControlFlow::Continue(()) ControlFlow::Continue(())
} }
match fmt(f) { match fmt(dur) {
ControlFlow::Break(b) => b, ControlFlow::Break((unit, value)) => write!(f, "{value}{unit}"),
ControlFlow::Continue(()) => String::from("0s"), ControlFlow::Continue(()) => write!(f, "0s"),
} }
} }
#[allow(clippy::module_name_repetitions)]
pub fn format_duration(f: Duration) -> String {
struct F(Duration);
impl fmt::Display for F {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_duration_into(self.0, f)
}
}
F(f).to_string()
}