use crate::builtins::CommandFailure;
pub(crate) mod cut;
pub(crate) mod grep;
pub(crate) mod sed;
pub(crate) mod sort;
pub(crate) mod uniq;
pub(crate) mod wc;
pub(crate) use cut::Cut;
pub(crate) use grep::Grep;
pub(crate) use sed::Sed;
pub(crate) use sort::Sort;
pub(crate) use uniq::Uniq;
pub(crate) use wc::Wc;
const METACHARACTERS: &[(char, &str)] = &[
('[', "a character class"),
(']', "a character class"),
('*', "a repetition"),
('+', "a repetition"),
('?', "an optional match"),
('(', "a group"),
(')', "a group"),
('|', "an alternation"),
('{', "a repetition count"),
('}', "a repetition count"),
];
pub(crate) fn literal_pattern(command: &str, pattern: &str) -> Result<String, CommandFailure> {
let mut literal = String::with_capacity(pattern.len());
let mut characters = pattern.chars();
while let Some(character) = characters.next() {
if character == '\\' {
match characters.next() {
Some(escaped) => literal.push(escaped),
None => literal.push('\\'),
}
continue;
}
if let Some((_, meaning)) = METACHARACTERS
.iter()
.find(|(candidate, _)| *candidate == character)
{
return Err(CommandFailure::usage(format!(
"{command}: {pattern:?} uses {character:?}, which would mean {meaning} in a regular expression; patterns here are literal text, so write `\\{character}` for the character itself or use `jq` for real matching"
)));
}
literal.push(character);
}
Ok(literal)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Pattern {
needle: String,
anchored_start: bool,
anchored_end: bool,
ignore_case: bool,
}
impl Pattern {
pub(crate) fn compile(
command: &str,
pattern: &str,
ignore_case: bool,
) -> Result<Self, CommandFailure> {
let mut needle = pattern;
let anchored_start = needle.starts_with('^');
if anchored_start {
needle = &needle[1..];
}
let anchored_end = needle.len() > 1 && needle.ends_with('$');
if anchored_end {
needle = &needle[..needle.len() - 1];
}
let needle = literal_pattern(command, needle)?;
Ok(Self {
needle: if ignore_case {
needle.to_lowercase()
} else {
needle
},
anchored_start,
anchored_end,
ignore_case,
})
}
pub(crate) fn matches(&self, line: &str) -> bool {
let candidate = if self.ignore_case {
line.to_lowercase()
} else {
line.to_owned()
};
match (self.anchored_start, self.anchored_end) {
(true, true) => candidate == self.needle,
(true, false) => candidate.starts_with(&self.needle),
(false, true) => candidate.ends_with(&self.needle),
(false, false) => candidate.contains(&self.needle),
}
}
}
#[cfg(test)]
mod tests {
use super::Pattern;
fn compile(pattern: &str, ignore_case: bool) -> Pattern {
Pattern::compile("grep", pattern, ignore_case).expect("a literal pattern")
}
#[test]
fn unanchored_patterns_match_substrings() {
let pattern = compile("ell", false);
assert!(pattern.matches("hello"));
assert!(!pattern.matches("world"));
}
#[test]
fn anchors_constrain_both_ends() {
assert!(compile("^he", false).matches("hello"));
assert!(!compile("^he", false).matches("the hen"));
assert!(compile("lo$", false).matches("hello"));
assert!(!compile("lo$", false).matches("hello there"));
assert!(compile("^hello$", false).matches("hello"));
assert!(!compile("^hello$", false).matches("hello there"));
}
#[test]
fn case_folding_is_opt_in() {
assert!(!compile("HELLO", false).matches("hello"));
assert!(compile("HELLO", true).matches("hello"));
}
#[test]
fn a_lone_dollar_stays_literal() {
assert!(compile("$", false).matches("cost: $5"));
}
#[test]
fn regex_syntax_is_rejected_by_name_rather_than_matched_literally() {
for pattern in ["[0-9]", "a|b", "^ *", "colou?r", "(a)", "x{2}", "a.*b"] {
let failure =
Pattern::compile("grep", pattern, false).expect_err("regex syntax is rejected");
let message = format!("{failure:?}");
assert!(message.contains("literal text"), "{pattern}: {message}");
}
}
#[test]
fn escaping_recovers_a_metacharacter_as_ordinary_text() {
assert!(compile(r"\[warn\]", false).matches("a [warn] line"));
assert!(compile(r"2 \+ 2", false).matches("2 + 2"));
assert!(compile("example.com", false).matches("host example.com here"));
assert!(!compile("example.com", false).matches("exampleXcom"));
}
}