Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use --file-name to detect syntax highlighting #892

Merged
merged 12 commits into from
Apr 21, 2020
7 changes: 5 additions & 2 deletions examples/cat.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions examples/simple.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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()
Expand Down
111 changes: 86 additions & 25 deletions src/assets.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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)]
Expand All @@ -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> {
Expand All @@ -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()
}
}

Expand All @@ -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();
Expand All @@ -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", "<?php"),
test.syntax_for_file_with_content("my_script", "<?php"),
"PHP"
);
}
Expand All @@ -333,4 +381,17 @@ mod tests {
.ok();
assert_eq!(test.syntax_for_file("README.MD"), "Markdown");
}

#[test]
fn syntax_detection_stdin_filename() {
let test = SyntaxDetectionTest::new();

// from file extension
assert_eq!(test.syntax_for_stdin_with_content("test.cpp", b"a"), "C++");
// from first line (fallback)
assert_eq!(
test.syntax_for_stdin_with_content("my_script", b"#!/bin/bash"),
"Bourne Again Shell (bash)"
);
}
}
84 changes: 55 additions & 29 deletions src/bin/bat/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::HashSet;
use std::env;
use std::ffi::OsStr;
use std::str::FromStr;

use atty::{self, Stream};
Expand All @@ -14,8 +15,8 @@ use console::Term;

use bat::{
config::{
Config, HighlightedLineRanges, InputFile, LineRange, LineRanges, MappingTarget, OutputWrap,
PagingMode, StyleComponent, StyleComponents, SyntaxMapping,
Config, HighlightedLineRanges, InputFile, LineRange, LineRanges, MappingTarget,
OrdinaryFile, OutputWrap, PagingMode, StyleComponent, StyleComponents, SyntaxMapping,
},
errors::*,
HighlightingAssets,
Expand Down Expand Up @@ -73,7 +74,7 @@ impl App {
}

pub fn config(&self) -> Result<Config> {
let files = self.files();
let files = self.files()?;
let style_components = self.style_components()?;

let paging_mode = match self.matches.value_of("paging") {
Expand All @@ -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.
Expand Down Expand Up @@ -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(|| {
Expand Down Expand Up @@ -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<InputFile> {
self.matches
fn files(&self) -> Result<Vec<InputFile>> {
// 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<Vec<&str>> = self
.matches
.values_of("file-name")
.map(|values| values.collect());

let mut filenames_or_none: Box<dyn Iterator<Item = _>> = 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<Vec<&str>> = 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<dyn Iterator<Item = _>> = 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<StyleComponents> {
Expand Down
6 changes: 4 additions & 2 deletions src/bin/bat/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -167,7 +167,9 @@ fn run() -> Result<bool> {
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)
}
Expand Down
Loading