Skip to content

Commit a7a9727

Browse files
authored
Merge pull request #2999 from eth-p/strip-ansi-from-input-option
Add option to remove ANSI escape sequences from bat's input.
2 parents c264ecd + 90dfa7f commit a7a9727

File tree

11 files changed

+273
-15
lines changed

11 files changed

+273
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
88
- `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
99
- Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar)
10+
- `bat --strip-ansi={never,always,auto}` to remove ANSI escape sequences from bat's input, see #2999 (@eth-p)
1011

1112
## Bugfixes
1213

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,9 +759,14 @@ bat() {
759759

760760
If an input file contains color codes or other ANSI escape sequences or control characters, `bat` will have problems
761761
performing syntax highlighting and text wrapping, and thus the output can become garbled.
762-
When displaying such files it is recommended to disable both syntax highlighting and wrapping by
762+
763+
If your version of `bat` supports the `--strip-ansi=auto` option, it can be used to remove such sequences
764+
before syntax highlighting. Alternatively, you may disable both syntax highlighting and wrapping by
763765
passing the `--color=never --wrap=never` options to `bat`.
764766

767+
> [!NOTE]
768+
> The `auto` option of `--strip-ansi` avoids removing escape sequences when the syntax is plain text.
769+
765770
### Terminals & colors
766771

767772
`bat` handles terminals *with* and *without* truecolor support. However, the colors in most syntax

doc/long-help.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ Options:
122122
--squeeze-limit <squeeze-limit>
123123
Set the maximum number of consecutive empty lines to be printed.
124124

125+
--strip-ansi <when>
126+
Specify when to strip ANSI escape sequences from the input. The automatic mode will remove
127+
escape sequences unless the syntax highlighting language is plain text. Possible values:
128+
auto, always, *never*.
129+
125130
--style <components>
126131
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
127132
to display in addition to the file contents. The argument is a comma-separated list of

src/bin/bat/app.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
clap_app,
88
config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars},
99
};
10+
use bat::StripAnsiMode;
1011
use clap::ArgMatches;
1112

1213
use console::Term;
@@ -242,6 +243,16 @@ impl App {
242243
4
243244
},
244245
),
246+
strip_ansi: match self
247+
.matches
248+
.get_one::<String>("strip-ansi")
249+
.map(|s| s.as_str())
250+
{
251+
Some("never") => StripAnsiMode::Never,
252+
Some("always") => StripAnsiMode::Always,
253+
Some("auto") => StripAnsiMode::Auto,
254+
_ => unreachable!("other values for --strip-ansi are not allowed"),
255+
},
245256
theme: self
246257
.matches
247258
.get_one::<String>("theme")

src/bin/bat/clap_app.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,20 @@ pub fn build_app(interactive_output: bool) -> Command {
402402
.long_help("Set the maximum number of consecutive empty lines to be printed.")
403403
.hide_short_help(true)
404404
)
405+
.arg(
406+
Arg::new("strip-ansi")
407+
.long("strip-ansi")
408+
.overrides_with("strip-ansi")
409+
.value_name("when")
410+
.value_parser(["auto", "always", "never"])
411+
.default_value("never")
412+
.hide_default_value(true)
413+
.help("Strip colors from the input (auto, always, *never*)")
414+
.long_help("Specify when to strip ANSI escape sequences from the input. \
415+
The automatic mode will remove escape sequences unless the syntax highlighting \
416+
language is plain text. Possible values: auto, always, *never*.")
417+
.hide_short_help(true)
418+
)
405419
.arg(
406420
Arg::new("style")
407421
.long("style")

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::paging::PagingMode;
55
use crate::style::StyleComponents;
66
use crate::syntax_mapping::SyntaxMapping;
77
use crate::wrapping::WrappingMode;
8+
use crate::StripAnsiMode;
89

910
#[derive(Debug, Clone)]
1011
pub enum VisibleLines {
@@ -100,6 +101,9 @@ pub struct Config<'a> {
100101

101102
/// The maximum number of consecutive empty lines to display
102103
pub squeeze_lines: Option<usize>,
104+
105+
// Weather or not to set terminal title when using a pager
106+
pub strip_ansi: StripAnsiMode,
103107
}
104108

105109
#[cfg(all(feature = "minimal-application", feature = "paging"))]

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mod vscreen;
5353
pub(crate) mod wrapping;
5454

5555
pub use nonprintable_notation::NonprintableNotation;
56+
pub use preprocessor::StripAnsiMode;
5657
pub use pretty_printer::{Input, PrettyPrinter, Syntax};
5758
pub use syntax_mapping::{MappingTarget, SyntaxMapping};
5859
pub use wrapping::WrappingMode;

src/preprocessor.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,27 @@ pub fn replace_nonprintable(
136136
output
137137
}
138138

139+
/// Strips ANSI escape sequences from the input.
140+
pub fn strip_ansi(line: &str) -> String {
141+
let mut buffer = String::with_capacity(line.len());
142+
143+
for seq in EscapeSequenceOffsetsIterator::new(line) {
144+
if let EscapeSequenceOffsets::Text { .. } = seq {
145+
buffer.push_str(&line[seq.index_of_start()..seq.index_past_end()]);
146+
}
147+
}
148+
149+
buffer
150+
}
151+
152+
#[derive(Debug, PartialEq, Clone, Copy, Default)]
153+
pub enum StripAnsiMode {
154+
#[default]
155+
Never,
156+
Always,
157+
Auto,
158+
}
159+
139160
#[test]
140161
fn test_try_parse_utf8_char() {
141162
assert_eq!(try_parse_utf8_char(&[0x20]), Some((' ', 1)));
@@ -179,3 +200,14 @@ fn test_try_parse_utf8_char() {
179200
assert_eq!(try_parse_utf8_char(&[0xef, 0x20]), None);
180201
assert_eq!(try_parse_utf8_char(&[0xf0, 0xf0]), None);
181202
}
203+
204+
#[test]
205+
fn test_strip_ansi() {
206+
// The sequence detection is covered by the tests in the vscreen module.
207+
assert_eq!(strip_ansi("no ansi"), "no ansi");
208+
assert_eq!(strip_ansi("\x1B[33mone"), "one");
209+
assert_eq!(
210+
strip_ansi("\x1B]1\x07multiple\x1B[J sequences"),
211+
"multiple sequences"
212+
);
213+
}

src/pretty_printer.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
input,
1212
line_range::{HighlightedLineRanges, LineRange, LineRanges},
1313
style::StyleComponent,
14-
SyntaxMapping, WrappingMode,
14+
StripAnsiMode, SyntaxMapping, WrappingMode,
1515
};
1616

1717
#[cfg(feature = "paging")]
@@ -182,6 +182,15 @@ impl<'a> PrettyPrinter<'a> {
182182
self
183183
}
184184

185+
/// Whether to remove ANSI escape sequences from the input (default: never)
186+
///
187+
/// If `Auto` is used, escape sequences will only be removed when the input
188+
/// is not plain text.
189+
pub fn strip_ansi(&mut self, mode: StripAnsiMode) -> &mut Self {
190+
self.config.strip_ansi = mode;
191+
self
192+
}
193+
185194
/// Text wrapping mode (default: do not wrap)
186195
pub fn wrapping_mode(&mut self, mode: WrappingMode) -> &mut Self {
187196
self.config.wrapping_mode = mode;

src/printer.rs

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ use crate::diff::LineChanges;
2929
use crate::error::*;
3030
use crate::input::OpenedInput;
3131
use crate::line_range::RangeCheckResult;
32+
use crate::preprocessor::strip_ansi;
3233
use crate::preprocessor::{expand_tabs, replace_nonprintable};
3334
use crate::style::StyleComponent;
3435
use crate::terminal::{as_terminal_escaped, to_ansi_color};
3536
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
3637
use crate::wrapping::WrappingMode;
38+
use crate::StripAnsiMode;
3739

3840
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
3941
raw_sequence: "\x1B[4m",
@@ -207,6 +209,7 @@ pub(crate) struct InteractivePrinter<'a> {
207209
highlighter_from_set: Option<HighlighterFromSet<'a>>,
208210
background_color_highlight: Option<Color>,
209211
consecutive_empty_lines: usize,
212+
strip_ansi: bool,
210213
}
211214

212215
impl<'a> InteractivePrinter<'a> {
@@ -265,20 +268,41 @@ impl<'a> InteractivePrinter<'a> {
265268
.content_type
266269
.map_or(false, |c| c.is_binary() && !config.show_nonprintable);
267270

268-
let highlighter_from_set = if is_printing_binary || !config.colored_output {
269-
None
270-
} else {
271+
let needs_to_match_syntax = !is_printing_binary
272+
&& (config.colored_output || config.strip_ansi == StripAnsiMode::Auto);
273+
274+
let (is_plain_text, highlighter_from_set) = if needs_to_match_syntax {
271275
// Determine the type of syntax for highlighting
272-
let syntax_in_set =
273-
match assets.get_syntax(config.language, input, &config.syntax_mapping) {
274-
Ok(syntax_in_set) => syntax_in_set,
275-
Err(Error::UndetectedSyntax(_)) => assets
276-
.find_syntax_by_name("Plain Text")?
277-
.expect("A plain text syntax is available"),
278-
Err(e) => return Err(e),
279-
};
276+
const PLAIN_TEXT_SYNTAX: &str = "Plain Text";
277+
match assets.get_syntax(config.language, input, &config.syntax_mapping) {
278+
Ok(syntax_in_set) => (
279+
syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX,
280+
Some(HighlighterFromSet::new(syntax_in_set, theme)),
281+
),
282+
283+
Err(Error::UndetectedSyntax(_)) => (
284+
true,
285+
Some(
286+
assets
287+
.find_syntax_by_name(PLAIN_TEXT_SYNTAX)?
288+
.map(|s| HighlighterFromSet::new(s, theme))
289+
.expect("A plain text syntax is available"),
290+
),
291+
),
292+
293+
Err(e) => return Err(e),
294+
}
295+
} else {
296+
(false, None)
297+
};
280298

281-
Some(HighlighterFromSet::new(syntax_in_set, theme))
299+
// Determine when to strip ANSI sequences
300+
let strip_ansi = match config.strip_ansi {
301+
_ if config.show_nonprintable => false,
302+
StripAnsiMode::Always => true,
303+
StripAnsiMode::Auto if is_plain_text => false, // Plain text may already contain escape sequences.
304+
StripAnsiMode::Auto => true,
305+
_ => false,
282306
};
283307

284308
Ok(InteractivePrinter {
@@ -293,6 +317,7 @@ impl<'a> InteractivePrinter<'a> {
293317
highlighter_from_set,
294318
background_color_highlight,
295319
consecutive_empty_lines: 0,
320+
strip_ansi,
296321
})
297322
}
298323

@@ -573,7 +598,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
573598
)
574599
.into()
575600
} else {
576-
match self.content_type {
601+
let mut line = match self.content_type {
577602
Some(ContentType::BINARY) | None => {
578603
return Ok(());
579604
}
@@ -590,7 +615,14 @@ impl<'a> Printer for InteractivePrinter<'a> {
590615
line
591616
}
592617
}
618+
};
619+
620+
// If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting.
621+
if self.strip_ansi {
622+
line = strip_ansi(&line).into()
593623
}
624+
625+
line
594626
};
595627

596628
let regions = self.highlight_regions_for_line(&line)?;

0 commit comments

Comments
 (0)