Rust · 16230 bytes Raw Blame History
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