Skip to content

Commit

Permalink
feat(set): implement nocaseglob + nocasematch options (reubeno#282)
Browse files Browse the repository at this point in the history
  • Loading branch information
reubeno authored Nov 30, 2024
1 parent a2db75f commit 2e9447d
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 22 deletions.
4 changes: 3 additions & 1 deletion brush-core/src/builtins/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ impl HelpCommand {
context: &commands::ExecutionContext<'_>,
topic_pattern: &str,
) -> Result<(), crate::error::Error> {
let pattern = crate::patterns::Pattern::from(topic_pattern);
let pattern = crate::patterns::Pattern::from(topic_pattern)
.set_extended_globbing(context.shell.options.extended_globbing)
.set_case_insensitive(context.shell.options.case_insensitive_pathname_expansion);

let mut found_count = 0;
for (builtin_name, builtin_registration) in get_builtins_sorted_by_name(context) {
Expand Down
8 changes: 5 additions & 3 deletions brush-core/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ impl Spec {

if let Some(glob_pattern) = &self.glob_pattern {
let pattern = patterns::Pattern::from(glob_pattern.as_str())
.set_extended_globbing(shell.options.extended_globbing);
.set_extended_globbing(shell.options.extended_globbing)
.set_case_insensitive(shell.options.case_insensitive_pathname_expansion);

let expansions = pattern.expand(
shell.working_dir.as_path(),
Expand Down Expand Up @@ -956,8 +957,9 @@ async fn get_file_completions(

let path_filter = |path: &Path| !must_be_dir || shell.get_absolute_path(path).is_dir();

let pattern =
patterns::Pattern::from(glob).set_extended_globbing(shell.options.extended_globbing);
let pattern = patterns::Pattern::from(glob)
.set_extended_globbing(shell.options.extended_globbing)
.set_case_insensitive(shell.options.case_insensitive_pathname_expansion);

pattern
.expand(shell.working_dir.as_path(), Some(&path_filter))
Expand Down
12 changes: 7 additions & 5 deletions brush-core/src/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,7 @@ impl<'a> WordExpander<'a> {
.flatten()
.collect();

let pattern = patterns::Pattern::from(pattern_pieces)
.set_extended_globbing(self.parser_options.enable_extended_globbing);
let pattern = patterns::Pattern::from(pattern_pieces);

Ok(pattern)
}
Expand All @@ -432,7 +431,8 @@ impl<'a> WordExpander<'a> {
.flatten()
.collect();

Ok(crate::regex::Regex::from(regex_pieces))
Ok(crate::regex::Regex::from(regex_pieces)
.set_case_insensitive(self.shell.options.case_insensitive_conditionals))
}

/// Apply tilde-expansion, parameter expansion, command substitution, and arithmetic expansion;
Expand Down Expand Up @@ -528,7 +528,8 @@ impl<'a> WordExpander<'a> {

fn expand_pathnames_in_field(&self, field: WordField) -> Vec<String> {
let pattern = patterns::Pattern::from(field.clone())
.set_extended_globbing(self.parser_options.enable_extended_globbing);
.set_extended_globbing(self.parser_options.enable_extended_globbing)
.set_case_insensitive(self.shell.options.case_insensitive_pathname_expansion);

let expansions = pattern
.expand(
Expand Down Expand Up @@ -1032,7 +1033,8 @@ impl<'a> WordExpander<'a> {
let expanded_replacement = self.basic_expand_to_str(&replacement).await?;

let pattern = patterns::Pattern::from(expanded_pattern.as_str())
.set_extended_globbing(self.parser_options.enable_extended_globbing);
.set_extended_globbing(self.parser_options.enable_extended_globbing)
.set_case_insensitive(self.shell.options.case_insensitive_conditionals);

let regex = pattern.to_regex(
matches!(match_kind, brush_parser::word::SubstringMatchKind::Prefix),
Expand Down
16 changes: 12 additions & 4 deletions brush-core/src/extendedtests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,10 @@ async fn apply_binary_predicate(
// TODO: implement case-insensitive matching if relevant via shopt options (nocasematch).
ast::BinaryPredicate::StringExactlyMatchesPattern => {
let s = expansion::basic_expand_word(shell, left).await?;
let pattern = expansion::basic_expand_pattern(shell, right).await?;
let pattern = expansion::basic_expand_pattern(shell, right)
.await?
.set_extended_globbing(shell.options.extended_globbing)
.set_case_insensitive(shell.options.case_insensitive_conditionals);

if shell.options.print_commands_and_arguments {
let expanded_right = expansion::basic_expand_word(shell, right).await?;
Expand All @@ -362,7 +365,10 @@ async fn apply_binary_predicate(
}
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
let s = expansion::basic_expand_word(shell, left).await?;
let pattern = expansion::basic_expand_pattern(shell, right).await?;
let pattern = expansion::basic_expand_pattern(shell, right)
.await?
.set_extended_globbing(shell.options.extended_globbing)
.set_case_insensitive(shell.options.case_insensitive_conditionals);

if shell.options.print_commands_and_arguments {
let expanded_right = expansion::basic_expand_word(shell, right).await?;
Expand Down Expand Up @@ -429,13 +435,15 @@ pub(crate) fn apply_binary_predicate_to_strs(
),
ast::BinaryPredicate::StringExactlyMatchesPattern => {
let pattern = patterns::Pattern::from(right)
.set_extended_globbing(shell.options.extended_globbing);
.set_extended_globbing(shell.options.extended_globbing)
.set_case_insensitive(shell.options.case_insensitive_conditionals);

pattern.exactly_matches(left)
}
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
let pattern = patterns::Pattern::from(right)
.set_extended_globbing(shell.options.extended_globbing);
.set_extended_globbing(shell.options.extended_globbing)
.set_case_insensitive(shell.options.case_insensitive_conditionals);

let eq = pattern.exactly_matches(left)?;
Ok(!eq)
Expand Down
6 changes: 5 additions & 1 deletion brush-core/src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,11 @@ impl Execute for ast::CaseClauseCommand {
} else {
let mut matches = false;
for pattern in &case.patterns {
let expanded_pattern = expansion::basic_expand_pattern(shell, pattern).await?;
let expanded_pattern = expansion::basic_expand_pattern(shell, pattern)
.await?
.set_extended_globbing(shell.options.extended_globbing)
.set_case_insensitive(shell.options.case_insensitive_conditionals);

if expanded_pattern.exactly_matches(expanded_value.as_str())? {
matches = true;
break;
Expand Down
19 changes: 16 additions & 3 deletions brush-core/src/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct Pattern {
pieces: PatternWord,
enable_extended_globbing: bool,
multiline: bool,
case_insensitive: bool,
}

impl Default for Pattern {
Expand All @@ -38,6 +39,7 @@ impl Default for Pattern {
pieces: vec![],
enable_extended_globbing: false,
multiline: true,
case_insensitive: false,
}
}
}
Expand Down Expand Up @@ -100,6 +102,16 @@ impl Pattern {
self
}

/// Enables (or disables) case-insensitive matching for this pattern.
///
/// # Arguments
///
/// * `value` - Whether or not to enable case-insensitive matching.
pub fn set_case_insensitive(mut self, value: bool) -> Pattern {
self.case_insensitive = value;
self
}

/// Returns whether or not the pattern is empty.
pub fn is_empty(&self) -> bool {
self.pieces.iter().all(|p| p.as_str().is_empty())
Expand Down Expand Up @@ -214,8 +226,9 @@ impl Pattern {

let current_paths = std::mem::take(&mut paths_so_far);
for current_path in current_paths {
let subpattern =
Pattern::from(&component).set_extended_globbing(self.enable_extended_globbing);
let subpattern = Pattern::from(&component)
.set_extended_globbing(self.enable_extended_globbing)
.set_case_insensitive(self.case_insensitive);

let regex = subpattern.to_regex(true, true)?;

Expand Down Expand Up @@ -333,7 +346,7 @@ impl Pattern {

tracing::debug!(target: trace_categories::PATTERN, "pattern: '{self:?}' => regex: '{regex_str}'");

let re = regex::compile_regex(regex_str)?;
let re = regex::compile_regex(regex_str, self.case_insensitive)?;
Ok(re)
}

Expand Down
28 changes: 24 additions & 4 deletions brush-core/src/regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,29 @@ type RegexWord = Vec<RegexPiece>;
#[derive(Clone, Debug)]
pub struct Regex {
pieces: RegexWord,
case_insensitive: bool,
}

impl From<RegexWord> for Regex {
fn from(pieces: RegexWord) -> Self {
Self { pieces }
Self {
pieces,
case_insensitive: false,
}
}
}

impl Regex {
/// Sets the regular expression's case sensitivity.
///
/// # Arguments
///
/// * `value` - The new case sensitivity value.
pub fn set_case_insensitive(mut self, value: bool) -> Self {
self.case_insensitive = value;
self
}

/// Computes if the regular expression matches the given string.
///
/// # Arguments
Expand All @@ -48,7 +62,7 @@ impl Regex {
.collect();

// TODO: Evaluate how compatible the `fancy_regex` crate is with POSIX EREs.
let re = compile_regex(regex_pattern)?;
let re = compile_regex(regex_pattern, self.case_insensitive)?;

Ok(re.captures(value)?.map(|captures| {
captures
Expand All @@ -61,8 +75,14 @@ impl Regex {

#[allow(clippy::needless_pass_by_value)]
#[cached::proc_macro::cached(size = 64, result = true)]
pub(crate) fn compile_regex(regex_str: String) -> Result<fancy_regex::Regex, error::Error> {
match fancy_regex::Regex::new(regex_str.as_str()) {
pub(crate) fn compile_regex(
regex_str: String,
case_insensitive: bool,
) -> Result<fancy_regex::Regex, error::Error> {
let mut builder = fancy_regex::RegexBuilder::new(regex_str.as_str());
builder.case_insensitive(case_insensitive);

match builder.build() {
Ok(re) => Ok(re),
Err(e) => Err(error::Error::InvalidRegexError(e, regex_str)),
}
Expand Down
3 changes: 2 additions & 1 deletion brush-core/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,8 @@ impl Shell {
for dir_str in self.env.get_str("PATH").unwrap_or_default().split(':') {
let pattern =
patterns::Pattern::from(std::format!("{dir_str}/{required_glob_pattern}"))
.set_extended_globbing(self.options.extended_globbing);
.set_extended_globbing(self.options.extended_globbing)
.set_case_insensitive(self.options.case_insensitive_pathname_expansion);

// TODO: Pass through quoting.
if let Ok(entries) = pattern.expand(&self.working_dir, Some(&is_executable)) {
Expand Down
8 changes: 8 additions & 0 deletions brush-shell/tests/cases/compound_cmds/case.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ cases:
*) echo "unhandled case" ;;
esac
- name: "Case with case insensitive pattern"
stdin: |
shopt -s nocasematch
case "A" in
a) echo "matched" ;;
*) echo "did not match" ;;
esac
- name: "Interesting patterns in cases"
stdin: |
for word in "-a" "!b" "*c" "(d" "{e" ":f" "'g"; do
Expand Down
8 changes: 8 additions & 0 deletions brush-shell/tests/cases/extended_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ cases:
[[ a =~ ^(a|b)$ ]] && echo "4. Pass"
[[ a =~ c ]] && echo "5. Pass"
- name: "Regex with case insensitivity"
stdin: |
shopt -u nocasematch
[[ "a" =~ A ]] && echo "1. Pass"
shopt -s nocasematch
[[ "a" =~ A ]] && echo "1. Pass"
- name: "Regex with capture"
stdin: |
pattern='(Hello), ([a-z]+)\.'
Expand Down
19 changes: 19 additions & 0 deletions brush-shell/tests/cases/patterns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ cases:
echo *.txt
echo *."txt"
- name: "File expansion with nocaseglob"
test_files:
- path: "FILE1.TXT"
- path: "file2.txt"
stdin: |
shopt -s nocaseglob
echo "*.txt:" *.txt
echo "*.TXT:" *.txt
- name: "Nested directory expansion"
test_files:
- path: "dir/file1.txt"
Expand Down Expand Up @@ -274,3 +283,13 @@ cases:
[[ "x" == [xyz] ]] && echo "2. Matched"
[[ "1" == [[:digit:]] ]] && echo "3. Matched"
[[ "(" == [\(] ]] && echo "4. Matched"
- name: "Pattern matching: case sensitivity"
stdin: |
shopt -u nocasematch
[[ "abc" == "ABC" ]] && echo "1. Matched"
[[ "abc" == "[A-Z]BC" ]] && echo "2. Matched"
shopt -s nocasematch
[[ "abc" == "ABC" ]] && echo "3. Matched"
[[ "abc" == "[A-Z]BC" ]] && echo "4. Matched"
Loading

0 comments on commit 2e9447d

Please sign in to comment.