diff --git a/examples/cat.rs b/examples/cat.rs index 73d8bb2b24..c98270bb8f 100644 --- a/examples/cat.rs +++ b/examples/cat.rs @@ -1,7 +1,7 @@ /// A very simple colorized `cat` clone, using `bat` as a library. /// See `src/bin/bat` for the full `bat` application. use bat::{ - config::{Config, InputFile, StyleComponent, StyleComponents}, + config::{Config, InputFile, OrdinaryFile, StyleComponent, StyleComponents}, Controller, HighlightingAssets, }; use console::Term; @@ -24,7 +24,10 @@ fn main() { StyleComponent::Grid, StyleComponent::Numbers, ]), - files: files.iter().map(|file| InputFile::Ordinary(file)).collect(), + files: files + .iter() + .map(|file| InputFile::Ordinary(OrdinaryFile::from_path(file))) + .collect(), ..Default::default() }; let assets = HighlightingAssets::from_binary(); diff --git a/examples/simple.rs b/examples/simple.rs index 009e10c713..a0d7afa1fc 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,6 +1,6 @@ /// A simple program that prints its own source code using the bat library use bat::{ - config::{Config, InputFile}, + config::{Config, InputFile, OrdinaryFile}, Controller, HighlightingAssets, }; use std::ffi::OsStr; @@ -9,7 +9,9 @@ fn main() { let path_to_this_file = OsStr::new(file!()); let config = Config { - files: vec![InputFile::Ordinary(path_to_this_file)], + files: vec![InputFile::Ordinary(OrdinaryFile::from_path( + path_to_this_file, + ))], colored_output: true, true_color: true, ..Default::default() diff --git a/src/assets.rs b/src/assets.rs index bc87eb361a..6aca40be38 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use std::ffi::OsStr; use std::fs::{self, File}; use std::io::BufReader; use std::path::Path; @@ -176,25 +177,15 @@ impl HighlightingAssets { pub(crate) fn get_syntax( &self, language: Option<&str>, - filename: InputFile, + file: InputFile, reader: &mut InputFileReader, mapping: &SyntaxMapping, ) -> &SyntaxReference { - let syntax = match (language, filename) { + let syntax = match (language, file) { (Some(language), _) => self.syntax_set.find_syntax_by_token(language), - (None, InputFile::Ordinary(filename)) => { - let path = Path::new(filename); - - let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); - let extension = path.extension().and_then(|x| x.to_str()).unwrap_or(""); - - let ext_syntax = self - .syntax_set - .find_syntax_by_extension(&file_name) - .or_else(|| self.syntax_set.find_syntax_by_extension(&extension)); - let line_syntax = String::from_utf8(reader.first_line.clone()) - .ok() - .and_then(|l| self.syntax_set.find_syntax_by_first_line(&l)); + (None, InputFile::Ordinary(ofile)) => { + let path = Path::new(ofile.provided_path()); + let line_syntax = self.get_first_line_syntax(reader); let absolute_path = path.canonicalize().ok().unwrap_or(path.to_owned()); match mapping.get_syntax_for(absolute_path) { @@ -204,17 +195,42 @@ impl HighlightingAssets { self.syntax_set.find_syntax_by_name(syntax_name) } Some(MappingTarget::MapToUnknown) => line_syntax, - None => ext_syntax.or(line_syntax), + None => { + let file_name = path.file_name().unwrap_or_default(); + self.get_extension_syntax(file_name).or(line_syntax) + } } } - (None, InputFile::StdIn) => String::from_utf8(reader.first_line.clone()) + (None, InputFile::StdIn(None)) => String::from_utf8(reader.first_line.clone()) .ok() .and_then(|l| self.syntax_set.find_syntax_by_first_line(&l)), + (None, InputFile::StdIn(Some(file_name))) => self + .get_extension_syntax(file_name) + .or(self.get_first_line_syntax(reader)), (_, InputFile::ThemePreviewFile) => self.syntax_set.find_syntax_by_name("Rust"), }; syntax.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()) } + + fn get_extension_syntax(&self, file_name: &OsStr) -> Option<&SyntaxReference> { + self.syntax_set + .find_syntax_by_extension(file_name.to_str().unwrap_or_default()) + .or_else(|| { + self.syntax_set.find_syntax_by_extension( + Path::new(file_name) + .extension() + .and_then(|x| x.to_str()) + .unwrap_or_default(), + ) + }) + } + + fn get_first_line_syntax(&self, reader: &mut InputFileReader) -> Option<&SyntaxReference> { + String::from_utf8(reader.first_line.clone()) + .ok() + .and_then(|l| self.syntax_set.find_syntax_by_first_line(&l)) + } } #[cfg(test)] @@ -227,7 +243,7 @@ mod tests { use tempdir::TempDir; use crate::assets::HighlightingAssets; - use crate::inputfile::InputFile; + use crate::inputfile::{InputFile, OrdinaryFile}; use crate::syntax_mapping::{MappingTarget, SyntaxMapping}; struct SyntaxDetectionTest<'a> { @@ -246,26 +262,45 @@ mod tests { } } - fn synax_for_file_with_content(&self, file_name: &str, first_line: &str) -> String { + fn syntax_for_file_with_content_os(&self, file_name: &OsStr, first_line: &str) -> String { let file_path = self.temp_dir.path().join(file_name); { let mut temp_file = File::create(&file_path).unwrap(); writeln!(temp_file, "{}", first_line).unwrap(); } - let input_file = InputFile::Ordinary(OsStr::new(&file_path)); + let input_file = InputFile::Ordinary(OrdinaryFile::from_path(file_path.as_os_str())); let syntax = self.assets.get_syntax( None, input_file, - &mut input_file.get_reader(&io::stdin()).unwrap(), + &mut input_file.get_reader(io::stdin().lock()).unwrap(), &self.syntax_mapping, ); syntax.name.clone() } + fn syntax_for_file_os(&self, file_name: &OsStr) -> String { + self.syntax_for_file_with_content_os(file_name, "") + } + + fn syntax_for_file_with_content(&self, file_name: &str, first_line: &str) -> String { + self.syntax_for_file_with_content_os(OsStr::new(file_name), first_line) + } + fn syntax_for_file(&self, file_name: &str) -> String { - self.synax_for_file_with_content(file_name, "") + self.syntax_for_file_with_content(file_name, "") + } + + fn syntax_for_stdin_with_content(&self, file_name: &str, content: &[u8]) -> String { + let input_file = InputFile::StdIn(Some(OsStr::new(file_name))); + let syntax = self.assets.get_syntax( + None, + input_file, + &mut input_file.get_reader(content).unwrap(), + &self.syntax_mapping, + ); + syntax.name.clone() } } @@ -284,6 +319,19 @@ mod tests { assert_eq!(test.syntax_for_file("Makefile"), "Makefile"); } + #[cfg(unix)] + #[test] + fn syntax_detection_invalid_utf8() { + use std::os::unix::ffi::OsStrExt; + + let test = SyntaxDetectionTest::new(); + + assert_eq!( + test.syntax_for_file_os(OsStr::from_bytes(b"invalid_\xFEutf8_filename.rs")), + "Rust" + ); + } + #[test] fn syntax_detection_well_defined_mapping_for_duplicate_extensions() { let test = SyntaxDetectionTest::new(); @@ -299,15 +347,15 @@ mod tests { let test = SyntaxDetectionTest::new(); assert_eq!( - test.synax_for_file_with_content("my_script", "#!/bin/bash"), + test.syntax_for_file_with_content("my_script", "#!/bin/bash"), "Bourne Again Shell (bash)" ); assert_eq!( - test.synax_for_file_with_content("build", "#!/bin/bash"), + test.syntax_for_file_with_content("build", "#!/bin/bash"), "Bourne Again Shell (bash)" ); assert_eq!( - test.synax_for_file_with_content("my_script", " Result { - let files = self.files(); + let files = self.files()?; let style_components = self.style_components()?; let paging_mode = match self.matches.value_of("paging") { @@ -83,7 +84,7 @@ impl App { if self.matches.occurrences_of("plain") > 1 { // If we have -pp as an option when in auto mode, the pager should be disabled. PagingMode::Never - } else if files.contains(&InputFile::StdIn) { + } else if files.contains(&InputFile::StdIn(None)) { // If we are reading from stdin, only enable paging if we write to an // interactive terminal and if we do not *read* from an interactive // terminal. @@ -132,13 +133,6 @@ impl App { } }); - match self.matches.values_of("file-name") { - Some(ref 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(|| { @@ -225,28 +219,60 @@ impl App { .map(LineRanges::from) .map(|lr| HighlightedLineRanges(lr)) .unwrap_or_default(), - filenames: self - .matches - .values_of("file-name") - .map(|values| values.collect()), }) } - fn files(&self) -> Vec { - self.matches + fn files(&self) -> Result> { + // verify equal length of file-names and input FILEs + match self.matches.values_of("file-name") { + Some(ref filenames) + if self.matches.values_of_os("FILE").is_some() + && filenames.len() != self.matches.values_of_os("FILE").unwrap().len() => + { + return Err("Must be one file name per input type.".into()); + } + _ => {} + } + let filenames: Option> = self + .matches + .values_of("file-name") + .map(|values| values.collect()); + + let mut filenames_or_none: Box> = match filenames { + Some(ref filenames) => { + Box::new(filenames.into_iter().map(|name| Some(OsStr::new(*name)))) + } + None => Box::new(std::iter::repeat(None)), + }; + let files: Option> = self + .matches .values_of_os("FILE") - .map(|values| { - values - .map(|filename| { - if filename == "-" { - InputFile::StdIn - } else { - InputFile::Ordinary(filename) - } - }) - .collect() - }) - .unwrap_or_else(|| vec![InputFile::StdIn]) + .map(|values| values.map(|fname| fname.to_str()).collect()) + .unwrap_or(None); + + if files.is_none() { + return Ok(vec![InputFile::StdIn(filenames_or_none.nth(0).unwrap())]); + } + let files_or_none: Box> = match files { + Some(ref files) => Box::new(files.into_iter().map(|name| Some(OsStr::new(*name)))), + None => Box::new(std::iter::repeat(None)), + }; + + let mut file_input = Vec::new(); + for (input, name) in files_or_none.zip(filenames_or_none) { + if let Some(input) = input { + if input.to_str().unwrap() == "-" { + file_input.push(InputFile::StdIn(name)); + } else { + let mut ofile = OrdinaryFile::from_path(input); + if let Some(path) = name { + ofile.set_provided_path(path); + } + file_input.push(InputFile::Ordinary(ofile)) + } + } + } + return Ok(file_input); } fn style_components(&self) -> Result { diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index a1936a1178..178c5c8ca0 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -26,7 +26,7 @@ use bat::Controller; use directories::PROJECT_DIRS; use bat::{ - config::{Config, InputFile, StyleComponent, StyleComponents}, + config::{Config, InputFile, OrdinaryFile, StyleComponent, StyleComponents}, errors::*, HighlightingAssets, }; @@ -167,7 +167,9 @@ fn run() -> Result { Ok(true) } else { let mut config = app.config()?; - config.files = vec![InputFile::Ordinary(OsStr::new("cache"))]; + config.files = vec![InputFile::Ordinary(OrdinaryFile::from_path(OsStr::new( + "cache", + )))]; run_controller(&config) } diff --git a/src/config.rs b/src/config.rs index b0544acd3e..a2bff63d6a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ pub use crate::inputfile::InputFile; +pub use crate::inputfile::OrdinaryFile; pub use crate::line_range::{HighlightedLineRanges, LineRange, LineRanges}; pub use crate::style::{StyleComponent, StyleComponents}; pub use crate::syntax_mapping::{MappingTarget, SyntaxMapping}; @@ -73,9 +74,6 @@ 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 fd874759d2..102305626c 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -35,8 +35,8 @@ impl<'b> Controller<'b> { let mut paging_mode = self.config.paging_mode; if self.config.paging_mode != PagingMode::Never { let call_pager = self.config.files.iter().any(|file| { - if let InputFile::Ordinary(path) = file { - return Path::new(path).exists(); + if let InputFile::Ordinary(ofile) = file { + return Path::new(ofile.provided_path()).exists(); } else { return true; } @@ -56,15 +56,8 @@ impl<'b> Controller<'b> { let writer = output_type.handle()?; let mut no_errors: bool = true; - let stdin = io::stdin(); - - 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) { + for input_file in self.config.files.iter() { + match input_file.get_reader(io::stdin().lock()) { Err(error) => { handle_error(&error); no_errors = false; @@ -72,7 +65,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, file_name) + self.print_file(reader, &mut printer, writer, *input_file) } else { let mut printer = InteractivePrinter::new( &self.config, @@ -80,7 +73,7 @@ impl<'b> Controller<'b> { *input_file, &mut reader, ); - self.print_file(reader, &mut printer, writer, *input_file, file_name) + self.print_file(reader, &mut printer, writer, *input_file) }; if let Err(error) = result { @@ -100,10 +93,9 @@ 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, file_name)?; + printer.print_header(writer, input_file)?; } if !reader.first_line.is_empty() { diff --git a/src/inputfile.rs b/src/inputfile.rs index 4a2c6f265c..e7ed739d8a 100644 --- a/src/inputfile.rs +++ b/src/inputfile.rs @@ -52,23 +52,48 @@ impl<'a> InputFileReader<'a> { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct OrdinaryFile<'a> { + path: &'a OsStr, + user_provided_path: Option<&'a OsStr>, +} + +impl<'a> OrdinaryFile<'a> { + pub fn from_path(path: &'a OsStr) -> OrdinaryFile<'a> { + OrdinaryFile { + path, + user_provided_path: None, + } + } + + pub fn set_provided_path(&mut self, user_provided_path: &'a OsStr) { + self.user_provided_path = Some(user_provided_path); + } + + pub(crate) fn provided_path(&self) -> &'a OsStr { + self.user_provided_path.unwrap_or_else(|| self.path) + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum InputFile<'a> { - StdIn, - Ordinary(&'a OsStr), + StdIn(Option<&'a OsStr>), + Ordinary(OrdinaryFile<'a>), ThemePreviewFile, } impl<'a> InputFile<'a> { - pub(crate) fn get_reader(&self, stdin: &'a io::Stdin) -> Result { + pub(crate) fn get_reader(&self, stdin: R) -> Result { match self { - InputFile::StdIn => Ok(InputFileReader::new(stdin.lock())), - InputFile::Ordinary(filename) => { - let file = File::open(filename) - .map_err(|e| format!("'{}': {}", filename.to_string_lossy(), e))?; + InputFile::StdIn(_) => Ok(InputFileReader::new(stdin)), + InputFile::Ordinary(ofile) => { + let file = File::open(ofile.path) + .map_err(|e| format!("'{}': {}", ofile.path.to_string_lossy(), e))?; if file.metadata()?.is_dir() { - return Err(format!("'{}' is a directory.", filename.to_string_lossy()).into()); + return Err( + format!("'{}' is a directory.", ofile.path.to_string_lossy()).into(), + ); } Ok(InputFileReader::new(BufReader::new(file))) diff --git a/src/printer.rs b/src/printer.rs index 3fa20ac71a..e63d5877de 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -34,12 +34,7 @@ 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, - file_name: Option<&str>, - ) -> Result<()>; + fn print_header(&mut self, handle: &mut dyn Write, file: InputFile) -> Result<()>; fn print_footer(&mut self, handle: &mut dyn Write) -> Result<()>; fn print_snip(&mut self, handle: &mut dyn Write) -> Result<()>; @@ -62,12 +57,7 @@ impl SimplePrinter { } impl Printer for SimplePrinter { - fn print_header( - &mut self, - _handle: &mut dyn Write, - _file: InputFile, - _file_name: Option<&str>, - ) -> Result<()> { + fn print_header(&mut self, _handle: &mut dyn Write, _file: InputFile) -> Result<()> { Ok(()) } @@ -170,8 +160,8 @@ impl<'a> InteractivePrinter<'a> { #[cfg(feature = "git")] { if config.style_components.changes() { - if let InputFile::Ordinary(filename) = file { - line_changes = get_git_diff(filename); + if let InputFile::Ordinary(ofile) = file { + line_changes = get_git_diff(ofile.provided_path()); } } } @@ -240,20 +230,19 @@ impl<'a> InteractivePrinter<'a> { } impl<'a> Printer for InteractivePrinter<'a> { - fn print_header( - &mut self, - handle: &mut dyn Write, - file: InputFile, - file_name: Option<&str>, - ) -> Result<()> { + fn print_header(&mut self, handle: &mut dyn Write, file: InputFile) -> 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 '{}'", - file_name.unwrap_or(&filename.to_string_lossy()) + InputFile::Ordinary(ofile) => { + format!("file '{}'", &ofile.provided_path().to_string_lossy()) + } + InputFile::StdIn(Some(name)) => format!( + "STDIN (with name '{}')", + name.to_string_lossy().into_owned() ), - _ => file_name.unwrap_or("STDIN").to_owned(), + InputFile::StdIn(None) => "STDIN".to_owned(), + InputFile::ThemePreviewFile => "".to_owned(), }; writeln!( @@ -288,11 +277,15 @@ impl<'a> Printer for InteractivePrinter<'a> { } let (prefix, name) = match file { - InputFile::Ordinary(filename) => ( + InputFile::Ordinary(ofile) => ( "File: ", - Cow::from(file_name.unwrap_or(&filename.to_string_lossy()).to_owned()), + Cow::from(ofile.provided_path().to_string_lossy().to_owned()), ), - _ => ("File: ", Cow::from(file_name.unwrap_or("STDIN").to_owned())), + InputFile::StdIn(Some(name)) => { + ("File: ", Cow::from(name.to_string_lossy().to_owned())) + } + InputFile::StdIn(None) => ("File: ", Cow::from("STDIN".to_owned())), + InputFile::ThemePreviewFile => ("", Cow::from("")), }; let mode = match self.content_type {