diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b0e636..ca7ab4d5 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. 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/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 diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 4c0ade1e..f8b429cb 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,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(); + 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") { @@ -91,6 +127,7 @@ impl App { Ok(App { matches, interactive_output, + number_from_cli, }) } @@ -384,8 +421,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.number_from_cli), tab_width: self .matches .get_one::("tabs") diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 5dc35d6e..8a653818 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -166,6 +166,53 @@ 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 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() @@ -415,26 +462,18 @@ 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] -#[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") .assert() .success() - .stdout( - "─────┬────────────────────────────────────────────────────────────────────────── - │ STDIN -─────┼────────────────────────────────────────────────────────────────────────── - 1 │ hello - 2 │ world -─────┴────────────────────────────────────────────────────────────────────────── -", - ); + .stdout("hello\nworld\n"); } #[test] @@ -452,6 +491,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()