| 1 | use std::collections::{HashMap, HashSet}; |
| 2 | use std::env; |
| 3 | use std::rc::Rc; |
| 4 | |
| 5 | #[cfg(unix)] |
| 6 | use rush_job::JobList; |
| 7 | |
| 8 | #[cfg(unix)] |
| 9 | use nix::unistd::Pid; |
| 10 | |
| 11 | /// State for active coproc (coprocess) |
| 12 | #[cfg(unix)] |
| 13 | #[derive(Debug, Clone)] |
| 14 | pub struct CoprocState { |
| 15 | /// Coproc name (default is "COPROC") |
| 16 | pub name: String, |
| 17 | /// File descriptor for reading from coproc's stdout |
| 18 | pub read_fd: i32, |
| 19 | /// File descriptor for writing to coproc's stdin |
| 20 | pub write_fd: i32, |
| 21 | /// Process ID of the coproc |
| 22 | pub pid: Pid, |
| 23 | /// Process group ID of the coproc |
| 24 | pub pgid: Pid, |
| 25 | } |
| 26 | |
| 27 | /// Callback type for executing command substitution internally |
| 28 | /// Takes the command string and returns the stdout output |
| 29 | pub type CommandExecutor = Rc<dyn Fn(&str, &mut Context) -> Result<String, String>>; |
| 30 | |
| 31 | /// Wrapper for CommandExecutor that implements Debug |
| 32 | pub struct CommandExecutorWrapper(pub Option<CommandExecutor>); |
| 33 | |
| 34 | impl std::fmt::Debug for CommandExecutorWrapper { |
| 35 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 36 | match &self.0 { |
| 37 | Some(_) => write!(f, "CommandExecutor(Some)"), |
| 38 | None => write!(f, "CommandExecutor(None)"), |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | /// Array types for distinguishing indexed vs associative |
| 44 | #[derive(Debug, Clone)] |
| 45 | pub enum ArrayType { |
| 46 | /// Indexed array: arr=(one two three), accessed by integer index |
| 47 | Indexed(Vec<String>), |
| 48 | /// Associative array: declare -A arr, accessed by string key |
| 49 | Associative(HashMap<String, String>), |
| 50 | } |
| 51 | |
| 52 | /// Shell options that can be set with the 'set' builtin |
| 53 | #[derive(Debug, Clone)] |
| 54 | pub struct ShellOptions { |
| 55 | /// Exit immediately if a command exits with a non-zero status (set -e) |
| 56 | pub errexit: bool, |
| 57 | /// Print commands before executing them (set -x) |
| 58 | pub xtrace: bool, |
| 59 | /// Treat unset variables as an error (set -u) |
| 60 | pub nounset: bool, |
| 61 | /// Pipeline fails if any command fails (not just the last) (set -o pipefail) |
| 62 | pub pipefail: bool, |
| 63 | /// Disable filename expansion (globbing) (set -f) |
| 64 | pub noglob: bool, |
| 65 | /// Globs that match nothing expand to nothing (shopt -s nullglob) |
| 66 | pub nullglob: bool, |
| 67 | /// Include dotfiles in glob expansion (shopt -s dotglob) |
| 68 | pub dotglob: bool, |
| 69 | /// Extended glob patterns (shopt -s extglob) |
| 70 | pub extglob: bool, |
| 71 | } |
| 72 | |
| 73 | impl Default for ShellOptions { |
| 74 | fn default() -> Self { |
| 75 | Self { |
| 76 | errexit: false, |
| 77 | xtrace: false, |
| 78 | nounset: false, |
| 79 | pipefail: false, |
| 80 | noglob: false, |
| 81 | nullglob: false, |
| 82 | dotglob: false, |
| 83 | extglob: false, |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | /// Execution context holding shell variables and state |
| 89 | #[derive(Debug)] |
| 90 | pub struct Context { |
| 91 | /// Shell variables (local and environment) |
| 92 | variables: HashMap<String, String>, |
| 93 | /// Variables that should be exported to child processes |
| 94 | exported: HashMap<String, String>, |
| 95 | /// Local variable scopes (stack of scopes, innermost first) |
| 96 | /// Each scope contains variables declared with 'local' in that function |
| 97 | local_scopes: Vec<HashMap<String, String>>, |
| 98 | /// Exit status of last command |
| 99 | pub last_exit_status: i32, |
| 100 | /// Flag indicating the shell should exit (set by `exit` builtin) |
| 101 | pub exit_requested: Option<i32>, |
| 102 | /// Shell functions (name -> body) |
| 103 | pub functions: HashMap<String, rush_parser::ast::FunctionDef>, |
| 104 | /// Arrays (name -> indexed or associative) |
| 105 | pub arrays: HashMap<String, ArrayType>, |
| 106 | /// Set of array names that are declared as associative (for type checking) |
| 107 | pub associative_array_names: HashSet<String>, |
| 108 | /// Command aliases (name -> expansion) |
| 109 | pub aliases: HashMap<String, String>, |
| 110 | /// Signal traps (signal name/number -> command) |
| 111 | pub traps: HashMap<String, String>, |
| 112 | /// Shell options (set -e, set -x, etc.) |
| 113 | pub options: ShellOptions, |
| 114 | /// Read-only variables |
| 115 | readonly_vars: HashSet<String>, |
| 116 | /// Positional parameters ($1, $2, $3, ...) |
| 117 | pub positional_params: Vec<String>, |
| 118 | /// Command hash table (command name -> full path) |
| 119 | pub command_hash: HashMap<String, String>, |
| 120 | /// OPTIND for getopts (current option index) |
| 121 | pub optind: usize, |
| 122 | /// Job list for job control (unix only) |
| 123 | #[cfg(unix)] |
| 124 | pub job_list: JobList, |
| 125 | /// Active coproc state (unix only, single coproc at a time) |
| 126 | #[cfg(unix)] |
| 127 | pub coproc: Option<CoprocState>, |
| 128 | /// Internal command executor (for command substitution) |
| 129 | /// If set, command substitution will use this instead of sh -c |
| 130 | pub command_executor: CommandExecutorWrapper, |
| 131 | /// Directory stack for pushd/popd |
| 132 | pub dir_stack: Vec<String>, |
| 133 | } |
| 134 | |
| 135 | impl Context { |
| 136 | /// Create a new context with environment variables |
| 137 | pub fn new() -> Self { |
| 138 | let mut context = Self { |
| 139 | variables: HashMap::new(), |
| 140 | exported: HashMap::new(), |
| 141 | local_scopes: Vec::new(), |
| 142 | last_exit_status: 0, |
| 143 | exit_requested: None, |
| 144 | functions: HashMap::new(), |
| 145 | arrays: HashMap::new(), |
| 146 | associative_array_names: HashSet::new(), |
| 147 | aliases: HashMap::new(), |
| 148 | traps: HashMap::new(), |
| 149 | options: ShellOptions::default(), |
| 150 | readonly_vars: HashSet::new(), |
| 151 | positional_params: Vec::new(), |
| 152 | command_hash: HashMap::new(), |
| 153 | optind: 1, |
| 154 | #[cfg(unix)] |
| 155 | job_list: JobList::new(nix::unistd::getpgrp()), |
| 156 | #[cfg(unix)] |
| 157 | coproc: None, |
| 158 | command_executor: CommandExecutorWrapper(None), |
| 159 | dir_stack: Vec::new(), |
| 160 | }; |
| 161 | |
| 162 | // Initialize with environment variables |
| 163 | for (key, value) in env::vars() { |
| 164 | context.variables.insert(key.clone(), value.clone()); |
| 165 | context.exported.insert(key, value); |
| 166 | } |
| 167 | |
| 168 | context |
| 169 | } |
| 170 | |
| 171 | /// Create a new empty context (for testing) |
| 172 | pub fn empty() -> Self { |
| 173 | Self { |
| 174 | variables: HashMap::new(), |
| 175 | exported: HashMap::new(), |
| 176 | local_scopes: Vec::new(), |
| 177 | last_exit_status: 0, |
| 178 | exit_requested: None, |
| 179 | functions: HashMap::new(), |
| 180 | arrays: HashMap::new(), |
| 181 | associative_array_names: HashSet::new(), |
| 182 | aliases: HashMap::new(), |
| 183 | traps: HashMap::new(), |
| 184 | options: ShellOptions::default(), |
| 185 | readonly_vars: HashSet::new(), |
| 186 | positional_params: Vec::new(), |
| 187 | command_hash: HashMap::new(), |
| 188 | optind: 1, |
| 189 | #[cfg(unix)] |
| 190 | job_list: JobList::new(nix::unistd::getpgrp()), |
| 191 | #[cfg(unix)] |
| 192 | coproc: None, |
| 193 | command_executor: CommandExecutorWrapper(None), |
| 194 | dir_stack: Vec::new(), |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | /// Set a variable (local to this shell) |
| 199 | /// Returns Ok(()) on success, Err with variable name if readonly |
| 200 | pub fn set_var(&mut self, name: impl Into<String>, value: impl Into<String>) -> Result<(), String> { |
| 201 | let name = name.into(); |
| 202 | |
| 203 | // Check if readonly |
| 204 | if self.is_readonly(&name) { |
| 205 | return Err(name); |
| 206 | } |
| 207 | |
| 208 | let value = value.into(); |
| 209 | self.variables.insert(name, value); |
| 210 | Ok(()) |
| 211 | } |
| 212 | |
| 213 | /// Get a variable value |
| 214 | /// Searches local scopes first (innermost to outermost), then global variables |
| 215 | pub fn get_var(&self, name: &str) -> Option<&str> { |
| 216 | // Search local scopes from innermost (most recent function) to outermost |
| 217 | for scope in self.local_scopes.iter().rev() { |
| 218 | if let Some(value) = scope.get(name) { |
| 219 | return Some(value.as_str()); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | // Not found in local scopes, check global variables |
| 224 | self.variables.get(name).map(|s| s.as_str()) |
| 225 | } |
| 226 | |
| 227 | /// Export a variable (make it available to child processes) |
| 228 | pub fn export_var(&mut self, name: impl Into<String>, value: impl Into<String>) { |
| 229 | let name = name.into(); |
| 230 | let value = value.into(); |
| 231 | self.variables.insert(name.clone(), value.clone()); |
| 232 | self.exported.insert(name, value); |
| 233 | } |
| 234 | |
| 235 | /// Check if a variable is exported |
| 236 | pub fn is_exported(&self, name: &str) -> bool { |
| 237 | self.exported.contains_key(name) |
| 238 | } |
| 239 | |
| 240 | /// Get all exported variables (for passing to child processes) |
| 241 | pub fn exported_vars(&self) -> &HashMap<String, String> { |
| 242 | &self.exported |
| 243 | } |
| 244 | |
| 245 | /// Get all variables (including non-exported) |
| 246 | pub fn all_vars(&self) -> &HashMap<String, String> { |
| 247 | &self.variables |
| 248 | } |
| 249 | |
| 250 | /// Remove a variable |
| 251 | pub fn unset_var(&mut self, name: &str) { |
| 252 | self.variables.remove(name); |
| 253 | self.exported.remove(name); |
| 254 | } |
| 255 | |
| 256 | /// Unexport a variable (remove from exported but keep in variables) |
| 257 | pub fn unexport_var(&mut self, name: &str) { |
| 258 | self.exported.remove(name); |
| 259 | } |
| 260 | |
| 261 | /// Mark a variable as readonly |
| 262 | pub fn mark_readonly(&mut self, name: impl Into<String>) { |
| 263 | self.readonly_vars.insert(name.into()); |
| 264 | } |
| 265 | |
| 266 | /// Check if a variable is readonly |
| 267 | pub fn is_readonly(&self, name: &str) -> bool { |
| 268 | self.readonly_vars.contains(name) |
| 269 | } |
| 270 | |
| 271 | /// Get all readonly variables |
| 272 | pub fn readonly_vars(&self) -> &HashSet<String> { |
| 273 | &self.readonly_vars |
| 274 | } |
| 275 | |
| 276 | /// Push a new local scope (when entering a function) |
| 277 | pub fn push_scope(&mut self) { |
| 278 | self.local_scopes.push(HashMap::new()); |
| 279 | } |
| 280 | |
| 281 | /// Pop the current local scope (when exiting a function) |
| 282 | /// Returns the popped scope |
| 283 | pub fn pop_scope(&mut self) -> Option<HashMap<String, String>> { |
| 284 | self.local_scopes.pop() |
| 285 | } |
| 286 | |
| 287 | /// Set a local variable in the current function scope |
| 288 | /// If not in a function (no scopes), sets as global variable |
| 289 | /// Returns Ok(()) on success, Err with variable name if readonly |
| 290 | pub fn set_local_var(&mut self, name: impl Into<String>, value: impl Into<String>) -> Result<(), String> { |
| 291 | let name = name.into(); |
| 292 | |
| 293 | // Check if readonly |
| 294 | if self.is_readonly(&name) { |
| 295 | return Err(name); |
| 296 | } |
| 297 | |
| 298 | let value = value.into(); |
| 299 | |
| 300 | // If we're in a function (have local scopes), set in current scope |
| 301 | if let Some(current_scope) = self.local_scopes.last_mut() { |
| 302 | current_scope.insert(name, value); |
| 303 | } else { |
| 304 | // Not in a function, set as global |
| 305 | self.variables.insert(name, value); |
| 306 | } |
| 307 | |
| 308 | Ok(()) |
| 309 | } |
| 310 | |
| 311 | /// Update the exit status |
| 312 | pub fn set_exit_status(&mut self, status: i32) { |
| 313 | self.last_exit_status = status; |
| 314 | } |
| 315 | |
| 316 | /// Get the exit status |
| 317 | pub fn exit_status(&self) -> i32 { |
| 318 | self.last_exit_status |
| 319 | } |
| 320 | |
| 321 | /// Check if an array is associative |
| 322 | pub fn is_associative_array(&self, name: &str) -> bool { |
| 323 | self.associative_array_names.contains(name) |
| 324 | } |
| 325 | |
| 326 | /// Get indexed array value by integer index |
| 327 | pub fn get_indexed_array(&self, name: &str, index: usize) -> Option<&String> { |
| 328 | match self.arrays.get(name) { |
| 329 | Some(ArrayType::Indexed(vec)) => vec.get(index), |
| 330 | _ => None, |
| 331 | } |
| 332 | } |
| 333 | |
| 334 | /// Get associative array value by string key |
| 335 | pub fn get_assoc_array(&self, name: &str, key: &str) -> Option<&String> { |
| 336 | match self.arrays.get(name) { |
| 337 | Some(ArrayType::Associative(map)) => map.get(key), |
| 338 | _ => None, |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | /// Get array length (works for both indexed and associative) |
| 343 | pub fn get_array_len(&self, name: &str) -> usize { |
| 344 | match self.arrays.get(name) { |
| 345 | Some(ArrayType::Indexed(vec)) => vec.len(), |
| 346 | Some(ArrayType::Associative(map)) => map.len(), |
| 347 | None => 0, |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | /// Set array element (auto-detects type based on whether array is associative) |
| 352 | /// For indexed arrays, parses key as integer index |
| 353 | /// For associative arrays, uses key as-is |
| 354 | /// Creates new indexed array if name doesn't exist and key is numeric |
| 355 | pub fn set_array_element(&mut self, name: &str, key: String, value: String) -> Result<(), String> { |
| 356 | if self.is_associative_array(name) { |
| 357 | // Associative array - use key as-is |
| 358 | match self.arrays.get_mut(name) { |
| 359 | Some(ArrayType::Associative(map)) => { |
| 360 | map.insert(key, value); |
| 361 | Ok(()) |
| 362 | } |
| 363 | Some(ArrayType::Indexed(_)) => { |
| 364 | Err(format!("{}: cannot use string index on indexed array", name)) |
| 365 | } |
| 366 | None => { |
| 367 | // Create new associative array |
| 368 | let mut map = HashMap::new(); |
| 369 | map.insert(key, value); |
| 370 | self.arrays.insert(name.to_string(), ArrayType::Associative(map)); |
| 371 | self.associative_array_names.insert(name.to_string()); |
| 372 | Ok(()) |
| 373 | } |
| 374 | } |
| 375 | } else { |
| 376 | // Indexed array - parse key as integer |
| 377 | let index = key.parse::<usize>() |
| 378 | .map_err(|_| format!("{}: bad array subscript", key))?; |
| 379 | |
| 380 | match self.arrays.get_mut(name) { |
| 381 | Some(ArrayType::Indexed(vec)) => { |
| 382 | // Extend vector if needed |
| 383 | if index >= vec.len() { |
| 384 | vec.resize(index + 1, String::new()); |
| 385 | } |
| 386 | vec[index] = value; |
| 387 | Ok(()) |
| 388 | } |
| 389 | Some(ArrayType::Associative(_)) => { |
| 390 | Err(format!("{}: cannot use integer index on associative array", name)) |
| 391 | } |
| 392 | None => { |
| 393 | // Create new indexed array |
| 394 | let mut vec = vec![String::new(); index + 1]; |
| 395 | vec[index] = value; |
| 396 | self.arrays.insert(name.to_string(), ArrayType::Indexed(vec)); |
| 397 | Ok(()) |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | /// Create an associative array |
| 404 | pub fn create_assoc_array(&mut self, name: String) { |
| 405 | self.arrays.insert(name.clone(), ArrayType::Associative(HashMap::new())); |
| 406 | self.associative_array_names.insert(name); |
| 407 | } |
| 408 | |
| 409 | /// Create an indexed array |
| 410 | pub fn create_indexed_array(&mut self, name: String) { |
| 411 | self.arrays.insert(name, ArrayType::Indexed(Vec::new())); |
| 412 | } |
| 413 | |
| 414 | /// Set coproc file descriptors as array variables |
| 415 | /// Creates array with indices 0 (read fd) and 1 (write fd) |
| 416 | #[cfg(unix)] |
| 417 | pub fn set_coproc_vars(&mut self, name: &str, read_fd: i32, write_fd: i32) { |
| 418 | let fds = vec![read_fd.to_string(), write_fd.to_string()]; |
| 419 | self.arrays.insert(name.to_string(), ArrayType::Indexed(fds)); |
| 420 | } |
| 421 | |
| 422 | /// Clear coproc variables and close file descriptors |
| 423 | #[cfg(unix)] |
| 424 | pub fn clear_coproc(&mut self) { |
| 425 | if let Some(coproc_state) = self.coproc.take() { |
| 426 | // Close file descriptors |
| 427 | unsafe { |
| 428 | nix::libc::close(coproc_state.read_fd); |
| 429 | nix::libc::close(coproc_state.write_fd); |
| 430 | } |
| 431 | // Remove array variable |
| 432 | self.arrays.remove(&coproc_state.name); |
| 433 | } |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | impl Default for Context { |
| 438 | fn default() -> Self { |
| 439 | Self::new() |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | #[cfg(test)] |
| 444 | mod tests { |
| 445 | use super::*; |
| 446 | |
| 447 | #[test] |
| 448 | fn test_set_and_get_var() { |
| 449 | let mut ctx = Context::empty(); |
| 450 | ctx.set_var("FOO", "bar").unwrap(); |
| 451 | assert_eq!(ctx.get_var("FOO"), Some("bar")); |
| 452 | } |
| 453 | |
| 454 | #[test] |
| 455 | fn test_export_var() { |
| 456 | let mut ctx = Context::empty(); |
| 457 | ctx.export_var("PATH", "/usr/bin"); |
| 458 | assert_eq!(ctx.get_var("PATH"), Some("/usr/bin")); |
| 459 | assert!(ctx.is_exported("PATH")); |
| 460 | assert_eq!(ctx.exported_vars().get("PATH"), Some(&"/usr/bin".to_string())); |
| 461 | } |
| 462 | |
| 463 | #[test] |
| 464 | fn test_unset_var() { |
| 465 | let mut ctx = Context::empty(); |
| 466 | ctx.set_var("FOO", "bar").unwrap(); |
| 467 | assert_eq!(ctx.get_var("FOO"), Some("bar")); |
| 468 | ctx.unset_var("FOO"); |
| 469 | assert_eq!(ctx.get_var("FOO"), None); |
| 470 | } |
| 471 | |
| 472 | #[test] |
| 473 | fn test_exit_status() { |
| 474 | let mut ctx = Context::empty(); |
| 475 | assert_eq!(ctx.exit_status(), 0); |
| 476 | ctx.set_exit_status(42); |
| 477 | assert_eq!(ctx.exit_status(), 42); |
| 478 | } |
| 479 | } |
| 480 |