Skip to content

Commit 44c93c5

Browse files
committed
fix(parser): allow glob expansion on unquoted suffix after quoted prefix
After a double-quoted string, check for adjacent unquoted content (e.g. "$DIR"/*) and concatenate into a single Word token instead of QuotedWord. This allows glob/brace expansion on the unquoted portion. Closes #398 https://claude.ai/code/session_01WZjYqxm5xMPAEe7FSHJkDy
1 parent fc51ce0 commit 44c93c5

File tree

2 files changed

+36
-0
lines changed

2 files changed

+36
-0
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9686,4 +9686,29 @@ bash /tmp/opts.sh -f xml -v
96869686
assert_eq!(lines[0], "FORMAT=json VERBOSE=1");
96879687
assert_eq!(lines[1], "FORMAT=xml VERBOSE=1");
96889688
}
9689+
9690+
#[tokio::test]
9691+
async fn test_glob_with_quoted_prefix() {
9692+
// Issue #398: "$DIR"/* should glob-expand the unquoted /*
9693+
let mut bash = crate::Bash::new();
9694+
bash.fs()
9695+
.mkdir(std::path::Path::new("/testdir"), true)
9696+
.await
9697+
.unwrap();
9698+
bash.fs()
9699+
.write_file(std::path::Path::new("/testdir/a.txt"), b"a")
9700+
.await
9701+
.unwrap();
9702+
bash.fs()
9703+
.write_file(std::path::Path::new("/testdir/b.txt"), b"b")
9704+
.await
9705+
.unwrap();
9706+
let result = bash
9707+
.exec(r#"DIR="/testdir"; for f in "$DIR"/*; do echo "$f"; done"#)
9708+
.await
9709+
.unwrap();
9710+
let mut lines: Vec<&str> = result.stdout.trim().lines().collect();
9711+
lines.sort();
9712+
assert_eq!(lines, vec!["/testdir/a.txt", "/testdir/b.txt"]);
9713+
}
96899714
}

crates/bashkit/src/parser/lexer.rs

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

1087+
// Check for continuation after closing quote: "foo"bar or "foo"/* etc.
1088+
// If there's adjacent unquoted content (word chars, globs, more quotes),
1089+
// concatenate and return as Word (not QuotedWord) so glob expansion works
1090+
// on the unquoted portion.
1091+
if let Some(ch) = self.peek_char() {
1092+
if self.is_word_char(ch) || ch == '\'' || ch == '"' || ch == '$' {
1093+
self.read_continuation_into(&mut content);
1094+
return Some(Token::Word(content));
1095+
}
1096+
}
1097+
10871098
Some(Token::QuotedWord(content))
10881099
}
10891100

0 commit comments

Comments
 (0)