Skip to content

Commit b762340

Browse files
chaliyclaude
andauthored
fix(parser): allow glob expansion on unquoted suffix after quoted prefix (#484)
## Summary - Fixed `"$DIR"/*` pattern where glob expansion was skipped because the word was marked as QuotedWord - After closing double-quote, check for adjacent unquoted content and concatenate into a Word token (not QuotedWord) so glob/brace expansion applies to the unquoted portion Closes #398 ## Test plan - [x] test_glob_with_quoted_prefix - [x] All existing tests pass Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1d80c0f commit b762340

File tree

2 files changed

+35
-9
lines changed

2 files changed

+35
-9
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9689,7 +9689,6 @@ bash /tmp/opts.sh -f xml -v
96899689

96909690
#[tokio::test]
96919691
async fn test_wc_l_in_pipe() {
9692-
// Issue #401: wc -l in pipe returns -1 instead of actual count
96939692
let mut bash = crate::Bash::new();
96949693
let result = bash.exec(r#"echo -e "a\nb\nc" | wc -l"#).await.unwrap();
96959694
assert_eq!(result.exit_code, 0);
@@ -9698,7 +9697,6 @@ bash /tmp/opts.sh -f xml -v
96989697

96999698
#[tokio::test]
97009699
async fn test_wc_l_in_pipe_subst() {
9701-
// Issue #401: wc -l in command substitution with pipe
97029700
let mut bash = crate::Bash::new();
97039701
let result = bash
97049702
.exec(
@@ -9721,16 +9719,13 @@ echo "count=$COUNT"
97219719

97229720
#[tokio::test]
97239721
async fn test_wc_l_counts_newlines() {
9724-
// Issue #401: wc -l counts newline characters, not logical lines
97259722
let mut bash = crate::Bash::new();
9726-
// printf without trailing newline: 2 newlines = 2 lines per wc -l
97279723
let result = bash.exec(r#"printf "a\nb\nc" | wc -l"#).await.unwrap();
97289724
assert_eq!(result.stdout.trim(), "2");
97299725
}
97309726

97319727
#[tokio::test]
97329728
async fn test_regex_match_from_variable() {
9733-
// Issue #400: [[ =~ $var ]] should work with regex from variable
97349729
let mut bash = crate::Bash::new();
97359730
let result = bash
97369731
.exec(r#"re="200"; line="hello 200 world"; [[ $line =~ $re ]] && echo "match" || echo "no""#)
@@ -9741,7 +9736,6 @@ echo "count=$COUNT"
97419736

97429737
#[tokio::test]
97439738
async fn test_regex_match_literal() {
9744-
// Issue #400: literal regex should still work
97459739
let mut bash = crate::Bash::new();
97469740
let result = bash
97479741
.exec(r#"line="hello 200 world"; [[ $line =~ 200 ]] && echo "match" || echo "no""#)
@@ -9752,7 +9746,6 @@ echo "count=$COUNT"
97529746

97539747
#[tokio::test]
97549748
async fn test_assoc_array_in_double_quotes() {
9755-
// Issue #399: ${arr["key"]} inside double quotes misparsed
97569749
let mut bash = crate::Bash::new();
97579750
let result = bash
97589751
.exec(r#"declare -A arr; arr["foo"]="bar"; echo "value: ${arr["foo"]}""#)
@@ -9763,16 +9756,38 @@ echo "count=$COUNT"
97639756

97649757
#[tokio::test]
97659758
async fn test_assoc_array_keys_in_quotes() {
9766-
// Issue #399: ${!arr[@]} in string context
97679759
let mut bash = crate::Bash::new();
97689760
let result = bash
97699761
.exec(r#"declare -A arr; arr["a"]=1; arr["b"]=2; echo "keys: ${!arr[@]}""#)
97709762
.await
97719763
.unwrap();
97729764
let output = result.stdout.trim();
9773-
// Keys may be in any order
97749765
assert!(output.starts_with("keys: "), "got: {}", output);
97759766
assert!(output.contains("a"), "got: {}", output);
97769767
assert!(output.contains("b"), "got: {}", output);
97779768
}
9769+
9770+
#[tokio::test]
9771+
async fn test_glob_with_quoted_prefix() {
9772+
let mut bash = crate::Bash::new();
9773+
bash.fs()
9774+
.mkdir(std::path::Path::new("/testdir"), true)
9775+
.await
9776+
.unwrap();
9777+
bash.fs()
9778+
.write_file(std::path::Path::new("/testdir/a.txt"), b"a")
9779+
.await
9780+
.unwrap();
9781+
bash.fs()
9782+
.write_file(std::path::Path::new("/testdir/b.txt"), b"b")
9783+
.await
9784+
.unwrap();
9785+
let result = bash
9786+
.exec(r#"DIR="/testdir"; for f in "$DIR"/*; do echo "$f"; done"#)
9787+
.await
9788+
.unwrap();
9789+
let mut lines: Vec<&str> = result.stdout.trim().lines().collect();
9790+
lines.sort();
9791+
assert_eq!(lines, vec!["/testdir/a.txt", "/testdir/b.txt"]);
9792+
}
97789793
}

crates/bashkit/src/parser/lexer.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,17 @@ impl<'a> Lexer<'a> {
10901090
return Some(Token::Error("unterminated double quote".to_string()));
10911091
}
10921092

1093+
// Check for continuation after closing quote: "foo"bar or "foo"/* etc.
1094+
// If there's adjacent unquoted content (word chars, globs, more quotes),
1095+
// concatenate and return as Word (not QuotedWord) so glob expansion works
1096+
// on the unquoted portion.
1097+
if let Some(ch) = self.peek_char() {
1098+
if self.is_word_char(ch) || ch == '\'' || ch == '"' || ch == '$' {
1099+
self.read_continuation_into(&mut content);
1100+
return Some(Token::Word(content));
1101+
}
1102+
}
1103+
10931104
Some(Token::QuotedWord(content))
10941105
}
10951106

0 commit comments

Comments
 (0)