Skip to content

Commit

Permalink
feature: support requiring capture group match in template string
Browse files Browse the repository at this point in the history
  • Loading branch information
solidiquis committed Jan 20, 2025
1 parent 953994d commit a8d284d
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 17 deletions.
10 changes: 5 additions & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,17 +158,17 @@ pub struct Cli {
#[arg(short, long)]
pub pattern: Vec<String>,

/// A template string that defines how to transform a line input. See long '--help'
/// A template string that defines how to transform a line input. Can be specified multiple
/// times. See long '--help'
#[arg(short, long)]
pub template: String,
pub template: Vec<String>,

/// Input files
pub files: Vec<String>,

/// Name of capture that must have at least one match for the output to show. Can be specified
/// multiple times
/// Comma-separated capture names that must all have a match for an input line to be processed.
#[arg(short, long)]
pub require: Vec<String>,
pub require: String,

/// Force output to be line-buffered. By default, output is line buffered when stdout is a
/// terminal and block-buffered otherwise
Expand Down
24 changes: 15 additions & 9 deletions src/line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ pub fn process_lines(tty: &mut TtyContext, args: &Cli) -> Result<()> {
..
} = args;

let template = OutputTemplate::parse(template)?;
let filters = require.split(",").map(str::trim).collect::<Vec<_>>();

let mut templates = Vec::with_capacity(template.len());
for templ in template {
templates.push(OutputTemplate::parse(templ)?);
}

let mut regexes = Vec::new();
for pat in pattern {
let re =
Regex::new(pat).with_context(|| format!("encountered invalid regular expression in pattern: {pat}"))?;
let re = Regex::new(pat).with_context(|| format!("encountered invalid regular expression: {pat}"))?;
regexes.push(re);
}

Expand Down Expand Up @@ -77,17 +81,19 @@ pub fn process_lines(tty: &mut TtyContext, args: &Cli) -> Result<()> {
}
}
}
for capture_name in require {
if captures_map.get(capture_name.as_str()).is_none_or(|c| c.is_empty()) {
for capture_name in &filters {
if captures_map.get(capture_name).is_none_or(|c| c.is_empty()) {
continue 'outer;
}
}
let output_line = template.transform(&captures_map);
for template in &templates {
let output_line = template.transform(&captures_map);

if output_line.is_empty() {
continue;
if output_line.is_empty() {
continue;
}
writer.writeln(&output_line)?;
}
writer.writeln(&output_line)?;
}
Ok(())
}
12 changes: 11 additions & 1 deletion src/template/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{
parse::rules::VALID_ANCHOR_CHARSET,
token::{ANCHOR_CLOSE, ATTRIBUTE_CLOSE, ATTRIBUTE_END, ESCAPE},
token::{ANCHOR_CLOSE, ATTRIBUTE_CLOSE, ATTRIBUTE_END, ESCAPE, REQUIRED},
};
use indoc::{formatdoc, indoc};
use std::fmt::{self, Display};
Expand Down Expand Up @@ -119,6 +119,16 @@ impl ParseError {
}
}

pub fn default_disallowed_with_required(char_index: usize, chars: &[char]) -> Self {
ParseError {
char_index,
partial_template: chars.iter().collect(),
message: format!(
"Default values are disallowed when an anchor is marked as required with a leading '{REQUIRED}'",
),
}
}

pub fn index_parsing_eol(char_index: usize, chars: &[char]) -> Self {
ParseError {
char_index,
Expand Down
9 changes: 8 additions & 1 deletion src/template/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ impl OutputTemplate {

for target in &self.targets {
match target {
InterpolationTarget::Literal(val) => out.push_str(val),
InterpolationTarget::Anchor(anchor) => {
let name = anchor.name.as_str();
let index = anchor.index.unwrap_or_default();

if let Some(val) = interpolation_map.get(name).and_then(|vals| vals.get(index)) {
if anchor.attributes.is_empty() {
out.push_str(val);
Expand All @@ -89,6 +89,12 @@ impl OutputTemplate {
}
continue;
}

// No match, return empty string.
if anchor.required {
return String::new();
}

for default_val in &anchor.defaults {
match default_val {
DefaultValue::Literal(val) => {
Expand Down Expand Up @@ -116,6 +122,7 @@ impl OutputTemplate {
}
}
}
InterpolationTarget::Literal(val) => out.push_str(val),
}
}
out
Expand Down
17 changes: 16 additions & 1 deletion src/template/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::{
error::ParseError,
token::{
ANCHOR_CLOSE, ANCHOR_OPEN, ATTRIBUTE_CLOSE, ATTRIBUTE_DELIMETER, ATTRIBUTE_END, ATTRIBUTE_OPEN, DEFAULT_PIPE,
ESCAPE, INDEX_CLOSE, INDEX_OPEN, LITERAL_DOUBLE_QUOTE, LITERAL_SINGLE_QUOTE,
ESCAPE, INDEX_CLOSE, INDEX_OPEN, LITERAL_DOUBLE_QUOTE, LITERAL_SINGLE_QUOTE, REQUIRED,
},
};
use anyhow::{format_err, Result};
Expand Down Expand Up @@ -41,6 +41,7 @@ pub struct Anchor {
pub index: Option<usize>,
pub defaults: Vec<DefaultValue>,
pub attributes: Vec<Attribute>,
pub required: bool,
}

/// State that is maintained during parsing. The `cursor` is the index of the current token
Expand Down Expand Up @@ -152,6 +153,16 @@ fn parse_impl(mode: &mut ParseState, anchors: &mut Vec<Anchor>, rules: &Rules) -
}
continue;
}
if mode.tokens.get(mode.cursor).is_some_and(|ch| *ch == REQUIRED) {
let Some(anchor) = mode.bound_anchor.as_mut() else {
log::error!("expected mode.bound_anchor to be `Some` while in `AnchorParseBegin`");
return Err(format_err!(
"An unexpected error occurred while parsing template string."
));
};
anchor.required = true;
mode.cursor += 1;
}
if mode.tokens.get(mode.cursor).is_some_and(|ch| *ch == ATTRIBUTE_OPEN) {
mode.mode = ParseStateMode::AttributeParse;
} else {
Expand Down Expand Up @@ -266,6 +277,10 @@ fn parse_impl(mode: &mut ParseState, anchors: &mut Vec<Anchor>, rules: &Rules) -
}

ParseStateMode::AnchorParseDefaultValue => {
if mode.bound_anchor.as_ref().is_some_and(|a| a.required) {
return Err(ParseError::default_disallowed_with_required(mode.cursor - 1, &mode.tokens).into());
}

mode.cursor += 1;
for i in mode.cursor..mode.tokens.len() {
mode.cursor = i;
Expand Down
47 changes: 47 additions & 0 deletions src/template/parse/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ fn test_parse_plain() {

let anchor = &anchors[0];
assert_eq!(&anchor.name, "log");
assert!(!anchor.required);
assert_eq!("{log}", &template_string[anchor.start..anchor.end]);
assert_eq!(anchor.index, None);
}
Expand Down Expand Up @@ -248,3 +249,49 @@ fn test_literal_anchor_with_attributes() {
assert!(anchor.attributes.iter().find(|a| a == &&Attribute::Red).is_some());
assert!(anchor.attributes.iter().find(|a| a == &&Attribute::Bold).is_some());
}

#[test]
fn test_required_anchor() {
let template_string = "output={!log}";
let anchors = parse(template_string).unwrap();
assert_eq!(anchors.len(), 1);

let anchor = &anchors[0];
assert_eq!(&anchor.name, "log");
assert_eq!("{!log}", &template_string[anchor.start..anchor.end]);
assert_eq!(anchor.index, None);
assert!(anchor.required);
}

#[test]
fn test_required_anchor_with_attrs() {
let template_string = "output={!(red|bold):log}";
let anchors = parse(template_string).unwrap();
assert_eq!(anchors.len(), 1);

let anchor = &anchors[0];
assert_eq!(&anchor.name, "log");
assert_eq!("{!(red|bold):log}", &template_string[anchor.start..anchor.end]);
assert_eq!(anchor.index, None);
assert!(anchor.required);
}

#[test]
fn test_required_anchor_no_defaults() {
let template_string = "output={!log || foo}";
let anchors = parse(template_string);
assert!(anchors.is_err());

let template_string = "output={!log||foo}";
let anchors = parse(template_string);
assert!(anchors.is_err());

let template_string = "output={!log||\"foo\"}";
let anchors = parse(template_string);
assert!(anchors.is_err());

// Pointless but will support anyway
let template_string = "output={!'log'}";
let anchors = parse(template_string);
assert!(anchors.is_ok());
}
17 changes: 17 additions & 0 deletions src/template/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,20 @@ fn test_output_template_attributes() {
let expected = Attribute::apply("bar_value_2", &[Attribute::Red, Attribute::Bold]);
assert_eq!(resultant, format!("{expected}"));
}

#[test]
fn test_output_template_required() {
let template = "log={!foo} out={bar} baz";
let out = OutputTemplate::parse(template).unwrap();
assert_eq!(out.targets.len(), 5);

let mut interpolation_map = HashMap::new();
interpolation_map.insert("foo", vec![]);
interpolation_map.insert("bar", vec!["bar_value"]);

let resultant = out.transform(&interpolation_map);
assert_eq!(
resultant, "",
"foo doesn't have any matches so string should come back empty"
);
}
1 change: 1 addition & 0 deletions src/template/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pub const ATTRIBUTE_OPEN: char = '(';
pub const ATTRIBUTE_CLOSE: char = ')';
pub const ATTRIBUTE_DELIMETER: char = '|';
pub const ATTRIBUTE_END: char = ':';
pub const REQUIRED: char = '!';

0 comments on commit a8d284d

Please sign in to comment.