From 9e8176b1c69235b91c14aaa56c5862ed7dff3a9b Mon Sep 17 00:00:00 2001 From: "Ethan P." Date: Mon, 10 Jun 2024 21:17:28 -0700 Subject: [PATCH] Add `--strip-ansi=auto` option When using `auto`, escape sequences will be stripped unless printing plain text. --- doc/long-help.txt | 5 +-- src/bin/bat/app.rs | 1 + src/bin/bat/clap_app.rs | 8 +++-- src/preprocessor.rs | 1 + src/printer.rs | 40 +++++++++++++++------- tests/integration_tests.rs | 70 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 107 insertions(+), 18 deletions(-) diff --git a/doc/long-help.txt b/doc/long-help.txt index 93f56968..d9cdce39 100644 --- a/doc/long-help.txt +++ b/doc/long-help.txt @@ -123,8 +123,9 @@ Options: Set the maximum number of consecutive empty lines to be printed. --strip-ansi - Specify when to strip ANSI escape sequences from the input. Possible values: always, - *never*. + Specify when to strip ANSI escape sequences from the input. The automatic mode will remove + escape sequences unless the syntax highlighting language is plain text. Possible values: + auto, always, *never*. --style Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 62ffdd6d..0d2600b2 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -250,6 +250,7 @@ impl App { { Some("never") => StripAnsiMode::Never, Some("always") => StripAnsiMode::Always, + Some("auto") => StripAnsiMode::Auto, _ => unreachable!("other values for --strip-ansi are not allowed"), }, theme: self diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 32c7c077..e70b1a5b 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -407,11 +407,13 @@ pub fn build_app(interactive_output: bool) -> Command { .long("strip-ansi") .overrides_with("strip-ansi") .value_name("when") - .value_parser(["always", "never"]) + .value_parser(["auto", "always", "never"]) .default_value("never") .hide_default_value(true) - .help("Strip colors from the input (always, *never*)") - .long_help("Specify when to strip ANSI escape sequences from the input. Possible values: always, *never*.") + .help("Strip colors from the input (auto, always, *never*)") + .long_help("Specify when to strip ANSI escape sequences from the input. \ + The automatic mode will remove escape sequences unless the syntax highlighting \ + language is plain text. Possible values: auto, always, *never*.") .hide_short_help(true) ) .arg( diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 707946f9..dc2aa66e 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -154,6 +154,7 @@ pub enum StripAnsiMode { #[default] Never, Always, + Auto, } #[test] diff --git a/src/printer.rs b/src/printer.rs index d76e6e0a..e9bea3fd 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -268,26 +268,40 @@ impl<'a> InteractivePrinter<'a> { .content_type .map_or(false, |c| c.is_binary() && !config.show_nonprintable); - let highlighter_from_set = if is_printing_binary || !config.colored_output { - None - } else { - // Determine the type of syntax for highlighting - let syntax_in_set = - match assets.get_syntax(config.language, input, &config.syntax_mapping) { - Ok(syntax_in_set) => syntax_in_set, - Err(Error::UndetectedSyntax(_)) => assets - .find_syntax_by_name("Plain Text")? - .expect("A plain text syntax is available"), - Err(e) => return Err(e), - }; + let needs_to_match_syntax = !is_printing_binary + && (config.colored_output || config.strip_ansi == StripAnsiMode::Auto); - Some(HighlighterFromSet::new(syntax_in_set, theme)) + let (is_plain_text, highlighter_from_set) = if needs_to_match_syntax { + // Determine the type of syntax for highlighting + const PLAIN_TEXT_SYNTAX: &str = "Plain Text"; + match assets.get_syntax(config.language, input, &config.syntax_mapping) { + Ok(syntax_in_set) => ( + syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX, + Some(HighlighterFromSet::new(syntax_in_set, theme)), + ), + + Err(Error::UndetectedSyntax(_)) => ( + true, + Some( + assets + .find_syntax_by_name(PLAIN_TEXT_SYNTAX)? + .map(|s| HighlighterFromSet::new(s, theme)) + .expect("A plain text syntax is available"), + ), + ), + + Err(e) => return Err(e), + } + } else { + (false, None) }; // Determine when to strip ANSI sequences let strip_ansi = match config.strip_ansi { _ if config.show_nonprintable => false, StripAnsiMode::Always => true, + StripAnsiMode::Auto if is_plain_text => false, // Plain text may already contain escape sequences. + StripAnsiMode::Auto => true, _ => false, }; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index bc86cb9b..d6009361 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2740,3 +2740,73 @@ fn strip_ansi_does_not_strip_when_show_nonprintable() { assert!(output.contains("␛")) } + +#[test] +fn strip_ansi_auto_strips_ansi_when_detected_syntax_by_filename() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--file-name=test.rs") + .write_stdin("fn \x1B[33mYellow\x1B[m() -> () {}") + .assert() + .success() + .stdout("fn Yellow() -> () {}"); +} + +#[test] +fn strip_ansi_auto_strips_ansi_when_provided_syntax_by_option() { + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--language=rust") + .write_stdin("fn \x1B[33mYellow\x1B[m() -> () {}") + .assert() + .success() + .stdout("fn Yellow() -> () {}"); +} + +#[test] +fn strip_ansi_auto_does_not_strip_when_plain_text_by_filename() { + let output = String::from_utf8( + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--file-name=ansi.txt") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .get_output() + .stdout + .clone(), + ) + .expect("valid utf8"); + + assert!(output.contains("\x1B[33mYellow")) +} + +#[test] +fn strip_ansi_auto_does_not_strip_ansi_when_plain_text_by_option() { + let output = String::from_utf8( + bat() + .arg("--style=plain") + .arg("--decorations=always") + .arg("--color=never") + .arg("--strip-ansi=auto") + .arg("--language=txt") + .write_stdin("\x1B[33mYellow\x1B[m") + .assert() + .success() + .get_output() + .stdout + .clone(), + ) + .expect("valid utf8"); + + assert!(output.contains("\x1B[33mYellow")) +}