aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/atuin-client/config.toml5
-rw-r--r--crates/atuin-client/src/settings.rs2
-rw-r--r--crates/atuin/src/command/client/history.rs90
3 files changed, 97 insertions, 0 deletions
diff --git a/crates/atuin-client/config.toml b/crates/atuin-client/config.toml
index 6e67a4e1..0d0672bf 100644
--- a/crates/atuin-client/config.toml
+++ b/crates/atuin-client/config.toml
@@ -113,6 +113,11 @@
## default history list format - can also be specified with the --format arg
# history_format = "{time}\t{command}\t{duration}"
+## Defaults to true. If enabled, strip trailing spaces and tabs from commands
+## before saving them to history.
+## Escaped trailing spaces (for example `printf foo\\ `) are preserved.
+# strip_trailing_whitespace = true
+
## prevent commands matching any of these regexes from being written to history.
## Note that these regular expressions are unanchored, i.e. if they don't start
## with ^ or end with $, they'll match anywhere in the command.
diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs
index 22b892d1..e2624136 100644
--- a/crates/atuin-client/src/settings.rs
+++ b/crates/atuin-client/src/settings.rs
@@ -1092,6 +1092,7 @@ pub struct Settings {
pub word_chars: String,
pub scroll_context_lines: usize,
pub history_format: String,
+ pub strip_trailing_whitespace: bool,
pub prefers_reduced_motion: bool,
pub store_failed: bool,
pub no_mouse: bool,
@@ -1497,6 +1498,7 @@ impl Settings {
.set_default("workspaces", false)?
.set_default("ctrl_n_shortcuts", false)?
.set_default("secrets_filter", true)?
+ .set_default("strip_trailing_whitespace", true)?
.set_default("network_connect_timeout", 5)?
.set_default("network_timeout", 30)?
.set_default("local_timeout", 2.0)?
diff --git a/crates/atuin/src/command/client/history.rs b/crates/atuin/src/command/client/history.rs
index fe9a7e32..4d81d144 100644
--- a/crates/atuin/src/command/client/history.rs
+++ b/crates/atuin/src/command/client/history.rs
@@ -365,6 +365,30 @@ fn parse_fmt(format: &str) -> ParsedFmt<'_> {
}
impl Cmd {
+ fn normalize_command_for_storage<'a>(command: &'a str, settings: &Settings) -> &'a str {
+ if !settings.strip_trailing_whitespace {
+ return command;
+ }
+
+ let trimmed = command.trim_end_matches([' ', '\t']);
+ if trimmed.len() == command.len() {
+ return command;
+ }
+
+ let trailing_backslashes = trimmed
+ .as_bytes()
+ .iter()
+ .rev()
+ .take_while(|&&byte| byte == b'\\')
+ .count();
+
+ if trailing_backslashes % 2 == 1 {
+ command
+ } else {
+ trimmed
+ }
+ }
+
fn apply_start_metadata(history: &mut History, author: Option<&str>, intent: Option<&str>) {
if let Some(author) = author.map(str::trim).filter(|author| !author.is_empty()) {
author.clone_into(&mut history.author);
@@ -388,6 +412,7 @@ impl Cmd {
// It's better for atuin to silently fail here and attempt to
// store whatever is ran, than to throw an error to the terminal
let cwd = utils::get_current_dir();
+ let command = Self::normalize_command_for_storage(command, settings);
let mut h: History = History::capture()
.timestamp(OffsetDateTime::now_utc())
@@ -424,6 +449,7 @@ impl Cmd {
// It's better for atuin to silently fail here and attempt to
// store whatever is ran, than to throw an error to the terminal
let cwd = utils::get_current_dir();
+ let command = Self::normalize_command_for_storage(command, settings);
let mut h: History = History::capture()
.timestamp(OffsetDateTime::now_utc())
@@ -831,6 +857,70 @@ mod tests {
use super::*;
#[test]
+ fn normalize_command_strips_trailing_spaces_and_tabs() {
+ let settings = Settings::utc();
+
+ assert!(settings.strip_trailing_whitespace);
+ assert_eq!(
+ Cmd::normalize_command_for_storage("ls \t", &settings),
+ "ls"
+ );
+ }
+
+ #[test]
+ fn normalize_command_preserves_escaped_trailing_space() {
+ let settings = Settings::utc();
+
+ assert_eq!(
+ Cmd::normalize_command_for_storage("printf foo\\ ", &settings),
+ "printf foo\\ "
+ );
+ assert_eq!(
+ Cmd::normalize_command_for_storage("printf foo\\\\ ", &settings),
+ "printf foo\\\\"
+ );
+ }
+
+ #[tokio::test]
+ async fn handle_start_saves_trimmed_command() {
+ let db = Sqlite::new("sqlite::memory:", 2.0).await.unwrap();
+ let settings = Settings::utc();
+
+ Cmd::handle_start(&db, &settings, "ls \t", None, None)
+ .await
+ .unwrap();
+
+ let history = db
+ .before(OffsetDateTime::now_utc() + time::Duration::SECOND, 1)
+ .await
+ .unwrap()
+ .pop()
+ .unwrap();
+ assert_eq!(history.command, "ls");
+ }
+
+ #[tokio::test]
+ async fn handle_start_can_keep_trailing_whitespace() {
+ let db = Sqlite::new("sqlite::memory:", 2.0).await.unwrap();
+ let settings = Settings {
+ strip_trailing_whitespace: false,
+ ..Settings::utc()
+ };
+
+ Cmd::handle_start(&db, &settings, "ls \t", None, None)
+ .await
+ .unwrap();
+
+ let history = db
+ .before(OffsetDateTime::now_utc() + time::Duration::SECOND, 1)
+ .await
+ .unwrap()
+ .pop()
+ .unwrap();
+ assert_eq!(history.command, "ls \t");
+ }
+
+ #[test]
fn test_format_string_no_panic() {
// Don't panic but provide helpful output (issue #2776)
let malformed_json = r#"{"command":"{command}","key":"value"}"#;