diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 12463e05..707bc9ba 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -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('"')) @@ -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) }; @@ -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 = inner - .split_whitespace() - .map(|s| self.parse_word(s.to_string())) + let elements: Vec = 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 { diff --git a/crates/bashkit/tests/spec_cases/bash/arrays.test.sh b/crates/bashkit/tests/spec_cases/bash/arrays.test.sh index c601df9d..3051b4d3 100644 --- a/crates/bashkit/tests/spec_cases/bash/arrays.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/arrays.test.sh @@ -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> + +### 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 + + +### 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 + + + +### end + ### unquoted_expansion_word_split_in_array # arr=($x) should word-split on IFS x="alpha beta gamma" diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index 3c7a36a4..da6feeab 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -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 |