Support merging --style arguments

The `overrides_with` clap builder option was removed
because it interfered with the matcher's ability to
retain all occurrences of `--style`.

The behavior it covered is expressed within the new
`forced_style_components` function.
This commit is contained in:
Ethan P. 2024-04-06 14:54:10 -07:00
parent fd1e0d5876
commit b74c125c43
No known key found for this signature in database
GPG key ID: B29B90B1B228FEBC
3 changed files with 250 additions and 51 deletions

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 bat::StripAnsiMode; use bat::StripAnsiMode;
use clap::ArgMatches; use clap::ArgMatches;
@ -86,7 +88,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
}; };
@ -364,34 +365,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")]
@ -419,34 +421,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(

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])
);
}
}