From 7d37325c19cc08b6d7d1fe5e3f87a3d93a85b860 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Sat, 29 Nov 2025 17:32:49 +0200 Subject: [PATCH 1/5] Fix to not show style decorations in auto mode when piping --- CHANGELOG.md | 1 + src/bin/bat/app.rs | 3 +-- tests/integration_tests.rs | 13 +++---------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b0e636..80d9c492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42) - Fix negative values of N not being parsed in line ranges without `=` flag value separator, see #3442 (@lmmx) - Fix broken Docker syntax preventing use of custom assets, see #3476 (@keith-hall) +- Fix decorations being applied unexpectedly when piping, breaking cat compatibility. See #3496 (@keith-hall) ## Other - Improve README documentation on pager options passed to less, see #3443 (@injust) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 4c0ade1e..8f9cc871 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -384,8 +384,7 @@ impl App { .map(|s| s.as_str()) == Some("always") || self.matches.get_flag("force-colorization") - || self.matches.get_flag("number") - || self.matches.contains_id("style") && !style_components.plain()), + || self.matches.get_flag("number")), tab_width: self .matches .get_one::("tabs") diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 5dc35d6e..d5ce3d03 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -415,7 +415,7 @@ fn piped_output_with_line_numbers_style_flag() { .write_stdin("hello\nworld\n") .assert() .success() - .stdout(" 1 hello\n 2 world\n"); + .stdout("hello\nworld\n"); } #[test] @@ -426,15 +426,7 @@ fn piped_output_with_line_numbers_with_header_grid_style_flag() { .write_stdin("hello\nworld\n") .assert() .success() - .stdout( - "─────┬────────────────────────────────────────────────────────────────────────── - │ STDIN -─────┼────────────────────────────────────────────────────────────────────────── - 1 │ hello - 2 │ world -─────┴────────────────────────────────────────────────────────────────────────── -", - ); + .stdout("hello\nworld\n"); } #[test] @@ -452,6 +444,7 @@ fn piped_output_with_auto_style() { fn piped_output_with_default_style_flag() { bat() .arg("--style=default") + .arg("--decorations=always") .write_stdin("hello\nworld\n") .assert() .success() From f9c33971d9f8805823f369435c987db3e423d18f Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Thu, 27 Nov 2025 22:21:21 +0200 Subject: [PATCH 2/5] Update readme to mention decorations --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 067d593f..82ebf606 100644 --- a/README.md +++ b/README.md @@ -540,6 +540,12 @@ variable to make these changes permanent or use `bat`'s > Or, if you want to override the styles completely, you use `--style=numbers` to > only show the line numbers. +### Decorations + +By default, `bat` only shows decorations (such as line numbers, file headers, grid borders, etc.) when outputting to an interactive terminal. You can control this behavior with the `--decorations` option. Use `--decorations=always` to show decorations even when piping output to another command, or `--decorations=never` to disable them entirely. Possible values are `auto` (default), `never`, and `always`. + +There is also the `--force-colorization` option, which is an alias for `--decorations=always --color=always`. This is useful if you want to keep colorization and decorations when piping `bat`'s output to another program. + ### Adding new syntaxes / language definitions Should you find that a particular syntax is not available within `bat`, you can follow these From abc72614886d93105b65573f0d709f983f54099a Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Sat, 29 Nov 2025 17:21:37 +0200 Subject: [PATCH 3/5] Fix -n flag to show line numbers in loop-through mode When the -n/--number flag is passed on the command line, bat now shows line numbers even when piping output to another process (loop-through mode), similar to how `cat -n` behaves. This change detects if -n or --number was passed on the CLI (before merging with config file and environment variables) and disables loop-through mode in that case, allowing the InteractivePrinter to add line numbers. The existing behavior is preserved: - Styles from config/env are still ignored when piping (unless --decorations=always is set) - Only the -n flag from CLI enables line numbers in piped mode - -p and --style options from CLI do not disable loop-through mode --- CHANGELOG.md | 2 +- src/bin/bat/app.rs | 16 +++++++++++++++- tests/integration_tests.rs | 13 ++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d9c492..ca7ab4d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Fix hang when using `--list-themes` with an explicit pager, see #3457 (@abhinavcool42) - Fix negative values of N not being parsed in line ranges without `=` flag value separator, see #3442 (@lmmx) - Fix broken Docker syntax preventing use of custom assets, see #3476 (@keith-hall) -- Fix decorations being applied unexpectedly when piping, breaking cat compatibility. See #3496 (@keith-hall) +- Fix decorations being applied unexpectedly when piping. Now only line numbers explicitly required on the command line should be applied in auto decorations mode for `cat` compatibility. See #3496 (@keith-hall) ## Other - Improve README documentation on pager options passed to less, see #3443 (@injust) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 8f9cc871..e1051850 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -46,6 +46,10 @@ enum HelpType { pub struct App { pub matches: ArgMatches, interactive_output: bool, + /// True if -n / --number was passed on the command line + /// (not from config file or environment variables). + /// This is used to honor the flag when piping output, similar to `cat -n`. + number_from_cli: bool, } impl App { @@ -54,6 +58,15 @@ impl App { let _ = nu_ansi_term::enable_ansi_support(); let interactive_output = std::io::stdout().is_terminal(); + + // Check if the -n / --number option was passed on the command line + // (before merging with config file and environment variables). + // This is needed to honor the -n flag when piping output, similar to `cat -n`. + let number_from_cli = wild::args_os().any(|arg| { + let arg_str = arg.to_string_lossy(); + arg_str == "-n" || arg_str == "--number" + }); + let matches = Self::matches(interactive_output)?; if matches.get_flag("help") { @@ -91,6 +104,7 @@ impl App { Ok(App { matches, interactive_output, + number_from_cli, }) } @@ -384,7 +398,7 @@ impl App { .map(|s| s.as_str()) == Some("always") || self.matches.get_flag("force-colorization") - || self.matches.get_flag("number")), + || self.number_from_cli), tab_width: self .matches .get_one::("tabs") diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d5ce3d03..6b62541c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -166,6 +166,17 @@ fn line_numbers() { .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); } +// Test that -n on command line shows line numbers even when piping (similar to `cat -n`) +#[test] +fn line_numbers_from_cli_in_loop_through_mode() { + bat() + .arg("multiline.txt") + .arg("-n") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); +} + #[test] fn line_range_2_3() { bat() @@ -419,8 +430,8 @@ fn piped_output_with_line_numbers_style_flag() { } #[test] -#[cfg(not(target_os = "windows"))] fn piped_output_with_line_numbers_with_header_grid_style_flag() { + // style ignored because non-interactive bat() .arg("--style=header,grid,numbers") .write_stdin("hello\nworld\n") From 3755f788ca1515f0b0377423cafcea643d64f227 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Sat, 29 Nov 2025 17:22:12 +0200 Subject: [PATCH 4/5] Improve -n detection to handle combined flags correctly Updated the logic to correctly handle combined short flags like -pn and -np. The -n flag is only honored when it's either: 1. A standalone flag (-n or --number) 2. The last flag in a combined form that includes -p (e.g., -pn), or 3. In a combined form without -p (e.g., -An) This ensures that -np (where -p overrides -n) correctly produces plain output, while -pn (where -n overrides -p) produces line numbers. --- src/bin/bat/app.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index e1051850..f8b429cb 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -58,15 +58,38 @@ impl App { let _ = nu_ansi_term::enable_ansi_support(); let interactive_output = std::io::stdout().is_terminal(); - + // Check if the -n / --number option was passed on the command line // (before merging with config file and environment variables). // This is needed to honor the -n flag when piping output, similar to `cat -n`. + // We need to handle both standalone (-n, --number) and combined short flags (-pn, -An, etc.) + // Note: We only check if -n appears and is not overridden by -p in the same combined flag. + // For combined flags like -np, -p comes after -n and overrides it, so we don't count it. + // For combined flags like -pn, -n comes after -p and takes effect. let number_from_cli = wild::args_os().any(|arg| { let arg_str = arg.to_string_lossy(); - arg_str == "-n" || arg_str == "--number" + if arg_str == "-n" || arg_str == "--number" { + return true; + } + // Handle combined short flags + // Only count -n if it's the LAST flag in the combined form (so -p doesn't override it) + // or if -p is not present in the combined form + if arg_str.starts_with('-') && !arg_str.starts_with("--") && arg_str.len() > 2 { + let chars: Vec = arg_str.chars().skip(1).collect(); + let n_pos = chars.iter().position(|&c| c == 'n'); + let p_pos = chars.iter().position(|&c| c == 'p'); + // -n is in the combined flag and either: + // - -p is not present, OR + // - -n comes after -p (so -n takes effect) + if let Some(n) = n_pos { + if p_pos.is_none() || n > p_pos.unwrap() { + return true; + } + } + } + false }); - + let matches = Self::matches(interactive_output)?; if matches.get_flag("help") { From 602df893de148549bf2dc45599c2a190511486cf Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Sat, 29 Nov 2025 22:03:26 +0200 Subject: [PATCH 5/5] Add additional test cases for -n in loop through mode --- tests/integration_tests.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 6b62541c..8a653818 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -177,6 +177,42 @@ fn line_numbers_from_cli_in_loop_through_mode() { .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); } +#[test] +fn style_from_env_var_ignored_and_line_numbers_from_cli_in_loop_through_mode() { + bat() + .env("BAT_STYLE", "full") + .arg("multiline.txt") + .arg("-n") + .arg("--decorations=auto") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); +} + +#[test] +fn numbers_ignored_from_cli_when_followed_by_plain_in_loop_through_mode() { + bat() + .arg("multiline.txt") + .arg("-np") + .arg("--decorations=auto") + .assert() + .success() + .stdout( + "line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\n", + ); +} + +#[test] +fn numbers_honored_from_cli_when_preceeded_by_plain_in_loop_through_mode() { + bat() + .arg("multiline.txt") + .arg("-pn") + .arg("--decorations=auto") + .assert() + .success() + .stdout(" 1 line 1\n 2 line 2\n 3 line 3\n 4 line 4\n 5 line 5\n 6 line 6\n 7 line 7\n 8 line 8\n 9 line 9\n 10 line 10\n"); +} + #[test] fn line_range_2_3() { bat()