diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index ae889453..542e5d99 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -136,6 +136,13 @@ impl App { } }); + match self.matches.values_of("file-name") { + Some(filenames) if filenames.len() != files.len() => { + return Err(format!("{} {}", filenames.len(), files.len()).into()); + } + _ => {} + } + Ok(Config { true_color: is_truecolor_terminal(), language: self.matches.value_of("language").or_else(|| { @@ -222,6 +229,10 @@ impl App { .map(LineRanges::from) .map(|lr| HighlightedLineRanges(lr)) .unwrap_or_default(), + filenames: self + .matches + .values_of("file-name") + .map(|values| values.collect()), }) } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 2f7c0a5a..c2505ca9 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -93,6 +93,18 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { '--highlight-line 40:' highlights lines 40 to the end of the file" ), ) + .arg( + Arg::with_name("file-name") + .long("file-name") + .takes_value(true) + .number_of_values(1) + .multiple(true) + .value_name("name") + .help("Specify the name to display for a file.") + .long_help("Specify the name to display for a file. Useful when piping \ + data to bat from STDIN when bat does not otherwise know \ + the filename."), + ) .arg( Arg::with_name("tabs") .long("tabs") diff --git a/src/config.rs b/src/config.rs index 19e91484..6a0c2530 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,9 @@ pub struct Config<'a> { /// Ranges of lines which should be highlighted with a special background color pub highlighted_lines: HighlightedLineRanges, + + /// Names of files to display when printing + pub filenames: Option>, } #[test] diff --git a/src/controller.rs b/src/controller.rs index ee1a1be3..90e465bd 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -45,7 +45,12 @@ impl<'b> Controller<'b> { let stdin = io::stdin(); - for input_file in &self.config.files { + let filenames: Box> = match self.config.filenames { + Some(ref filenames) => Box::new(filenames.into_iter().map(|name| Some(*name))), + None => Box::new(std::iter::repeat(None)), + }; + + for (input_file, file_name) in self.config.files.iter().zip(filenames) { match input_file.get_reader(&stdin) { Err(error) => { handle_error(&error); @@ -54,7 +59,7 @@ impl<'b> Controller<'b> { Ok(mut reader) => { let result = if self.config.loop_through { let mut printer = SimplePrinter::new(); - self.print_file(reader, &mut printer, writer, *input_file) + self.print_file(reader, &mut printer, writer, *input_file, file_name) } else { let mut printer = InteractivePrinter::new( &self.config, @@ -62,7 +67,7 @@ impl<'b> Controller<'b> { *input_file, &mut reader, ); - self.print_file(reader, &mut printer, writer, *input_file) + self.print_file(reader, &mut printer, writer, *input_file, file_name) }; if let Err(error) = result { @@ -82,9 +87,10 @@ impl<'b> Controller<'b> { printer: &mut P, writer: &mut dyn Write, input_file: InputFile<'a>, + file_name: Option<&str>, ) -> Result<()> { if !reader.first_line.is_empty() || self.config.style_components.header() { - printer.print_header(writer, input_file)?; + printer.print_header(writer, input_file, file_name)?; } if !reader.first_line.is_empty() { diff --git a/src/printer.rs b/src/printer.rs index 629b11da..3924cd62 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -34,7 +34,12 @@ use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::wrap::OutputWrap; pub trait Printer { - fn print_header(&mut self, handle: &mut dyn Write, file: InputFile) -> Result<()>; + fn print_header( + &mut self, + handle: &mut dyn Write, + file: InputFile, + file_name: Option<&str>, + ) -> Result<()>; fn print_footer(&mut self, handle: &mut dyn Write) -> Result<()>; fn print_snip(&mut self, handle: &mut dyn Write) -> Result<()>; @@ -57,7 +62,12 @@ impl SimplePrinter { } impl Printer for SimplePrinter { - fn print_header(&mut self, _handle: &mut dyn Write, _file: InputFile) -> Result<()> { + fn print_header( + &mut self, + _handle: &mut dyn Write, + _file: InputFile, + _file_name: Option<&str>, + ) -> Result<()> { Ok(()) } @@ -224,14 +234,20 @@ impl<'a> InteractivePrinter<'a> { } impl<'a> Printer for InteractivePrinter<'a> { - fn print_header(&mut self, handle: &mut dyn Write, file: InputFile) -> Result<()> { + fn print_header( + &mut self, + handle: &mut dyn Write, + file: InputFile, + file_name: Option<&str>, + ) -> Result<()> { if !self.config.style_components.header() { if Some(ContentType::BINARY) == self.content_type && !self.config.show_nonprintable { let input = match file { - InputFile::Ordinary(filename) => { - format!("file '{}'", filename.to_string_lossy()) - } - _ => "STDIN".into(), + InputFile::Ordinary(filename) => format!( + "file '{}'", + file_name.unwrap_or(&filename.to_string_lossy()) + ), + _ => file_name.unwrap_or("STDIN").to_owned(), }; writeln!( @@ -266,8 +282,11 @@ impl<'a> Printer for InteractivePrinter<'a> { } let (prefix, name) = match file { - InputFile::Ordinary(filename) => ("File: ", filename.to_string_lossy()), - _ => ("", Cow::from("STDIN")), + InputFile::Ordinary(filename) => ( + "File: ", + Cow::from(file_name.unwrap_or(&filename.to_string_lossy()).to_owned()), + ), + _ => ("File: ", Cow::from(file_name.unwrap_or("STDIN").to_owned())), }; let mode = match self.content_type { diff --git a/tests/examples/test.binary b/tests/examples/test.binary new file mode 100644 index 00000000..593f4708 Binary files /dev/null and b/tests/examples/test.binary differ diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 0d5f4a3c..7750f6cd 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -541,3 +541,89 @@ fn empty_file_leads_to_empty_output_with_grid_enabled() { .success() .stdout(""); } + +#[test] +fn filename_basic() { + bat() + .arg("test.txt") + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo\n") + .stderr(""); +} + +#[test] +fn filename_binary() { + bat() + .arg("test.binary") + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo \n") + .stderr(""); +} + +#[test] +fn filename_stdin() { + bat() + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("-") + .write_stdin("stdin\n") + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo\n") + .stderr(""); +} + +#[test] +fn filename_stdin_binary() { + let vec = vec![0; 1]; + bat_with_config() + .arg("--decorations=always") + .arg("--style=header") + .write_stdin(vec) + .arg("--file-name=foo") + .assert() + .success() + .stdout("File: foo \n") + .stderr(""); +} + +#[test] +fn filename_multiple_ok() { + bat() + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("test.txt") + .arg("--file-name=foo") + .arg("single-line.txt") + .arg("--file-name=bar") + .assert() + .success() + .stdout("File: foo\nFile: bar\n") + .stderr(""); +} + +#[test] +fn filename_multiple_err() { + bat() + .arg("--decorations=always") + .arg("--style=header") + .arg("-r=0:0") + .arg("test.txt") + .arg("--file-name=foo") + .arg("single-line.txt") + .assert() + .failure(); +}