Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/bashkit/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ fn literal_word(word: &Word) -> Option<String> {
_ => return None,
}
}
// A `QuotedGlobWord` stores its quoted glob characters backslash-escaped;
// report the text the command actually sees.
if word.quoted && word.has_unquoted_glob {
out = crate::parser::unescape_glob_literal(&out);
}
Some(out)
}

Expand Down
25 changes: 24 additions & 1 deletion crates/bashkit/src/interpreter/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,15 @@ impl Interpreter {
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async move {
let expanded = self.expand_word_inner(word).await?;
Ok(Self::strip_quote_markers(&expanded))
let expanded = Self::strip_quote_markers(&expanded);
// A `QuotedGlobWord` carries glob escapes for its quoted text;
// contexts that do not glob (command names, redirect targets,
// operands) want the text itself.
if word.quoted && word.has_unquoted_glob {
Ok(Self::glob_path_unescape(&expanded))
} else {
Ok(expanded)
}
})
}

Expand Down Expand Up @@ -164,6 +172,21 @@ impl Interpreter {
}
}

/// Expand a word used as a pattern (`case` item, `[[ == ]]` operand).
/// Quoted text must match literally, so a fully quoted word has every
/// glob metacharacter escaped. A mixed word (`QuotedGlobWord`) already
/// carries escapes for its quoted literals from the lexer and for its
/// quoted expansions from `append_expansion_for_word`.
pub(super) async fn expand_pattern_word(&mut self, word: &Word) -> Result<String> {
let expanded = Box::pin(self.expand_word_inner(word)).await?;
let expanded = Self::strip_quote_markers(&expanded);
if word.quoted && !word.has_unquoted_glob {
Ok(Self::quote_expansion_for_quoted_glob(&expanded))
} else {
Ok(expanded)
}
}

pub(super) async fn expand_word_inner(&mut self, word: &Word) -> Result<String> {
let mut result = String::new();
// Keep command-substitution bytes charged until the complete word has
Expand Down
38 changes: 32 additions & 6 deletions crates/bashkit/src/interpreter/glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,22 @@ impl Interpreter {
if self.contains_glob_chars(pattern) || self.contains_extglob(pattern) {
self.glob_match(value, pattern)
} else {
// Literal match
value == pattern
// Literal match; a backslash still escapes the next character
// (`\*` from quoted pattern text matches a literal `*`, and an
// unquoted `a\b` pattern matches `ab`, as in bash).
if pattern.contains('\\') {
let mut literal = String::with_capacity(pattern.len());
let mut chars = pattern.chars();
while let Some(ch) = chars.next() {
match ch {
'\\' => literal.push(chars.next().unwrap_or('\\')),
_ => literal.push(ch),
}
}
value == literal
} else {
value == pattern
}
}
}

Expand Down Expand Up @@ -703,17 +717,29 @@ impl Interpreter {

/// Expand glob for a single item, applying noglob/failglob/nullglob.
/// Returns Err(pattern) if failglob triggers, Ok(items) otherwise.
/// Glob-expand one field. `escaped` marks a field from a word whose
/// quoted glob characters the lexer backslash-escaped (quoted &&
/// has_unquoted_glob): when the field is not replaced by matches, those
/// escapes are removed so `"*"zz*` with no match prints `*zz*`.
pub(crate) async fn expand_glob_item(
&self,
item: &str,
escaped: bool,
) -> std::result::Result<Vec<String>, String> {
let literal = |s: &str| {
if escaped {
Self::glob_path_unescape(s)
} else {
s.to_string()
}
};
if !(self.contains_glob_chars(item) || self.contains_extglob(item)) || self.is_noglob() {
return Ok(vec![item.to_string()]);
return Ok(vec![literal(item)]);
}
let glob_matches = self.expand_glob(item).await.unwrap_or_default();
if glob_matches.is_empty() {
if self.is_failglob() {
return Err(item.to_string());
return Err(literal(item));
}
let nullglob = self
.scoped
Expand All @@ -724,7 +750,7 @@ impl Interpreter {
if nullglob {
Ok(vec![])
} else {
Ok(vec![item.to_string()])
Ok(vec![literal(item)])
}
} else {
Ok(glob_matches)
Expand All @@ -743,7 +769,7 @@ impl Interpreter {
/// The metacharacter set below must stay in sync with the escape set in
/// `Interpreter::quote_expansion_for_quoted_glob` (interpreter/mod.rs); if
/// one side adds or drops a character, lookups break or escapes leak.
fn glob_path_unescape(s: &str) -> String {
pub(super) fn glob_path_unescape(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
Expand Down
35 changes: 28 additions & 7 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3652,7 +3652,10 @@ impl Interpreter {
for expanded in fields {
let brace_expanded = self.expand_braces(&expanded);
for item in brace_expanded {
match self.expand_glob_item(&item).await {
match self
.expand_glob_item(&item, w.quoted && w.has_unquoted_glob)
.await
{
Ok(items) => vals.extend(items),
Err(pat) => {
self.last_exit_code = 1;
Expand Down Expand Up @@ -3778,7 +3781,10 @@ impl Interpreter {
for expanded in fields {
let brace_expanded = self.expand_braces(&expanded);
for item in brace_expanded {
match self.expand_glob_item(&item).await {
match self
.expand_glob_item(&item, w.quoted && w.has_unquoted_glob)
.await
{
Ok(items) => values.extend(items),
Err(pat) => {
self.last_exit_code = 1;
Expand Down Expand Up @@ -4171,10 +4177,22 @@ impl Interpreter {
return self.evaluate_conditional_words(&words[i + 1..]).await;
}

// Leaf: expand words and evaluate as a simple condition
// Leaf: expand words and evaluate as a simple condition. The
// right side of `==`/`=`/`!=` is a pattern: its quoted parts
// match literally.
let mut expanded = Vec::new();
for word in words {
expanded.push(self.expand_word(word).await?);
for (i, word) in words.iter().enumerate() {
let is_pattern = i > 0
&& !words[i - 1].quoted
&& matches!(
words[i - 1].parts.as_slice(),
[WordPart::Literal(op)] if matches!(op.as_str(), "==" | "=" | "!=")
);
expanded.push(if is_pattern {
self.expand_pattern_word(word).await?
} else {
self.expand_word(word).await?
});
}
Ok(self.evaluate_conditional(&expanded).await)
})
Expand Down Expand Up @@ -4597,7 +4615,7 @@ impl Interpreter {
} else {
let mut m = false;
for pattern in &case_item.patterns {
let pattern_str = self.expand_word(pattern).await?;
let pattern_str = self.expand_pattern_word(pattern).await?;
if self.pattern_matches(&word_value, &pattern_str) {
m = true;
break;
Expand Down Expand Up @@ -6898,7 +6916,10 @@ impl Interpreter {

// Step 2: For each brace-expanded item, do glob expansion
for item in brace_expanded {
match self.expand_glob_item(&item).await {
match self
.expand_glob_item(&item, word.quoted && word.has_unquoted_glob)
.await
{
Ok(items) => args.extend(items),
Err(pat) => {
self.last_exit_code = 1;
Expand Down
Loading
Loading