Merge upstream into PR branch, resolve conflicts

This commit is contained in:
Mikhail Alabugin committed 2026-10-20 13:59:30 +03:00
commit b841eadf09
14 files changed
+224 -63

No files matched your search

+1 -1
View File
@@ -457,7 +457,7 @@ jobs:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
- uses: vedantmgoyal9/winget-releaser@a8fff44b123f115ce4c21543edf8b677376fc7e0
with:
identifier: sharkdp.bat
installers-regex: '-pc-windows-msvc\.zip$'
+5
View File
@@ -4,6 +4,7 @@
## Other
- Fix dead Visual C++ Redistributable link in README, see #3895 (@1t1sCooL)
- Document `StripAnsiMode` and its variants in rustdoc, see #4067 (@ump45nose)
- Update Cargo dependencies to resolve current RustSec advisories, see #3861 (@TyceHerrman)
- Add instructions for removing fish help abbreviations to README, see #3655 (@claw-explorer). Closes #3536
- Add .NET slnx extension, see #3682 (@ltrzesniewski)
@@ -26,6 +27,9 @@
- Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres)
## Bugfixes
- Fix the first line of UTF-16BE input swallowing the second line, which broke line numbers, `--line-range` and highlighting, see #XXXX (@cindykrafft)
- Exit quietly with status 0 instead of panicking when stdout is closed early (broken pipe) on auxiliary output paths such as `--completion`, `--config-file`, `--diagnostic`, `--generate-config-file`, `--set-terminal-title` and `cache --build`/`--clear`, matching the main output path since #232, closes #4034, see #4035 (@Mathjk)
- Track strikethrough (SGR 9 / 29) in the ANSI style tracker so it is re-emitted like bold, dim, italic, and underline, and is no longer dropped after the first highlighted token when displaying ANSI input with syntax highlighting, see #4015 (@mikehasa)
- Allow boolean flags to be given more than once, so that a flag set in the config file can also be passed on the command line without erroring, see #3912 (@logarithmone1128)
- Use parsed CLI arguments to detect number flags, see #3908 (@cuishuang)
- Detect binary content beyond the first line, preventing encrypted files with early line breaks from being treated as text. Closes #3554, see #3877 (@Matei02355)
@@ -85,6 +89,7 @@
- Add syntax highlighting for `Caddyfile` #3789 (@CosmicHorrorDev)
- Include `.code-workspace` as a JSON extension #3809 (@dhruvkb)
- Add syntax mapping for DNF repo configuration files, see #3814 (@injust)
- Add syntax mapping for Podman `.quadlets` files, see #4069 (@injust)
## Themes
+25 -15
View File
@@ -1,4 +1,5 @@
use std::convert::TryInto;
use std::io::{self, Write};
use std::path::Path;
use syntect::highlighting::ThemeSet;
@@ -24,7 +25,7 @@ pub fn build(
let acknowledgements = build_acknowledgements(source_dir, include_acknowledgements)?;
print_unlinked_contexts(&syntax_set);
print_unlinked_contexts(&syntax_set)?;
write_assets(
&theme_set,
@@ -46,16 +47,18 @@ fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> Result
if theme_dir.exists() {
let res = theme_set.add_from_folder(&theme_dir);
if let Err(err) = res {
println!(
writeln!(
io::stdout(),
"Failed to load one or more themes from '{}' (reason: '{err}')",
theme_dir.to_string_lossy(),
);
)?;
}
} else {
println!(
writeln!(
io::stdout(),
"No themes were found in '{}', using the default set",
theme_dir.to_string_lossy()
);
)?;
}
theme_set.try_into()
@@ -78,23 +81,25 @@ fn build_syntax_set_builder(
if syntax_dir.exists() {
syntax_set_builder.add_from_folder(syntax_dir, true)?;
} else {
println!(
writeln!(
io::stdout(),
"No syntaxes were found in '{}', using the default set.",
syntax_dir.to_string_lossy()
);
)?;
}
Ok(syntax_set_builder)
}
fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
fn print_unlinked_contexts(syntax_set: &SyntaxSet) -> Result<()> {
let missing_contexts = syntax_set.find_unlinked_contexts();
if !missing_contexts.is_empty() {
println!("Some referenced contexts could not be found!");
writeln!(io::stdout(), "Some referenced contexts could not be found!")?;
for context in missing_contexts {
println!("- {context}");
writeln!(io::stdout(), "- {context}")?;
}
}
Ok(())
}
fn write_assets(
@@ -127,12 +132,13 @@ fn write_assets(
)?;
}
print!(
write!(
io::stdout(),
"Writing metadata to folder {} ... ",
target_dir.to_string_lossy()
);
)?;
crate::assets_metadata::AssetsMetadata::new(current_version).save_to_folder(target_dir)?;
println!("okay");
writeln!(io::stdout(), "okay")?;
Ok(())
}
@@ -161,10 +167,14 @@ fn asset_to_cache<T: serde::Serialize>(
description: &str,
compressed: bool,
) -> Result<()> {
print!("Writing {description} to {} ... ", path.to_string_lossy());
write!(
io::stdout(),
"Writing {description} to {} ... ",
path.to_string_lossy()
)?;
let contents = asset_to_contents(asset, description, compressed)?;
std::fs::write(path, &contents[..])
.map_err(|_| format!("Could not save {description} to {}", path.to_string_lossy()))?;
println!("okay");
writeln!(io::stdout(), "okay")?;
Ok(())
}
+15 -10
View File
@@ -1,5 +1,5 @@
use std::fs;
use std::io;
use std::io::{self, Write};
use std::path::Path;
use std::path::PathBuf;
@@ -9,10 +9,11 @@ use bat::assets::HighlightingAssets;
use bat::assets_metadata::AssetsMetadata;
use bat::error::*;
pub fn clear_assets(cache_dir: &Path) {
clear_asset(cache_dir.join("themes.bin"), "theme set cache");
clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache");
clear_asset(cache_dir.join("metadata.yaml"), "metadata file");
pub fn clear_assets(cache_dir: &Path) -> Result<()> {
clear_asset(cache_dir.join("themes.bin"), "theme set cache")?;
clear_asset(cache_dir.join("syntaxes.bin"), "syntax set cache")?;
clear_asset(cache_dir.join("metadata.yaml"), "metadata file")?;
Ok(())
}
pub fn assets_from_cache_or_binary(
@@ -43,15 +44,19 @@ pub fn assets_from_cache_or_binary(
Ok(custom_assets.unwrap_or_else(HighlightingAssets::from_binary))
}
fn clear_asset(path: PathBuf, description: &str) {
print!("Clearing {description} ... ");
fn clear_asset(path: PathBuf, description: &str) -> Result<()> {
write!(io::stdout(), "Clearing {description} ... ")?;
match fs::remove_file(&path) {
Err(err) if err.kind() == io::ErrorKind::NotFound => {
println!("skipped (not present)");
writeln!(io::stdout(), "skipped (not present)")?;
}
Err(err) => {
println!("could not remove the cache file {path:?}: {err}");
writeln!(
io::stdout(),
"could not remove the cache file {path:?}: {err}"
)?;
}
Ok(_) => println!("okay"),
Ok(_) => writeln!(io::stdout(), "okay")?,
}
Ok(())
}
+7 -5
View File
@@ -32,12 +32,13 @@ pub fn config_file() -> PathBuf {
pub fn generate_config_file() -> bat::error::Result<()> {
let config_file = config_file();
if config_file.is_file() {
println!(
writeln!(
io::stdout(),
"A config file already exists at: {}",
config_file.to_string_lossy()
);
)?;
print!("Overwrite? (y/N): ");
write!(io::stdout(), "Overwrite? (y/N): ")?;
io::stdout().flush()?;
let mut decision = String::new();
io::stdin().read_line(&mut decision)?;
@@ -93,10 +94,11 @@ pub fn generate_config_file() -> bat::error::Result<()> {
)
})?;
println!(
writeln!(
io::stdout(),
"Success! Config file written to {}",
config_file.to_string_lossy()
);
)?;
Ok(())
}
+23 -17
View File
@@ -74,9 +74,9 @@ fn run_cache_subcommand(
#[cfg(feature = "build-assets")]
build_assets(matches, config_dir, cache_dir)?;
#[cfg(not(feature = "build-assets"))]
println!("bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.");
writeln!(io::stdout(), "bat has been built without the 'build-assets' feature. The 'cache --build' option is not available.")?;
} else if matches.get_flag("clear") {
clear_assets(cache_dir);
clear_assets(cache_dir)?;
}
Ok(())
@@ -262,13 +262,17 @@ pub fn list_themes(
Ok(())
}
fn set_terminal_title_to(new_terminal_title: String) {
fn set_terminal_title_to(new_terminal_title: String) -> Result<()> {
let osc_command_for_setting_terminal_title = "\x1b]0;";
let osc_end_command = "\x07";
// Prevent BEL/ESC/C1 bytes in the title from terminating or nesting the OSC.
let safe_title = bat::sanitize_for_terminal(&new_terminal_title);
print!("{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}");
io::stdout().flush().unwrap();
write!(
io::stdout(),
"{osc_command_for_setting_terminal_title}{safe_title}{osc_end_command}"
)?;
io::stdout().flush()?;
Ok(())
}
fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
@@ -286,13 +290,13 @@ fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Resu
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
let controller = Controller::new(config, &assets);
if config.paging_mode != PagingMode::Never && config.set_terminal_title {
set_terminal_title_to(get_new_terminal_title(&inputs));
set_terminal_title_to(get_new_terminal_title(&inputs))?;
}
controller.run(inputs, None)
}
#[cfg(feature = "bugreport")]
fn invoke_bugreport(app: &App, cache_dir: &Path) {
fn invoke_bugreport(app: &App, cache_dir: &Path) -> Result<()> {
use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry};
struct ColorSchemeCollector;
@@ -370,7 +374,8 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
))
};
report.print::<Markdown>();
writeln!(io::stdout(), "{}", report.format::<Markdown>())?;
Ok(())
}
/// Returns `Err(..)` upon fatal errors. Otherwise, returns `Ok(true)` on full success and
@@ -382,21 +387,22 @@ fn run() -> Result<bool> {
if app.matches.get_flag("diagnostic") {
#[cfg(feature = "bugreport")]
invoke_bugreport(&app, cache_dir);
invoke_bugreport(&app, cache_dir)?;
#[cfg(not(feature = "bugreport"))]
println!("bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available.");
writeln!(io::stdout(), "bat has been built without the 'bugreport' feature. The '--diagnostic' option is not available.")?;
return Ok(true);
}
#[cfg(feature = "application")]
if let Some(shell) = app.matches.get_one::<String>("completion") {
match shell.as_str() {
"bash" => println!("{}", completions::BASH_COMPLETION),
"fish" => println!("{}", completions::FISH_COMPLETION),
"ps1" => println!("{}", completions::PS1_COMPLETION),
"zsh" => println!("{}", completions::ZSH_COMPLETION),
let completion = match shell.as_str() {
"bash" => completions::BASH_COMPLETION,
"fish" => completions::FISH_COMPLETION,
"ps1" => completions::PS1_COMPLETION,
"zsh" => completions::ZSH_COMPLETION,
_ => unreachable!("No completion for shell '{shell}' available."),
}
};
writeln!(io::stdout(), "{completion}")?;
return Ok(true);
}
@@ -435,7 +441,7 @@ fn run() -> Result<bool> {
list_themes(&config, config_dir, cache_dir, app.theme_options())?;
Ok(true)
} else if app.matches.get_flag("config-file") {
println!("{}", config_file().to_string_lossy());
writeln!(io::stdout(), "{}", config_file().to_string_lossy())?;
Ok(true)
} else if app.matches.get_flag("generate-config-file") {
generate_config_file()?;
+4 -4
View File
@@ -98,20 +98,20 @@ pub struct Config<'a> {
/// cached assets) are not available, assets from the binary will be used instead.
pub use_custom_assets: bool,
// Whether or not to use $LESSOPEN if set
/// Whether or not to use $LESSOPEN if set
#[cfg(feature = "lessopen")]
pub use_lessopen: bool,
// Whether or not to set terminal title when using a pager
/// Whether or not to set terminal title when using a pager
pub set_terminal_title: bool,
/// The maximum number of consecutive empty lines to display
pub squeeze_lines: Option<usize>,
// Whether or not to strip ANSI escape codes from the input
/// Whether or not to strip ANSI escape codes from the input
pub strip_ansi: StripAnsiMode,
// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
/// Substitute terminal-active and spoofing-relevant bytes; implies strip_ansi.
pub sanitize: StripAnsiMode,
/// Whether or not to produce no output when input is empty
+6 -1
View File
@@ -291,7 +291,12 @@ impl<'a> InputReader<'a> {
if content_type == Some(ContentType::UTF_16LE) {
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?;
} else if content_type == Some(ContentType::UTF_16BE) {
} else if content_type == Some(ContentType::UTF_16BE)
// In UTF-16BE a newline is encoded as 0x00 0x0A, so `read_until(b'\n')` above
// may already have read a complete first line. Only keep reading if it did not
// (e.g. the 0x0A byte was part of another character such as U+4E0A).
&& !(first_line.len() % 2 == 0 && first_line.ends_with(&[0x00, 0x0A]))
{
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?;
}
+3 -3
View File
@@ -355,13 +355,13 @@ fn test_parse_context_fail() {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RangeCheckResult {
// Within one of the given ranges
/// Within one of the given ranges
InRange,
// Before the first range or within two ranges
/// Before the first range or within two ranges
BeforeOrBetweenRanges,
// Line number is outside of all ranges and larger than the last range.
/// Line number is outside of all ranges and larger than the last range.
AfterLastRange,
}
+10
View File
@@ -302,11 +302,21 @@ pub fn strip_overstrike(line: &str, first_backspace: usize) -> String {
output
}
/// Specify when to strip ANSI escape sequences from the input.
///
/// Used for both the `--strip-ansi` and the `--sanitize` option, the latter
/// implying the former with the same value.
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub enum StripAnsiMode {
/// Keep escape sequences in the output
#[default]
Never,
/// Strip escape sequences from the output
Always,
/// Strip escape sequences unless the syntax highlighting language is plain
/// text, in which case they may be part of what the input wants to display
Auto,
}
@@ -1,5 +1,6 @@
# see `man quadlet`
[mappings]
"INI" = [
"*.quadlets",
"**/containers/systemd/**/*.{container,volume,network,kube,image,build,pod,artifact}",
]
+29 -3
View File
@@ -173,8 +173,10 @@ impl Attributes {
2 => self.dim = "\x1B[2m".to_owned(),
3 => self.italic = "\x1B[3m".to_owned(),
4 => self.underline = "\x1B[4m".to_owned(),
9 => self.strike = "\x1B[9m".to_owned(),
23 => self.italic.clear(),
24 => self.underline.clear(),
29 => self.strike.clear(),
22 => {
self.bold.clear();
self.dim.clear();
@@ -1112,10 +1114,10 @@ mod tests {
fn test_sgr_attributes_do_not_leak_into_wrong_field() {
let mut attrs = crate::vscreen::Attributes::new();
// Bold, Dim, Italic, Underline, Foreground, Background
// Bold, Dim, Italic, Underline, Strike, Foreground, Background
attrs.update(EscapeSequence::CSI {
raw_sequence: "\x1B[1;2;3;4;31;41m",
parameters: "1;2;3;4;31;41",
raw_sequence: "\x1B[1;2;3;4;9;31;41m",
parameters: "1;2;3;4;9;31;41",
intermediates: "",
final_byte: "m",
});
@@ -1124,6 +1126,7 @@ mod tests {
assert_eq!(attrs.dim, "\x1B[2m");
assert_eq!(attrs.italic, "\x1B[3m");
assert_eq!(attrs.underline, "\x1B[4m");
assert_eq!(attrs.strike, "\x1B[9m");
assert_eq!(attrs.foreground, "\x1B[31m");
assert_eq!(attrs.background, "\x1B[41m");
@@ -1140,4 +1143,27 @@ mod tests {
assert_eq!(attrs.foreground, "\x1B[94m");
assert_eq!(attrs.background, "\x1B[103m");
}
#[test]
fn test_sgr_strike_is_tracked_and_cleared() {
let mut attrs = crate::vscreen::Attributes::new();
// Strikethrough on (SGR 9) must be tracked so it can be re-emitted.
attrs.update(EscapeSequence::CSI {
raw_sequence: "\x1B[9m",
parameters: "9",
intermediates: "",
final_byte: "m",
});
assert_eq!(attrs.strike, "\x1B[9m");
// Strikethrough off (SGR 29) must clear it, just like italic (23) and underline (24).
attrs.update(EscapeSequence::CSI {
raw_sequence: "\x1B[29m",
parameters: "29",
intermediates: "",
final_byte: "m",
});
assert_eq!(attrs.strike, "");
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
pub enum WrappingMode {
Character,
Word,
// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
/// The bool specifies whether wrapping has been explicitly disabled by the user via --wrap=never
NoWrapping(bool),
}
+94 -3
View File
@@ -25,10 +25,8 @@ mod unix {
use unix::*;
mod utils;
use utils::command::{bat, bat_with_config};
use utils::command::{bat, bat_raw_command, bat_raw_command_with_config, bat_with_config};
#[cfg(unix)]
use utils::command::bat_raw_command;
use utils::mocked_pagers;
const EXAMPLES_DIR: &str = "tests/examples";
@@ -60,6 +58,67 @@ fn stdin() {
.stdout("foo\nbar\n");
}
/// Runs `bat` with `args` while its stdout is a pipe whose read end has
/// already been closed, so the very first write fails with `BrokenPipe`
/// (EPIPE on Unix, `ERROR_NO_DATA` on Windows).
fn assert_quiet_exit_when_stdout_is_closed(command: &mut std::process::Command, args: &[&str]) {
use std::process::Stdio;
let (pipe_reader, pipe_writer) = std::io::pipe().expect("could not create a pipe");
drop(pipe_reader);
let output = command
.args(args)
.stdin(Stdio::null())
.stdout(pipe_writer)
.stderr(Stdio::piped())
.spawn()
.and_then(|child| child.wait_with_output())
.expect("could not run bat");
assert!(
output.status.success(),
"bat {args:?} should exit quietly when stdout is closed, status: {}, stderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
assert!(
output.stderr.is_empty(),
"bat {args:?} should not print to stderr when stdout is closed, stderr: {}",
String::from_utf8_lossy(&output.stderr),
);
}
#[test]
fn auxiliary_output_does_not_panic_on_closed_stdout() {
// Auxiliary outputs used to be written via `println!`, which panics when
// the reader of a piped stdout is already gone (e.g. `bat --config-file |
// head -n 0`). The main output path has exited quietly on `BrokenPipe`
// since #232; auxiliary outputs should behave the same way.
assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--config-file"]);
assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--diagnostic"]);
// The `cache` subcommand is only enabled when the working directory does
// not contain a file named `cache` (see `cache_clear`), and it conflicts
// with top-level flags like `--no-config`. Run it from a temporary
// directory that doubles as `BAT_CACHE_PATH`, leaving the real cache alone.
let temp_dir = tempdir().expect("could not create a temporary directory");
let mut command = bat_raw_command_with_config();
command
.current_dir(temp_dir.path())
.env("BAT_CONFIG_PATH", temp_dir.path().join("config"))
.env("BAT_CACHE_PATH", temp_dir.path());
assert_quiet_exit_when_stdout_is_closed(&mut command, &["cache", "--clear"]);
}
#[cfg(feature = "application")]
#[test]
fn completion_does_not_panic_on_closed_stdout() {
for shell in ["bash", "fish", "ps1", "zsh"] {
assert_quiet_exit_when_stdout_is_closed(&mut bat_raw_command(), &["--completion", shell]);
}
}
#[test]
fn concatenate() {
bat()
@@ -2042,6 +2101,19 @@ fn utf16be() {
.stdout(" 1 上一伊刀\n 2 foo bar\n 3 hello world\n");
}
#[test]
fn utf16be_first_line_not_merged_with_second() {
// The first line of a UTF-16BE file must not swallow the second line
bat()
.arg("--decorations=always")
.arg("--style=numbers")
.arg("--color=never")
.arg("test_UTF-16BE.txt")
.assert()
.success()
.stdout(" 1 hello world\n 2 this is a test\n");
}
// Regression test for https://github.com/sharkdp/bat/issues/1922
#[test]
fn bom_not_stripped_in_loop_through_mode() {
@@ -3355,6 +3427,25 @@ fn ansi_sgr_joins_attributes_when_wrapped() {
.stderr("");
}
// Ensure that strikethrough (SGR 9) is tracked like the other attributes, so it is
// re-emitted (and therefore preserved) across a wrap boundary instead of being dropped.
#[test]
fn ansi_sgr_strike_joins_attributes_when_wrapped() {
bat()
.arg("--paging=never")
.arg("--color=never")
.arg("--terminal-width=20")
.arg("--wrap=character")
.arg("--decorations=always")
.arg("--style=plain")
.write_stdin("\x1B[33mColor. \x1B[9mStrike.......Struck+color.\n")
.assert()
.success()
.stdout("\x1B[33m\x1B[33mColor. \x1B[9m\x1B[33m\x1B[9mStrike.......\n\x1B[33m\x1B[9mStruck+color.\n")
// FIXME: ~~~~~~~~ ~~~~~~~~~~~~~~~ should not be emitted twice.
.stderr("");
}
#[test]
fn ignored_suffix_arg() {
bat()