This commit is contained in:
Ethan P 2024-04-09 12:50:01 -07:00 committed by GitHub
commit c84f5eb0bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 354 additions and 59 deletions

View File

@ -6,6 +6,7 @@
- `bat --squeeze-blank`/`bat -s` will now squeeze consecutive empty lines, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) - `bat --squeeze-blank`/`bat -s` will now squeeze consecutive empty lines, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) - `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815) - `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- Add or remove individual style components without replacing all styles #2929 (@eth-p)
## Bugfixes ## Bugfixes

View File

@ -507,6 +507,16 @@ and line numbers but no grid and no file header. Set the `BAT_STYLE` environment
variable to make these changes permanent or use `bat`s variable to make these changes permanent or use `bat`s
[configuration file](https://github.com/sharkdp/bat#configuration-file). [configuration file](https://github.com/sharkdp/bat#configuration-file).
>[!tip]
> If you specify a default style in `bat`'s config file, you can change which components
> are displayed during a single run of `bat` using the `--style` command-line argument.
> By prefixing a component with `+` or `-`, it can be added or removed from the current style.
>
> For example, if your config contains `--style=full,-snip`, you can run bat with
> `--style=-grid,+snip` to remove the grid and add back the `snip` component.
> Or, if you want to override the styles completely, you use `--style=numbers` to
> only show the line numbers.
### Adding new syntaxes / language definitions ### Adding new syntaxes / language definitions
Should you find that a particular syntax is not available within `bat`, you can follow these Should you find that a particular syntax is not available within `bat`, you can follow these

View File

@ -125,9 +125,16 @@ Options:
--style <components> --style <components>
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
to display in addition to the file contents. The argument is a comma-separated list of to display in addition to the file contents. The argument is a comma-separated list of
components to display (e.g. 'numbers,changes,grid') or a pre-defined style ('full'). To components to display (e.g. 'numbers,changes,grid') or a pre-defined style ('full').
set a default style, add the '--style=".."' option to the configuration file or export the
BAT_STYLE environment variable (e.g.: export BAT_STYLE=".."). To set a default style, add the '--style=".."' option to the configuration file or export
the BAT_STYLE environment variable (e.g.: export BAT_STYLE="..").
When styles are specified in multiple places, the "nearest" set of styles take precedence.
The command-line arguments are the highest priority, followed by the BAT_STYLE environment
variable, and then the configuration file. If any set of styles consists entirely of
components prefixed with "+" or "-", it will modify the previous set of styles instead of
replacing them.
By default, the following components are enabled: By default, the following components are enabled:
changes, grid, header-filename, numbers, snip changes, grid, header-filename, numbers, snip

View File

@ -2,11 +2,13 @@ use std::collections::HashSet;
use std::env; use std::env;
use std::io::IsTerminal; use std::io::IsTerminal;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::{ use crate::{
clap_app, clap_app,
config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars}, config::{get_args_from_config_file, get_args_from_env_opts_var, get_args_from_env_vars},
}; };
use bat::style::StyleComponentList;
use clap::ArgMatches; use clap::ArgMatches;
use console::Term; use console::Term;
@ -85,7 +87,6 @@ impl App {
// .. and the rest at the end // .. and the rest at the end
cli_args.for_each(|a| args.push(a)); cli_args.for_each(|a| args.push(a));
args args
}; };
@ -353,34 +354,57 @@ impl App {
Ok(file_input) Ok(file_input)
} }
fn forced_style_components(&self) -> Option<StyleComponents> {
// No components if `--decorations=never``.
if self
.matches
.get_one::<String>("decorations")
.map(|s| s.as_str())
== Some("never")
{
return Some(StyleComponents(HashSet::new()));
}
// Only line numbers if `--number`.
if self.matches.get_flag("number") {
return Some(StyleComponents(HashSet::from([
StyleComponent::LineNumbers,
])));
}
// Plain if `--plain` is specified at least once.
if self.matches.get_count("plain") > 0 {
return Some(StyleComponents(HashSet::from([StyleComponent::Plain])));
}
// Default behavior.
None
}
fn style_components(&self) -> Result<StyleComponents> { fn style_components(&self) -> Result<StyleComponents> {
let matches = &self.matches; let matches = &self.matches;
let mut styled_components = StyleComponents( let mut styled_components = match self.forced_style_components() {
if matches.get_one::<String>("decorations").map(|s| s.as_str()) == Some("never") { Some(forced_components) => forced_components,
HashSet::new()
} else if matches.get_flag("number") { // Parse the `--style` arguments and merge them.
[StyleComponent::LineNumbers].iter().cloned().collect() None if matches.contains_id("style") => {
} else if 0 < matches.get_count("plain") { let lists = matches
[StyleComponent::Plain].iter().cloned().collect() .get_many::<String>("style")
} else { .expect("styles present")
matches .map(|v| StyleComponentList::from_str(v))
.get_one::<String>("style") .collect::<Result<Vec<StyleComponentList>>>()?;
.map(|styles| {
styles StyleComponentList::to_components(lists, self.interactive_output)
.split(',') }
.map(|style| style.parse::<StyleComponent>())
.filter_map(|style| style.ok()) // Use the default.
.collect::<Vec<_>>() None => StyleComponents(HashSet::from_iter(
}) StyleComponent::Default
.unwrap_or_else(|| vec![StyleComponent::Default]) .components(self.interactive_output)
.into_iter() .into_iter()
.map(|style| style.components(self.interactive_output)) .cloned(),
.fold(HashSet::new(), |mut acc, components| { )),
acc.extend(components.iter().cloned()); };
acc
})
},
);
// If `grid` is set, remove `rule` as it is a subset of `grid`, and print a warning. // If `grid` is set, remove `rule` as it is a subset of `grid`, and print a warning.
if styled_components.grid() && styled_components.0.remove(&StyleComponent::Rule) { if styled_components.grid() && styled_components.0.remove(&StyleComponent::Rule) {

View File

@ -1,9 +1,11 @@
use bat::style::StyleComponentList;
use clap::{ use clap::{
crate_name, crate_version, value_parser, Arg, ArgAction, ArgGroup, ColorChoice, Command, crate_name, crate_version, value_parser, Arg, ArgAction, ArgGroup, ColorChoice, Command,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::env; use std::env;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr;
static VERSION: Lazy<String> = Lazy::new(|| { static VERSION: Lazy<String> = Lazy::new(|| {
#[cfg(feature = "bugreport")] #[cfg(feature = "bugreport")]
@ -405,34 +407,13 @@ pub fn build_app(interactive_output: bool) -> Command {
.arg( .arg(
Arg::new("style") Arg::new("style")
.long("style") .long("style")
.action(ArgAction::Append)
.value_name("components") .value_name("components")
.overrides_with("style")
.overrides_with("plain")
.overrides_with("number")
// Cannot use claps built in validation because we have to turn off clap's delimiters // Cannot use claps built in validation because we have to turn off clap's delimiters
.value_parser(|val: &str| { .value_parser(|val: &str| {
let mut invalid_vals = val.split(',').filter(|style| { match StyleComponentList::from_str(val) {
!&[ Err(err) => Err(err),
"auto", Ok(_) => Ok(val.to_owned()),
"full",
"default",
"plain",
"header",
"header-filename",
"header-filesize",
"grid",
"rule",
"numbers",
"snip",
#[cfg(feature = "git")]
"changes",
].contains(style)
});
if let Some(invalid) = invalid_vals.next() {
Err(format!("Unknown style, '{invalid}'"))
} else {
Ok(val.to_owned())
} }
}) })
.help( .help(
@ -444,9 +425,16 @@ pub fn build_app(interactive_output: bool) -> Command {
borders, Git modifications, ..) to display in addition to the \ borders, Git modifications, ..) to display in addition to the \
file contents. The argument is a comma-separated list of \ file contents. The argument is a comma-separated list of \
components to display (e.g. 'numbers,changes,grid') or a \ components to display (e.g. 'numbers,changes,grid') or a \
pre-defined style ('full'). To set a default style, add the \ pre-defined style ('full').\n\n\
'--style=\"..\"' option to the configuration file or export the \ To set a default style, add the '--style=\"..\"' option to the \
BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\").\n\n\ configuration file or export the BAT_STYLE environment variable \
(e.g.: export BAT_STYLE=\"..\").\n\n\
When styles are specified in multiple places, the \"nearest\" set \
of styles take precedence. The command-line arguments are the highest \
priority, followed by the BAT_STYLE environment variable, and then \
the configuration file. If any set of styles consists entirely of \
components prefixed with \"+\" or \"-\", it will modify the \
previous set of styles instead of replacing them.\n\n\
By default, the following components are enabled:\n \ By default, the following components are enabled:\n \
changes, grid, header-filename, numbers, snip\n\n\ changes, grid, header-filename, numbers, snip\n\n\
Possible values:\n\n \ Possible values:\n\n \

View File

@ -146,8 +146,11 @@ pub fn get_args_from_env_vars() -> Vec<OsString> {
("--style", "BAT_STYLE"), ("--style", "BAT_STYLE"),
] ]
.iter() .iter()
.filter_map(|(flag, key)| env::var(key).ok().map(|var| [flag.to_string(), var])) .filter_map(|(flag, key)| {
.flatten() env::var(key)
.ok()
.map(|var| [flag.to_string(), var].join("="))
})
.map(|a| a.into()) .map(|a| a.into())
.collect() .collect()
} }

View File

@ -138,3 +138,197 @@ impl StyleComponents {
self.0.clear(); self.0.clear();
} }
} }
#[derive(Debug, PartialEq)]
enum ComponentAction {
Override,
Add,
Remove,
}
impl ComponentAction {
fn extract_from_str(string: &str) -> (ComponentAction, &str) {
match string.chars().next() {
Some('-') => (ComponentAction::Remove, string.strip_prefix('-').unwrap()),
Some('+') => (ComponentAction::Add, string.strip_prefix('+').unwrap()),
_ => (ComponentAction::Override, string),
}
}
}
/// A list of [StyleComponent] that can be parsed from a string.
pub struct StyleComponentList(Vec<(ComponentAction, StyleComponent)>);
impl StyleComponentList {
fn expand_into(&self, components: &mut HashSet<StyleComponent>, interactive_terminal: bool) {
for (action, component) in self.0.iter() {
let subcomponents = component.components(interactive_terminal);
use ComponentAction::*;
match action {
Override | Add => components.extend(subcomponents),
Remove => components.retain(|c| !subcomponents.contains(c)),
}
}
}
/// Returns `true` if any component in the list was not prefixed with `+` or `-`.
fn contains_override(&self) -> bool {
self.0.iter().any(|(a, _)| *a == ComponentAction::Override)
}
/// Combines multiple [StyleComponentList]s into a single [StyleComponents] set.
///
/// ## Precedence
/// The most recent list will take precedence and override all previous lists
/// unless it only contains components prefixed with `-` or `+`. When this
/// happens, the list's components will be merged into the previous list.
///
/// ## Example
/// ```text
/// [numbers,grid] + [header,changes] -> [header,changes]
/// [numbers,grid] + [+header,-grid] -> [numbers,header]
/// ```
pub fn to_components(
lists: impl IntoIterator<Item = StyleComponentList>,
interactive_terminal: bool,
) -> StyleComponents {
StyleComponents(
lists
.into_iter()
.fold(HashSet::new(), |mut components, list| {
if list.contains_override() {
components.clear();
}
list.expand_into(&mut components, interactive_terminal);
components
}),
)
}
}
impl Default for StyleComponentList {
fn default() -> Self {
StyleComponentList(vec![(ComponentAction::Override, StyleComponent::Default)])
}
}
impl FromStr for StyleComponentList {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Ok(StyleComponentList(
s.split(",")
.map(|s| ComponentAction::extract_from_str(s)) // If the component starts with "-", it's meant to be removed
.map(|(a, s)| Ok((a, StyleComponent::from_str(s)?)))
.collect::<Result<Vec<(ComponentAction, StyleComponent)>>>()?,
))
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use std::str::FromStr;
use super::ComponentAction::*;
use super::StyleComponent::*;
use super::StyleComponentList;
#[test]
pub fn style_component_list_parse() {
assert_eq!(
StyleComponentList::from_str("grid,+numbers,snip,-snip,header")
.expect("no error")
.0,
vec![
(Override, Grid),
(Add, LineNumbers),
(Override, Snip),
(Remove, Snip),
(Override, Header),
]
);
assert!(StyleComponentList::from_str("not-a-component").is_err());
assert!(StyleComponentList::from_str("grid,not-a-component").is_err());
assert!(StyleComponentList::from_str("numbers,-not-a-component").is_err());
}
#[test]
pub fn style_component_list_to_components() {
assert_eq!(
StyleComponentList::to_components(
vec![StyleComponentList::from_str("grid,numbers").expect("no error")],
false
)
.0,
HashSet::from([Grid, LineNumbers])
);
}
#[test]
pub fn style_component_list_to_components_removes_negated() {
assert_eq!(
StyleComponentList::to_components(
vec![StyleComponentList::from_str("grid,numbers,-grid").expect("no error")],
false
)
.0,
HashSet::from([LineNumbers])
);
}
#[test]
pub fn style_component_list_to_components_expands_subcomponents() {
assert_eq!(
StyleComponentList::to_components(
vec![StyleComponentList::from_str("full").expect("no error")],
false
)
.0,
HashSet::from_iter(Full.components(true).to_owned())
);
}
#[test]
pub fn style_component_list_expand_negates_subcomponents() {
assert!(!StyleComponentList::to_components(
vec![StyleComponentList::from_str("full,-numbers").expect("no error")],
true
)
.numbers());
}
#[test]
pub fn style_component_list_to_components_precedence_overrides_previous_lists() {
assert_eq!(
StyleComponentList::to_components(
vec![
StyleComponentList::from_str("grid").expect("no error"),
StyleComponentList::from_str("numbers").expect("no error"),
],
false
)
.0,
HashSet::from([LineNumbers])
);
}
#[test]
pub fn style_component_list_to_components_precedence_merges_previous_lists() {
assert_eq!(
StyleComponentList::to_components(
vec![
StyleComponentList::from_str("grid,header").expect("no error"),
StyleComponentList::from_str("-grid").expect("no error"),
StyleComponentList::from_str("+numbers").expect("no error"),
],
false
)
.0,
HashSet::from([HeaderFilename, LineNumbers])
);
}
}

View File

@ -2631,3 +2631,71 @@ fn highlighting_independant_from_map_syntax_case() {
.stdout(expected) .stdout(expected)
.stderr(""); .stderr("");
} }
// Tests that style components can be removed with `-component`.
#[test]
fn style_components_can_be_removed() {
bat()
.arg({
#[cfg(not(feature = "git"))]
{
"--style=full,-grid"
}
#[cfg(feature = "git")]
{
"--style=full,-grid,-changes"
}
})
.arg("--decorations=always")
.arg("--color=never")
.write_stdin("test")
.assert()
.success()
.stdout(" STDIN\n Size: -\n 1 test\n")
.stderr("");
}
// Tests that style components are chosen based on the rightmost `--style` argument.
#[test]
fn style_components_can_be_overidden() {
bat()
.arg("--style=full")
.arg("--style=header,numbers")
.arg("--decorations=always")
.arg("--color=never")
.write_stdin("test")
.assert()
.success()
.stdout(" STDIN\n 1 test\n")
.stderr("");
}
// Tests that style components can be merged across multiple `--style` arguments.
#[test]
fn style_components_will_merge() {
bat()
.arg("--style=header,grid")
.arg("--style=-grid,+numbers")
.arg("--decorations=always")
.arg("--color=never")
.write_stdin("test")
.assert()
.success()
.stdout(" STDIN\n 1 test\n")
.stderr("");
}
// Tests that style components can be merged with the `BAT_STYLE` environment variable.
#[test]
fn style_components_will_merge_with_env_var() {
bat()
.env("BAT_STYLE", "header,grid")
.arg("--style=-grid,+numbers")
.arg("--decorations=always")
.arg("--color=never")
.write_stdin("test")
.assert()
.success()
.stdout(" STDIN\n 1 test\n")
.stderr("");
}