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
62 changes: 58 additions & 4 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use crate::builtins::{self, Builtin};
use crate::error::{Error, Result};
use crate::fs::FileSystem;
use crate::parser::{
CaseCommand, Command, CommandList, CompoundCommand, ForCommand, FunctionDef, IfCommand,
ListOperator, ParameterOp, Pipeline, Redirect, RedirectKind, Script, SimpleCommand,
AssignmentValue, CaseCommand, Command, CommandList, CompoundCommand, ForCommand, FunctionDef,
IfCommand, ListOperator, ParameterOp, Pipeline, Redirect, RedirectKind, Script, SimpleCommand,
UntilCommand, WhileCommand, Word, WordPart,
};

Expand All @@ -33,6 +33,8 @@ pub struct Interpreter {
fs: Arc<dyn FileSystem>,
env: HashMap<String, String>,
variables: HashMap<String, String>,
/// Arrays - stored as name -> index -> value
arrays: HashMap<String, HashMap<usize, String>>,
cwd: PathBuf,
last_exit_code: i32,
builtins: HashMap<&'static str, Box<dyn Builtin>>,
Expand Down Expand Up @@ -74,6 +76,7 @@ impl Interpreter {
fs,
env: HashMap::new(),
variables: HashMap::new(),
arrays: HashMap::new(),
cwd: PathBuf::from("/home/user"),
last_exit_code: 0,
builtins,
Expand Down Expand Up @@ -600,8 +603,29 @@ impl Interpreter {
) -> Result<ExecResult> {
// Process variable assignments first
for assignment in &command.assignments {
let value = self.expand_word(&assignment.value).await?;
self.variables.insert(assignment.name.clone(), value);
match &assignment.value {
AssignmentValue::Scalar(word) => {
let value = self.expand_word(word).await?;
if let Some(index_str) = &assignment.index {
// arr[index]=value - set array element
let index: usize =
self.evaluate_arithmetic(index_str).try_into().unwrap_or(0);
let arr = self.arrays.entry(assignment.name.clone()).or_default();
arr.insert(index, value);
} else {
self.variables.insert(assignment.name.clone(), value);
}
}
AssignmentValue::Array(words) => {
// arr=(a b c) - set whole array
let mut arr = HashMap::new();
for (i, word) in words.iter().enumerate() {
let value = self.expand_word(word).await?;
arr.insert(i, value);
}
self.arrays.insert(assignment.name.clone(), arr);
}
}
}

let name = self.expand_word(&command.name).await?;
Expand Down Expand Up @@ -793,6 +817,36 @@ impl Interpreter {
let expanded = self.apply_parameter_op(&value, name, operator, operand);
result.push_str(&expanded);
}
WordPart::ArrayAccess { name, index } => {
if index == "@" || index == "*" {
// ${arr[@]} or ${arr[*]} - expand to all elements
if let Some(arr) = self.arrays.get(name) {
let mut indices: Vec<_> = arr.keys().collect();
indices.sort();
let values: Vec<_> =
indices.iter().filter_map(|i| arr.get(i)).collect();
result.push_str(
&values.into_iter().cloned().collect::<Vec<_>>().join(" "),
);
}
} else {
// ${arr[n]} - get specific element
let idx: usize = self.evaluate_arithmetic(index).try_into().unwrap_or(0);
if let Some(arr) = self.arrays.get(name) {
if let Some(value) = arr.get(&idx) {
result.push_str(value);
}
}
}
}
WordPart::ArrayLength(name) => {
// ${#arr[@]} - number of elements
if let Some(arr) = self.arrays.get(name) {
result.push_str(&arr.len().to_string());
} else {
result.push('0');
}
}
}
}

Expand Down
38 changes: 38 additions & 0 deletions crates/bashkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,4 +745,42 @@ mod tests {
let result = bash.exec("X=file.tar.gz; echo ${X%.*}").await.unwrap();
assert_eq!(result.stdout, "file.tar\n");
}

#[tokio::test]
async fn test_array_basic() {
let mut bash = Bash::new();
// Basic array declaration and access
let result = bash.exec("arr=(a b c); echo ${arr[1]}").await.unwrap();
assert_eq!(result.stdout, "b\n");
}

#[tokio::test]
async fn test_array_all_elements() {
let mut bash = Bash::new();
// ${arr[@]} - all elements
let result = bash
.exec("arr=(one two three); echo ${arr[@]}")
.await
.unwrap();
assert_eq!(result.stdout, "one two three\n");
}

#[tokio::test]
async fn test_array_length() {
let mut bash = Bash::new();
// ${#arr[@]} - number of elements
let result = bash.exec("arr=(a b c d e); echo ${#arr[@]}").await.unwrap();
assert_eq!(result.stdout, "5\n");
}

#[tokio::test]
async fn test_array_indexed_assignment() {
let mut bash = Bash::new();
// arr[n]=value assignment
let result = bash
.exec("arr[0]=first; arr[1]=second; echo ${arr[0]} ${arr[1]}")
.await
.unwrap();
assert_eq!(result.stdout, "first second\n");
}
}
19 changes: 18 additions & 1 deletion crates/bashkit/src/parser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ impl fmt::Display for Word {
write!(f, "${{{}{}{}}}", name, op_str, operand)?
}
WordPart::Length(name) => write!(f, "${{#{}}}", name)?,
WordPart::ArrayAccess { name, index } => write!(f, "${{{}[{}]}}", name, index)?,
WordPart::ArrayLength(name) => write!(f, "${{#{}[@]}}", name)?,
}
}
Ok(())
Expand All @@ -213,6 +215,10 @@ pub enum WordPart {
},
/// Length expansion ${#var}
Length(String),
/// Array element access ${arr[index]} or ${arr[@]} or ${arr[*]}
ArrayAccess { name: String, index: String },
/// Array length ${#arr[@]} or ${#arr[*]}
ArrayLength(String),
}

/// Parameter expansion operators
Expand Down Expand Up @@ -272,5 +278,16 @@ pub enum RedirectKind {
#[derive(Debug, Clone)]
pub struct Assignment {
pub name: String,
pub value: Word,
/// Optional array index for indexed assignments like arr[0]=value
pub index: Option<String>,
pub value: AssignmentValue,
}

/// Value in an assignment - scalar or array
#[derive(Debug, Clone)]
pub enum AssignmentValue {
/// Scalar value: VAR=value
Scalar(Word),
/// Array value: VAR=(a b c)
Array(Vec<Word>),
}
Loading