Repository: sharkdp/bat (language: rust)

Issue:
Allow `bat` to render some binary content
```
$ bat /proc/3151/cmdline
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       │ File: /proc/3151/cmdline   <BINARY>
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
$ /bin/cat /proc/3151/cmdline;
sleep100%
```

I was trying to read `/proc/[pid]/cmdline` using `bat` and expected it to behave like `cat`, but it doesn't.

Should any exceptions be made for the `/proc` filesystem?

See `man 5 proc`:

```
PROC(5)                                                   Linux Programmer's Manual                                                   PROC(5)

NAME
       proc - process information pseudo-filesystem

DESCRIPTION
       The  proc  filesystem  is a pseudo-filesystem which provides an interface to kernel data structures.  It is commonly mounted at /proc.
       Typically, it is mounted automatically by the system, but it can also be mounted manually using a command such as:

           mount -t proc proc /proc

       Most of the files in the proc filesystem are read-only, but some files are writable, allowing kernel variables to be changed.
```


Discussion / hints:
Although it still doesn't quite behave like `cat`, you could use:
```bash
bat --show-all /proc/[pid]/cmdline
```
which will look something like this:
```
───────┬────────────────────────────────────────────────────────────────────────
       │ File: /proc/5877/cmdline   <BINARY>
───────┼────────────────────────────────────────────────────────────────────────
   1   │ sleep␀100␀
───────┴────────────────────────────────────────────────────────────────────────
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55879dce12..165cf2c223 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 - Syntax highlighting for JavaScript files that start with `#!/usr/bin/env bun` #2913 (@sharunkumar)
 - `bat --strip-ansi={never,always,auto}` to remove ANSI escape sequences from bat's input, see #2999 (@eth-p)
 - Add or remove individual style components without replacing all styles #2929 (@eth-p)
+- Add option `--binary=as-text` for printing binary content, see issue #2974 and PR #2976 (@einfachIrgendwer0815)
 
 ## Bugfixes
 
diff --git a/doc/long-help.txt b/doc/long-help.txt
index 2b03490f33..87fb5d9663 100644
--- a/doc/long-help.txt
+++ b/doc/long-help.txt
@@ -20,6 +20,13 @@ Options:
             * unicode (␇, ␊, ␀, ..)
             * caret   (^G, ^J, ^@, ..)
 
+      --binary <behavior>
+          How to treat binary content. (default: no-printing)
+          
+          Possible values:
+            * no-printing: do not print any binary content
+            * as-text: treat binary content as normal text
+
   -p, --plain...
           Only show plain style, no decorations. This is an alias for '--style=plain'. When '-p' is
           used twice ('-pp'), it also disables automatic paging (alias for '--style=plain
diff --git a/doc/short-help.txt b/doc/short-help.txt
index 305bbf3ddd..16b9eb05c0 100644
--- a/doc/short-help.txt
+++ b/doc/short-help.txt
@@ -11,6 +11,8 @@ Options:
           Show non-printable characters (space, tab, newline, ..).
       --nonprintable-notation <notation>
           Set notation for non-printable characters.
+      --binary <behavior>
+          How to treat binary content. (default: no-printing)
   -p, --plain...
           Show plain style (alias for '--style=plain').
   -l, --language <language>
diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs
index d662866875..4d52c73dfc 100644
--- a/src/bin/bat/app.rs
+++ b/src/bin/bat/app.rs
@@ -9,6 +9,7 @@ use crate::{
     config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars},
 };
 use bat::style::StyleComponentList;
+use bat::BinaryBehavior;
 use bat::StripAnsiMode;
 use clap::ArgMatches;
 
@@ -193,6 +194,11 @@ impl App {
                 Some("caret") => NonprintableNotation::Caret,
                 _ => unreachable!("other values for --nonprintable-notation are not allowed"),
             },
+            binary: match self.matches.get_one::<String>("binary").map(|s| s.as_str()) {
+                Some("as-text") => BinaryBehavior::AsText,
+                Some("no-printing") => BinaryBehavior::NoPrinting,
+                _ => unreachable!("other values for --binary are not allowed"),
+            },
             wrapping_mode: if self.interactive_output || maybe_term_width.is_some() {
                 if !self.matches.get_flag("chop-long-lines") {
                     match self.matches.get_one::<String>("wrap").map(|s| s.as_str()) {
diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs
index 33dde9801b..4327fa1545 100644
--- a/src/bin/bat/clap_app.rs
+++ b/src/bin/bat/clap_app.rs
@@ -77,6 +77,22 @@ pub fn build_app(interactive_output: bool) -> Command {
                     * caret   (^G, ^J, ^@, ..)",
                 ),
         )
+        .arg(
+            Arg::new("binary")
+                .long("binary")
+                .action(ArgAction::Set)
+                .default_value("no-printing")
+                .value_parser(["no-printing", "as-text"])
+                .value_name("behavior")
+                .hide_default_value(true)
+                .help("How to treat binary content. (default: no-printing)")
+                .long_help(
+                    "How to treat binary content. (default: no-printing)\n\n\
+                    Possible values:\n  \
+                    * no-printing: do not print any binary content\n  \
+                    * as-text: treat binary content as normal text",
+                ),
+        )
         .arg(
             Arg::new("plain")
                 .overrides_with("plain")
diff --git a/src/config.rs b/src/config.rs
index eb7df8ee9f..eb44281ccb 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,5 +1,5 @@
 use crate::line_range::{HighlightedLineRanges, LineRanges};
-use crate::nonprintable_notation::NonprintableNotation;
+use crate::nonprintable_notation::{BinaryBehavior, NonprintableNotation};
 #[cfg(feature = "paging")]
 use crate::paging::PagingMode;
 use crate::style::StyleComponents;
@@ -44,6 +44,9 @@ pub struct Config<'a> {
     /// The configured notation for non-printable characters
     pub nonprintable_notation: NonprintableNotation,
 
+    /// How to treat binary content
+    pub binary: BinaryBehavior,
+
     /// The character width of the terminal
     pub term_width: usize,
 
diff --git a/src/lib.rs b/src/lib.rs
index 23c4a80033..a015913e40 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -52,7 +52,7 @@ mod terminal;
 mod vscreen;
 pub(crate) mod wrapping;
 
-pub use nonprintable_notation::NonprintableNotation;
+pub use nonprintable_notation::{BinaryBehavior, NonprintableNotation};
 pub use preprocessor::StripAnsiMode;
 pub use pretty_printer::{Input, PrettyPrinter, Syntax};
 pub use syntax_mapping::{MappingTarget, SyntaxMapping};
diff --git a/src/nonprintable_notation.rs b/src/nonprintable_notation.rs
index ff09aca686..9f8d7cb8d5 100644
--- a/src/nonprintable_notation.rs
+++ b/src/nonprintable_notation.rs
@@ -10,3 +10,15 @@ pub enum NonprintableNotation {
     #[default]
     Unicode,
 }
+
+/// How to treat binary content
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum BinaryBehavior {
+    /// Do not print any binary content
+    #[default]
+    NoPrinting,
+
+    /// Treat binary content as normal text
+    AsText,
+}
diff --git a/src/printer.rs b/src/printer.rs
index e9bea3fd8f..95017188c3 100644
--- a/src/printer.rs
+++ b/src/printer.rs
@@ -35,6 +35,7 @@ use crate::style::StyleComponent;
 use crate::terminal::{as_terminal_escaped, to_ansi_color};
 use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
 use crate::wrapping::WrappingMode;
+use crate::BinaryBehavior;
 use crate::StripAnsiMode;
 
 const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
@@ -268,7 +269,8 @@ impl<'a> InteractivePrinter<'a> {
             .content_type
             .map_or(false, |c| c.is_binary() && !config.show_nonprintable);
 
-        let needs_to_match_syntax = !is_printing_binary
+        let needs_to_match_syntax = (!is_printing_binary
+            || matches!(config.binary, BinaryBehavior::AsText))
             && (config.colored_output || config.strip_ansi == StripAnsiMode::Auto);
 
         let (is_plain_text, highlighter_from_set) = if needs_to_match_syntax {
@@ -458,7 +460,10 @@ impl<'a> Printer for InteractivePrinter<'a> {
         }
 
         if !self.config.style_components.header() {
-            if Some(ContentType::BINARY) == self.content_type && !self.config.show_nonprintable {
+            if Some(ContentType::BINARY) == self.content_type
+                && !self.config.show_nonprintable
+                && !matches!(self.config.binary, BinaryBehavior::AsText)
+            {
                 writeln!(
                     handle,
                     "{}: Binary content from {} will not be printed to the terminal \
@@ -539,7 +544,10 @@ impl<'a> Printer for InteractivePrinter<'a> {
             })?;
 
         if self.config.style_components.grid() {
-            if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
+            if self.content_type.map_or(false, |c| c.is_text())
+                || self.config.show_nonprintable
+                || matches!(self.config.binary, BinaryBehavior::AsText)
+            {
                 self.print_horizontal_line(handle, '┼')?;
             } else {
                 self.print_horizontal_line(handle, '┴')?;
@@ -551,7 +559,9 @@ impl<'a> Printer for InteractivePrinter<'a> {
 
     fn print_footer(&mut self, handle: &mut OutputHandle, _input: &OpenedInput) -> Result<()> {
         if self.config.style_components.grid()
-            && (self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable)
+            && (self.content_type.map_or(false, |c| c.is_text())
+                || self.config.show_nonprintable
+                || matches!(self.config.binary, BinaryBehavior::AsText))
         {
             self.print_horizontal_line(handle, '┴')
         } else {
@@ -599,7 +609,9 @@ impl<'a> Printer for InteractivePrinter<'a> {
             .into()
         } else {
             let mut line = match self.content_type {
-                Some(ContentType::BINARY) | None => {
+                Some(ContentType::BINARY) | None
+                    if !matches!(self.config.binary, BinaryBehavior::AsText) =>
+                {
                     return Ok(());
                 }
                 Some(ContentType::UTF_16LE) => UTF_16LE.decode_with_bom_removal(line_buffer).0,


Test patch:
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 13b357184f..5f5b9fd7bc 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -1938,6 +1938,16 @@ fn show_all_with_unicode() {
         .stderr("");
 }
 
+#[test]
+fn binary_as_text() {
+    bat()
+        .arg("--binary=as-text")
+        .arg("control_characters.txt")
+        .assert()
+        .stdout("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F")
+        .stderr("");
+}
+
 #[test]
 fn no_paging_arg() {
     bat()