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
71 changes: 68 additions & 3 deletions crates/bashkit/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1792,6 +1792,59 @@ impl<'a> Parser<'a> {
}

/// Strip surrounding quotes from a string value
/// Split array element text respecting single and double quotes.
/// Returns Vec of (element_text, was_quoted).
/// Quoted elements have their outer quotes stripped.
fn split_array_elements(s: &str) -> Vec<(String, bool)> {
let mut result = Vec::new();
let mut current = String::new();
let mut chars = s.chars().peekable();
let mut in_double_quote = false;
let mut in_single_quote = false;
let mut is_quoted = false;

while let Some(c) = chars.next() {
match c {
'"' if !in_single_quote => {
in_double_quote = !in_double_quote;
is_quoted = true;
// Don't include the quote character in output
}
'\'' if !in_double_quote => {
in_single_quote = !in_single_quote;
is_quoted = true;
// Don't include the quote character in output
}
'\\' if in_double_quote => {
// In double quotes, backslash escapes certain chars
if let Some(&next) = chars.peek() {
if matches!(next, '$' | '`' | '"' | '\\' | '\n') {
current.push(chars.next().unwrap());
} else {
current.push(c);
}
} else {
current.push(c);
}
}
c if c.is_ascii_whitespace() && !in_double_quote && !in_single_quote => {
if !current.is_empty() {
result.push((current.clone(), is_quoted));
current.clear();
is_quoted = false;
}
}
_ => {
current.push(c);
}
}
}
if !current.is_empty() {
result.push((current, is_quoted));
}
result
}

fn strip_quotes(s: &str) -> &str {
if s.len() >= 2
&& ((s.starts_with('"') && s.ends_with('"'))
Expand Down Expand Up @@ -1879,6 +1932,10 @@ impl<'a> Parser<'a> {
parts: vec![WordPart::Literal(elem_clone)],
quoted: true,
}
} else if matches!(&self.current_token, Some(tokens::Token::QuotedWord(_))) {
let mut w = self.parse_word(elem_clone);
w.quoted = true;
w
} else {
self.parse_word(elem_clone)
};
Expand Down Expand Up @@ -1906,9 +1963,17 @@ impl<'a> Parser<'a> {
// Array literal in the token itself: arr=(a b c)
if value_str.starts_with('(') && value_str.ends_with(')') {
let inner = &value_str[1..value_str.len() - 1];
let elements: Vec<Word> = inner
.split_whitespace()
.map(|s| self.parse_word(s.to_string()))
let elements: Vec<Word> = Self::split_array_elements(inner)
.into_iter()
.map(|(s, quoted)| {
if quoted {
let mut w = self.parse_word(s);
w.quoted = true;
w
} else {
self.parse_word(s)
}
})
.collect();
return Some((
Assignment {
Expand Down
36 changes: 36 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/arrays.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,42 @@ echo "${arr[1]}"
y
### end

### quoted_expansion_no_word_split_in_array
# arr=("test ${X} done") should NOT word-split inside quotes
X="hello world"
arr=(-a "test ${X} done")
echo "count: ${#arr[@]}"
printf "<%s>\n" "${arr[@]}"
### expect
count: 2
<-a>
<test hello world done>
### end

### quoted_single_quote_no_word_split_in_array
# arr=('multi word') should NOT word-split inside single quotes
arr=('hello world' 'foo bar')
echo "count: ${#arr[@]}"
printf "<%s>\n" "${arr[@]}"
### expect
count: 2
<hello world>
<foo bar>
### end

### quoted_mixed_elements_in_array
# Mix of quoted and unquoted elements preserves quoting
X="a b"
arr=(plain "quoted ${X} end" 'literal $X')
echo "count: ${#arr[@]}"
printf "<%s>\n" "${arr[@]}"
### expect
count: 3
<plain>
<quoted a b end>
<literal $X>
### end

### unquoted_expansion_word_split_in_array
# arr=($x) should word-split on IFS
x="alpha beta gamma"
Expand Down
2 changes: 1 addition & 1 deletion specs/009-implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
| arithmetic.test.sh | 75 | includes logical, bitwise, compound assign, increment/decrement, `let` builtin, `declare -i` arithmetic |
| array-slicing.test.sh | 8 | array slice operations |
| array-splat.test.sh | 2 | `"${arr[@]}"` individual element splatting in assignments |
| arrays.test.sh | 31 | indices, `${arr[@]}` / `${arr[*]}`, negative indexing `${arr[-1]}` |
| arrays.test.sh | 34 | indices, `${arr[@]}` / `${arr[*]}`, negative indexing `${arr[-1]}`, quoted expansion |
| assoc-arrays.test.sh | 22 | associative arrays `declare -A` |
| awk-printf-width.test.sh | 4 | AWK printf width/precision memory limits |
| background.test.sh | 2 | background job handling |
Expand Down
Loading