Add --strip-ansi option

This commit is contained in:
Ethan P. 2024-06-10 21:05:20 -07:00
parent c264ecd26b
commit 70ff93d238
No known key found for this signature in database
GPG Key ID: B29B90B1B228FEBC
8 changed files with 155 additions and 1 deletions

View File

@ -122,6 +122,10 @@ Options:
--squeeze-limit <squeeze-limit> --squeeze-limit <squeeze-limit>
Set the maximum number of consecutive empty lines to be printed. Set the maximum number of consecutive empty lines to be printed.
--strip-ansi <when>
Specify when to strip ANSI escape sequences from the input. Possible values: always,
*never*.
--style <components> --style <components>
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
to display in addition to the file contents. The argument is a comma-separated list of to display in addition to the file contents. The argument is a comma-separated list of

View File

@ -7,6 +7,7 @@ use crate::{
clap_app, clap_app,
config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars}, config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars},
}; };
use bat::StripAnsiMode;
use clap::ArgMatches; use clap::ArgMatches;
use console::Term; use console::Term;
@ -242,6 +243,15 @@ impl App {
4 4
}, },
), ),
strip_ansi: match self
.matches
.get_one::<String>("strip-ansi")
.map(|s| s.as_str())
{
Some("never") => StripAnsiMode::Never,
Some("always") => StripAnsiMode::Always,
_ => unreachable!("other values for --strip-ansi are not allowed"),
},
theme: self theme: self
.matches .matches
.get_one::<String>("theme") .get_one::<String>("theme")

View File

@ -402,6 +402,18 @@ pub fn build_app(interactive_output: bool) -> Command {
.long_help("Set the maximum number of consecutive empty lines to be printed.") .long_help("Set the maximum number of consecutive empty lines to be printed.")
.hide_short_help(true) .hide_short_help(true)
) )
.arg(
Arg::new("strip-ansi")
.long("strip-ansi")
.overrides_with("strip-ansi")
.value_name("when")
.value_parser(["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*.")
.hide_short_help(true)
)
.arg( .arg(
Arg::new("style") Arg::new("style")
.long("style") .long("style")

View File

@ -5,6 +5,7 @@ use crate::paging::PagingMode;
use crate::style::StyleComponents; use crate::style::StyleComponents;
use crate::syntax_mapping::SyntaxMapping; use crate::syntax_mapping::SyntaxMapping;
use crate::wrapping::WrappingMode; use crate::wrapping::WrappingMode;
use crate::StripAnsiMode;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum VisibleLines { pub enum VisibleLines {
@ -100,6 +101,9 @@ pub struct Config<'a> {
/// The maximum number of consecutive empty lines to display /// The maximum number of consecutive empty lines to display
pub squeeze_lines: Option<usize>, pub squeeze_lines: Option<usize>,
// Weather or not to set terminal title when using a pager
pub strip_ansi: StripAnsiMode,
} }
#[cfg(all(feature = "minimal-application", feature = "paging"))] #[cfg(all(feature = "minimal-application", feature = "paging"))]

View File

@ -53,6 +53,7 @@ mod vscreen;
pub(crate) mod wrapping; pub(crate) mod wrapping;
pub use nonprintable_notation::NonprintableNotation; pub use nonprintable_notation::NonprintableNotation;
pub use preprocessor::StripAnsiMode;
pub use pretty_printer::{Input, PrettyPrinter, Syntax}; pub use pretty_printer::{Input, PrettyPrinter, Syntax};
pub use syntax_mapping::{MappingTarget, SyntaxMapping}; pub use syntax_mapping::{MappingTarget, SyntaxMapping};
pub use wrapping::WrappingMode; pub use wrapping::WrappingMode;

View File

@ -136,6 +136,26 @@ pub fn replace_nonprintable(
output output
} }
/// Strips ANSI escape sequences from the input.
pub fn strip_ansi(line: &str) -> String {
let mut buffer = String::with_capacity(line.len());
for seq in EscapeSequenceOffsetsIterator::new(line) {
if let EscapeSequenceOffsets::Text { .. } = seq {
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]);
}
}
buffer
}
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub enum StripAnsiMode {
#[default]
Never,
Always,
}
#[test] #[test]
fn test_try_parse_utf8_char() { fn test_try_parse_utf8_char() {
assert_eq!(try_parse_utf8_char(&[0x20]), Some((' ', 1))); assert_eq!(try_parse_utf8_char(&[0x20]), Some((' ', 1)));
@ -179,3 +199,14 @@ fn test_try_parse_utf8_char() {
assert_eq!(try_parse_utf8_char(&[0xef, 0x20]), None); assert_eq!(try_parse_utf8_char(&[0xef, 0x20]), None);
assert_eq!(try_parse_utf8_char(&[0xf0, 0xf0]), None); assert_eq!(try_parse_utf8_char(&[0xf0, 0xf0]), None);
} }
#[test]
fn test_strip_ansi() {
// The sequence detection is covered by the tests in the vscreen module.
assert_eq!(strip_ansi("no ansi"), "no ansi");
assert_eq!(strip_ansi("\x1B[33mone"), "one");
assert_eq!(
strip_ansi("\x1B]1\x07multiple\x1B[J sequences"),
"multiple sequences"
);
}

View File

@ -29,11 +29,13 @@ use crate::diff::LineChanges;
use crate::error::*; use crate::error::*;
use crate::input::OpenedInput; use crate::input::OpenedInput;
use crate::line_range::RangeCheckResult; use crate::line_range::RangeCheckResult;
use crate::preprocessor::strip_ansi;
use crate::preprocessor::{expand_tabs, replace_nonprintable}; use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::style::StyleComponent; use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
use crate::wrapping::WrappingMode; use crate::wrapping::WrappingMode;
use crate::StripAnsiMode;
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI { const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
raw_sequence: "\x1B[4m", raw_sequence: "\x1B[4m",
@ -207,6 +209,7 @@ pub(crate) struct InteractivePrinter<'a> {
highlighter_from_set: Option<HighlighterFromSet<'a>>, highlighter_from_set: Option<HighlighterFromSet<'a>>,
background_color_highlight: Option<Color>, background_color_highlight: Option<Color>,
consecutive_empty_lines: usize, consecutive_empty_lines: usize,
strip_ansi: bool,
} }
impl<'a> InteractivePrinter<'a> { impl<'a> InteractivePrinter<'a> {
@ -281,6 +284,13 @@ impl<'a> InteractivePrinter<'a> {
Some(HighlighterFromSet::new(syntax_in_set, theme)) Some(HighlighterFromSet::new(syntax_in_set, theme))
}; };
// Determine when to strip ANSI sequences
let strip_ansi = match config.strip_ansi {
_ if config.show_nonprintable => false,
StripAnsiMode::Always => true,
_ => false,
};
Ok(InteractivePrinter { Ok(InteractivePrinter {
panel_width, panel_width,
colors, colors,
@ -293,6 +303,7 @@ impl<'a> InteractivePrinter<'a> {
highlighter_from_set, highlighter_from_set,
background_color_highlight, background_color_highlight,
consecutive_empty_lines: 0, consecutive_empty_lines: 0,
strip_ansi,
}) })
} }
@ -573,7 +584,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
) )
.into() .into()
} else { } else {
match self.content_type { let mut line = match self.content_type {
Some(ContentType::BINARY) | None => { Some(ContentType::BINARY) | None => {
return Ok(()); return Ok(());
} }
@ -590,7 +601,14 @@ impl<'a> Printer for InteractivePrinter<'a> {
line line
} }
} }
};
// If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
if self.strip_ansi {
line = strip_ansi(&line).into()
} }
line
}; };
let regions = self.highlight_regions_for_line(&line)?; let regions = self.highlight_regions_for_line(&line)?;

View File

@ -2666,3 +2666,77 @@ fn highlighting_independant_from_map_syntax_case() {
.stdout(expected) .stdout(expected)
.stderr(""); .stderr("");
} }
#[test]
fn strip_ansi_always_strips_ansi() {
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--strip-ansi=always")
.write_stdin("\x1B[33mYellow\x1B[m")
.assert()
.success()
.stdout("Yellow");
}
#[test]
fn strip_ansi_never_does_not_strip_ansi() {
let output = String::from_utf8(
bat()
.arg("--style=plain")
.arg("--decorations=always")
.arg("--color=never")
.arg("--strip-ansi=never")
.write_stdin("\x1B[33mYellow\x1B[m")
.assert()
.success()
.get_output()
.stdout
.clone(),
)
.expect("valid utf8");
assert!(output.contains("\x1B[33mYellow"))
}
#[test]
fn strip_ansi_does_not_affect_simple_printer() {
let output = String::from_utf8(
bat()
.arg("--style=plain")
.arg("--decorations=never")
.arg("--color=never")
.arg("--strip-ansi=always")
.write_stdin("\x1B[33mYellow\x1B[m")
.assert()
.success()
.get_output()
.stdout
.clone(),
)
.expect("valid utf8");
assert!(output.contains("\x1B[33mYellow"))
}
#[test]
fn strip_ansi_does_not_strip_when_show_nonprintable() {
let output = String::from_utf8(
bat()
.arg("--style=plain")
.arg("--decorations=never")
.arg("--color=always")
.arg("--strip-ansi=always")
.arg("--show-nonprintable")
.write_stdin("\x1B[33mY")
.assert()
.success()
.get_output()
.stdout
.clone(),
)
.expect("valid utf8");
assert!(output.contains(""))
}