2018-08-22 22:29:12 +02:00
|
|
|
use std::collections::HashSet;
|
|
|
|
use std::env;
|
2020-04-05 02:49:55 +02:00
|
|
|
use std::ffi::OsStr;
|
2018-09-10 02:28:04 +02:00
|
|
|
use std::str::FromStr;
|
2018-08-22 22:29:12 +02:00
|
|
|
|
2018-05-10 23:39:13 +02:00
|
|
|
use atty::{self, Stream};
|
2018-08-22 22:29:12 +02:00
|
|
|
|
2019-10-06 03:44:14 +02:00
|
|
|
use crate::{
|
|
|
|
clap_app,
|
|
|
|
config::{get_args_from_config_file, get_args_from_env_var},
|
|
|
|
};
|
2019-05-15 08:07:08 +02:00
|
|
|
use clap::ArgMatches;
|
2018-08-22 22:29:12 +02:00
|
|
|
|
2018-05-10 23:39:13 +02:00
|
|
|
use console::Term;
|
|
|
|
|
2019-10-06 04:10:03 +02:00
|
|
|
use bat::{
|
2020-04-22 18:30:06 +02:00
|
|
|
assets::HighlightingAssets,
|
|
|
|
config::{Config, PagingMode},
|
2020-04-22 21:45:47 +02:00
|
|
|
error::*,
|
2020-04-22 18:30:06 +02:00
|
|
|
input::Input,
|
2020-04-22 19:15:33 +02:00
|
|
|
line_range::{HighlightedLineRanges, LineRange, LineRanges},
|
2020-04-22 21:16:30 +02:00
|
|
|
style::{StyleComponent, StyleComponents},
|
|
|
|
MappingTarget, SyntaxMapping, WrappingMode,
|
2019-10-06 04:10:03 +02:00
|
|
|
};
|
2018-08-22 22:29:12 +02:00
|
|
|
|
|
|
|
fn is_truecolor_terminal() -> bool {
|
|
|
|
env::var("COLORTERM")
|
|
|
|
.map(|colorterm| colorterm == "truecolor" || colorterm == "24bit")
|
|
|
|
.unwrap_or(false)
|
|
|
|
}
|
|
|
|
|
2018-05-10 23:39:13 +02:00
|
|
|
pub struct App {
|
|
|
|
pub matches: ArgMatches<'static>,
|
|
|
|
interactive_output: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl App {
|
2018-10-11 22:50:37 +02:00
|
|
|
pub fn new() -> Result<Self> {
|
2018-05-13 09:37:54 +02:00
|
|
|
#[cfg(windows)]
|
2018-09-03 20:47:15 +02:00
|
|
|
let _ = ansi_term::enable_ansi_support();
|
|
|
|
|
|
|
|
let interactive_output = atty::is(Stream::Stdout);
|
2018-05-13 09:37:54 +02:00
|
|
|
|
2018-10-11 22:50:37 +02:00
|
|
|
Ok(App {
|
|
|
|
matches: Self::matches(interactive_output)?,
|
2018-05-10 23:39:13 +02:00
|
|
|
interactive_output,
|
2018-10-11 22:50:37 +02:00
|
|
|
})
|
2018-05-10 23:39:13 +02:00
|
|
|
}
|
|
|
|
|
2018-10-11 22:50:37 +02:00
|
|
|
fn matches(interactive_output: bool) -> Result<ArgMatches<'static>> {
|
2018-10-11 21:40:14 +02:00
|
|
|
let args = if wild::args_os().nth(1) == Some("cache".into())
|
2018-10-11 23:02:57 +02:00
|
|
|
|| wild::args_os().any(|arg| arg == "--no-config")
|
2018-10-11 21:40:14 +02:00
|
|
|
{
|
2018-10-07 23:22:42 +02:00
|
|
|
// Skip the arguments in bats config file
|
|
|
|
|
|
|
|
wild::args_os().collect::<Vec<_>>()
|
|
|
|
} else {
|
|
|
|
let mut cli_args = wild::args_os();
|
|
|
|
|
|
|
|
// Read arguments from bats config file
|
2018-10-11 23:01:19 +02:00
|
|
|
let mut args = get_args_from_env_var()
|
2019-03-08 11:46:49 +01:00
|
|
|
.unwrap_or_else(get_args_from_config_file)
|
2018-10-11 23:01:19 +02:00
|
|
|
.chain_err(|| "Could not parse configuration file")?;
|
2018-10-07 23:22:42 +02:00
|
|
|
|
|
|
|
// Put the zero-th CLI argument (program name) first
|
|
|
|
args.insert(0, cli_args.next().unwrap());
|
|
|
|
|
|
|
|
// .. and the rest at the end
|
|
|
|
cli_args.for_each(|a| args.push(a));
|
|
|
|
|
|
|
|
args
|
|
|
|
};
|
|
|
|
|
2018-10-11 22:50:37 +02:00
|
|
|
Ok(clap_app::build_app(interactive_output).get_matches_from(args))
|
2018-05-10 23:39:13 +02:00
|
|
|
}
|
|
|
|
|
2020-04-21 21:19:06 +02:00
|
|
|
pub fn config(&self, inputs: &[Input]) -> Result<Config> {
|
2020-03-21 20:54:16 +01:00
|
|
|
let style_components = self.style_components()?;
|
2018-05-10 23:39:13 +02:00
|
|
|
|
2018-09-11 05:12:13 +02:00
|
|
|
let paging_mode = match self.matches.value_of("paging") {
|
|
|
|
Some("always") => PagingMode::Always,
|
|
|
|
Some("never") => PagingMode::Never,
|
2018-09-26 19:16:03 +02:00
|
|
|
Some("auto") | _ => {
|
2019-05-09 03:56:02 +02:00
|
|
|
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
|
2020-04-22 16:27:34 +02:00
|
|
|
} else if inputs.iter().any(Input::is_stdin) {
|
2018-09-26 19:16:03 +02:00
|
|
|
// 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.
|
|
|
|
if self.interactive_output && !atty::is(Stream::Stdin) {
|
|
|
|
PagingMode::QuitIfOneScreen
|
|
|
|
} else {
|
|
|
|
PagingMode::Never
|
|
|
|
}
|
2019-03-08 11:46:49 +01:00
|
|
|
} else if self.interactive_output {
|
|
|
|
PagingMode::QuitIfOneScreen
|
2018-09-11 05:12:13 +02:00
|
|
|
} else {
|
2019-03-08 11:46:49 +01:00
|
|
|
PagingMode::Never
|
2018-09-11 05:12:13 +02:00
|
|
|
}
|
2018-09-26 19:16:03 +02:00
|
|
|
}
|
2018-09-11 05:12:13 +02:00
|
|
|
};
|
|
|
|
|
2020-03-22 09:55:13 +01:00
|
|
|
let mut syntax_mapping = SyntaxMapping::builtin();
|
2018-10-17 22:30:09 +02:00
|
|
|
|
|
|
|
if let Some(values) = self.matches.values_of("map-syntax") {
|
|
|
|
for from_to in values {
|
2019-03-08 11:46:49 +01:00
|
|
|
let parts: Vec<_> = from_to.split(':').collect();
|
2018-10-17 22:30:09 +02:00
|
|
|
|
|
|
|
if parts.len() != 2 {
|
2020-03-22 10:37:18 +01:00
|
|
|
return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is '<glob-pattern>:<syntax-name>'. For example: '*.cpp:C++'.".into());
|
2018-10-17 22:30:09 +02:00
|
|
|
}
|
|
|
|
|
2020-03-22 09:55:13 +01:00
|
|
|
syntax_mapping.insert(parts[0], MappingTarget::MapTo(parts[1]))?;
|
2018-10-17 22:30:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-21 10:50:59 +01:00
|
|
|
let maybe_term_width = self.matches.value_of("terminal-width").and_then(|w| {
|
|
|
|
if w.starts_with('+') || w.starts_with('-') {
|
|
|
|
// Treat argument as a delta to the current terminal width
|
|
|
|
w.parse().ok().map(|delta: i16| {
|
|
|
|
let old_width: u16 = Term::stdout().size().1;
|
|
|
|
let new_width: i32 = i32::from(old_width) + i32::from(delta);
|
|
|
|
|
|
|
|
if new_width <= 0 {
|
|
|
|
old_width as usize
|
|
|
|
} else {
|
|
|
|
new_width as usize
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
w.parse().ok()
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-05-10 23:39:13 +02:00
|
|
|
Ok(Config {
|
|
|
|
true_color: is_truecolor_terminal(),
|
2018-11-01 13:02:29 +01:00
|
|
|
language: self.matches.value_of("language").or_else(|| {
|
|
|
|
if self.matches.is_present("show-all") {
|
|
|
|
Some("show-nonprintable")
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
show_nonprintable: self.matches.is_present("show-all"),
|
2020-04-21 20:06:09 +02:00
|
|
|
wrapping_mode: if self.interactive_output || maybe_term_width.is_some() {
|
2018-05-12 22:44:10 +02:00
|
|
|
match self.matches.value_of("wrap") {
|
2020-04-21 20:06:09 +02:00
|
|
|
Some("character") => WrappingMode::Character,
|
|
|
|
Some("never") => WrappingMode::NoWrapping,
|
2018-09-26 19:16:03 +02:00
|
|
|
Some("auto") | _ => {
|
2020-03-21 20:54:16 +01:00
|
|
|
if style_components.plain() {
|
2020-04-21 20:06:09 +02:00
|
|
|
WrappingMode::NoWrapping
|
2018-09-26 19:16:03 +02:00
|
|
|
} else {
|
2020-04-21 20:06:09 +02:00
|
|
|
WrappingMode::Character
|
2018-09-26 19:16:03 +02:00
|
|
|
}
|
|
|
|
}
|
2018-05-12 22:44:10 +02:00
|
|
|
}
|
2019-12-21 10:50:59 +01:00
|
|
|
} else {
|
|
|
|
// We don't have the tty width when piping to another program.
|
|
|
|
// There's no point in wrapping when this is the case.
|
2020-04-21 20:06:09 +02:00
|
|
|
WrappingMode::NoWrapping
|
2018-05-12 22:44:10 +02:00
|
|
|
},
|
2018-09-03 20:47:15 +02:00
|
|
|
colored_output: match self.matches.value_of("color") {
|
|
|
|
Some("always") => true,
|
|
|
|
Some("never") => false,
|
|
|
|
Some("auto") | _ => self.interactive_output,
|
|
|
|
},
|
2018-09-11 05:12:13 +02:00
|
|
|
paging_mode,
|
2019-12-21 10:50:59 +01:00
|
|
|
term_width: maybe_term_width.unwrap_or(Term::stdout().size().1 as usize),
|
2018-08-23 23:13:24 +02:00
|
|
|
loop_through: !(self.interactive_output
|
|
|
|
|| self.matches.value_of("color") == Some("always")
|
|
|
|
|| self.matches.value_of("decorations") == Some("always")),
|
2018-09-11 03:11:59 +02:00
|
|
|
tab_width: self
|
|
|
|
.matches
|
|
|
|
.value_of("tabs")
|
2018-09-12 19:08:58 +02:00
|
|
|
.map(String::from)
|
|
|
|
.or_else(|| env::var("BAT_TABS").ok())
|
|
|
|
.and_then(|t| t.parse().ok())
|
2018-09-11 05:12:13 +02:00
|
|
|
.unwrap_or(
|
2020-03-21 20:54:16 +01:00
|
|
|
if style_components.plain() && paging_mode == PagingMode::Never {
|
2018-09-11 05:12:13 +02:00
|
|
|
0
|
|
|
|
} else {
|
2018-11-01 13:34:14 +01:00
|
|
|
4
|
2018-09-11 05:12:13 +02:00
|
|
|
},
|
|
|
|
),
|
2018-09-07 20:13:03 +02:00
|
|
|
theme: self
|
|
|
|
.matches
|
2018-07-21 08:26:24 +02:00
|
|
|
.value_of("theme")
|
2018-07-23 21:19:47 +02:00
|
|
|
.map(String::from)
|
2018-07-23 21:38:45 +02:00
|
|
|
.or_else(|| env::var("BAT_THEME").ok())
|
2019-11-26 23:43:20 +01:00
|
|
|
.map(|s| {
|
2019-12-15 20:20:13 +01:00
|
|
|
if s == "default" {
|
2020-03-21 20:31:32 +01:00
|
|
|
String::from(HighlightingAssets::default_theme())
|
2019-11-26 23:43:20 +01:00
|
|
|
} else {
|
|
|
|
s
|
|
|
|
}
|
|
|
|
})
|
2020-03-21 20:31:32 +01:00
|
|
|
.unwrap_or_else(|| String::from(HighlightingAssets::default_theme())),
|
2020-03-21 19:40:43 +01:00
|
|
|
line_ranges: self
|
|
|
|
.matches
|
|
|
|
.values_of("line-range")
|
|
|
|
.map(|vs| vs.map(LineRange::from).collect())
|
|
|
|
.transpose()?
|
|
|
|
.map(LineRanges::from)
|
|
|
|
.unwrap_or_default(),
|
2020-03-21 20:54:16 +01:00
|
|
|
style_components,
|
2018-10-17 22:30:09 +02:00
|
|
|
syntax_mapping,
|
2018-10-21 12:52:29 +02:00
|
|
|
pager: self.matches.value_of("pager"),
|
2018-11-04 18:08:33 +01:00
|
|
|
use_italic_text: match self.matches.value_of("italic-text") {
|
2018-10-31 20:27:31 +01:00
|
|
|
Some("always") => true,
|
2018-11-02 12:06:49 +01:00
|
|
|
_ => false,
|
2018-10-31 20:27:31 +01:00
|
|
|
},
|
2020-03-21 19:40:43 +01:00
|
|
|
highlighted_lines: self
|
|
|
|
.matches
|
|
|
|
.values_of("highlight-line")
|
|
|
|
.map(|ws| ws.map(LineRange::from).collect())
|
|
|
|
.transpose()?
|
|
|
|
.map(LineRanges::from)
|
|
|
|
.map(|lr| HighlightedLineRanges(lr))
|
|
|
|
.unwrap_or_default(),
|
2018-05-10 23:39:13 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-04-21 21:19:06 +02:00
|
|
|
pub fn inputs(&self) -> Result<Vec<Input>> {
|
2020-04-05 02:49:55 +02:00
|
|
|
// verify equal length of file-names and input FILEs
|
|
|
|
match self.matches.values_of("file-name") {
|
2020-04-18 17:57:57 +02:00
|
|
|
Some(ref filenames)
|
2020-04-05 02:49:55 +02:00
|
|
|
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)),
|
|
|
|
};
|
2020-04-21 14:09:05 +02:00
|
|
|
let files: Option<Vec<&OsStr>> = self.matches.values_of_os("FILE").map(|vs| vs.collect());
|
2020-04-05 02:49:55 +02:00
|
|
|
|
|
|
|
if files.is_none() {
|
2020-04-22 22:41:25 +02:00
|
|
|
let input = Input::stdin().with_name(filenames_or_none.nth(0).unwrap_or(None));
|
2020-04-22 16:27:34 +02:00
|
|
|
return Ok(vec![input]);
|
2020-04-05 02:49:55 +02:00
|
|
|
}
|
|
|
|
let files_or_none: Box<dyn Iterator<Item = _>> = match files {
|
2020-04-21 14:09:05 +02:00
|
|
|
Some(ref files) => Box::new(files.into_iter().map(|name| Some(*name))),
|
2020-04-05 02:49:55 +02:00
|
|
|
None => Box::new(std::iter::repeat(None)),
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut file_input = Vec::new();
|
2020-04-22 16:27:34 +02:00
|
|
|
for (filepath, provided_name) in files_or_none.zip(filenames_or_none) {
|
|
|
|
if let Some(filepath) = filepath {
|
|
|
|
if filepath.to_str().unwrap_or_default() == "-" {
|
2020-04-22 22:41:25 +02:00
|
|
|
file_input.push(Input::stdin().with_name(provided_name));
|
2020-04-11 19:23:32 +02:00
|
|
|
} else {
|
2020-04-22 22:41:25 +02:00
|
|
|
file_input.push(Input::ordinary_file(filepath).with_name(provided_name));
|
2020-04-05 02:49:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Ok(file_input);
|
2018-05-10 23:39:13 +02:00
|
|
|
}
|
|
|
|
|
2020-03-21 20:54:16 +01:00
|
|
|
fn style_components(&self) -> Result<StyleComponents> {
|
2018-05-10 23:39:13 +02:00
|
|
|
let matches = &self.matches;
|
2020-03-21 20:54:16 +01:00
|
|
|
Ok(StyleComponents(
|
2018-08-23 23:13:24 +02:00
|
|
|
if matches.value_of("decorations") == Some("never") {
|
|
|
|
HashSet::new()
|
|
|
|
} else if matches.is_present("number") {
|
2020-04-22 20:36:20 +02:00
|
|
|
[StyleComponent::LineNumbers].iter().cloned().collect()
|
2018-08-26 03:44:02 +02:00
|
|
|
} else if matches.is_present("plain") {
|
2020-03-21 20:54:16 +01:00
|
|
|
[StyleComponent::Plain].iter().cloned().collect()
|
2018-09-12 21:35:23 +02:00
|
|
|
} else {
|
2020-03-21 20:54:16 +01:00
|
|
|
let env_style_components: Option<Vec<StyleComponent>> = env::var("BAT_STYLE")
|
2019-10-20 22:01:20 +02:00
|
|
|
.ok()
|
|
|
|
.map(|style_str| {
|
2018-09-12 21:35:23 +02:00
|
|
|
style_str
|
2019-03-08 11:46:49 +01:00
|
|
|
.split(',')
|
2020-03-21 20:54:16 +01:00
|
|
|
.map(|x| StyleComponent::from_str(&x))
|
|
|
|
.collect::<Result<Vec<StyleComponent>>>()
|
2019-10-20 22:01:20 +02:00
|
|
|
})
|
|
|
|
.transpose()?;
|
2018-09-12 21:35:23 +02:00
|
|
|
|
2018-10-21 17:50:53 +02:00
|
|
|
matches
|
|
|
|
.value_of("style")
|
2018-10-21 15:27:07 +02:00
|
|
|
.map(|styles| {
|
|
|
|
styles
|
2019-03-08 11:46:49 +01:00
|
|
|
.split(',')
|
2020-03-21 20:54:16 +01:00
|
|
|
.map(|style| style.parse::<StyleComponent>())
|
2018-10-21 15:27:07 +02:00
|
|
|
.filter_map(|style| style.ok())
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
})
|
2018-09-12 21:35:23 +02:00
|
|
|
.or(env_style_components)
|
2020-03-21 20:54:16 +01:00
|
|
|
.unwrap_or_else(|| vec![StyleComponent::Full])
|
2018-08-23 23:13:24 +02:00
|
|
|
.into_iter()
|
|
|
|
.map(|style| style.components(self.interactive_output))
|
|
|
|
.fold(HashSet::new(), |mut acc, components| {
|
|
|
|
acc.extend(components.iter().cloned());
|
|
|
|
acc
|
|
|
|
})
|
|
|
|
},
|
|
|
|
))
|
2018-05-10 23:39:13 +02:00
|
|
|
}
|
|
|
|
}
|