diff options
| author | Ellie Huxtable <ellie@atuin.sh> | 2026-04-10 02:13:55 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-10 02:13:55 +0100 |
| commit | 75190f58827026b5f5902b24815a8950e7333bac (patch) | |
| tree | a919763f3974e6ffa617d3f9c8fc91504df099e9 | |
| parent | docs: Minor readability improvement to README (#3381) (diff) | |
| download | atuin-75190f58827026b5f5902b24815a8950e7333bac.zip | |
feat: add strip_trailing_whitespace, on by default (#3390)
I can't think of any reason you would want this disabled by default -
trailing whitespace means nothing, breaks dedupe, and wastes a few bytes
closes #3387
## Checks
- [ ] I am happy for maintainers to push small adjustments to this PR,
to speed up the review cycle
- [ ] I have checked that there are no existing pull requests for the
same thing
| -rw-r--r-- | crates/atuin-client/config.toml | 5 | ||||
| -rw-r--r-- | crates/atuin-client/src/settings.rs | 2 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/history.rs | 90 |
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"}"#; |
