Skip to main content

rust_bash/interpreter/
mod.rs

1//! Interpreter engine: parsing, AST walking, and execution state.
2
3pub(crate) mod arithmetic;
4pub(crate) mod brace;
5pub(crate) mod builtins;
6mod expansion;
7pub(crate) mod pattern;
8mod walker;
9
10use crate::commands::VirtualCommand;
11use crate::error::RustBashError;
12use crate::network::NetworkPolicy;
13use crate::platform::Instant;
14use crate::vfs::VirtualFs;
15use bitflags::bitflags;
16use brush_parser::ast;
17use std::collections::{BTreeMap, HashMap};
18use std::sync::Arc;
19use std::time::Duration;
20
21pub use builtins::builtin_names;
22pub use expansion::expand_word;
23pub use walker::execute_program;
24
25// ── Core types ───────────────────────────────────────────────────────
26
27/// Signal for loop control flow (`break`, `continue`) and function return.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ControlFlow {
30    Break(usize),
31    Continue(usize),
32    Return(i32),
33}
34
35/// Result of executing a shell command.
36#[derive(Debug, Clone, Default, PartialEq, Eq)]
37pub struct ExecResult {
38    pub stdout: String,
39    pub stderr: String,
40    pub exit_code: i32,
41    /// Binary output for commands that produce non-text data.
42    pub stdout_bytes: Option<Vec<u8>>,
43}
44
45// ── Variable types ──────────────────────────────────────────────────
46
47/// The value stored in a shell variable: scalar, indexed array, or associative array.
48#[derive(Debug, Clone, PartialEq)]
49pub enum VariableValue {
50    Scalar(String),
51    IndexedArray(BTreeMap<usize, String>),
52    AssociativeArray(BTreeMap<String, String>),
53}
54
55impl VariableValue {
56    /// Return the scalar value, or element \[0\] for indexed arrays,
57    /// or empty string for associative arrays (matches bash behavior).
58    pub fn as_scalar(&self) -> &str {
59        match self {
60            VariableValue::Scalar(s) => s,
61            VariableValue::IndexedArray(map) => map.get(&0).map(|s| s.as_str()).unwrap_or(""),
62            VariableValue::AssociativeArray(map) => map.get("0").map(|s| s.as_str()).unwrap_or(""),
63        }
64    }
65
66    /// Return element count for arrays, or 1 for non-empty scalars.
67    pub fn count(&self) -> usize {
68        match self {
69            VariableValue::Scalar(s) => usize::from(!s.is_empty()),
70            VariableValue::IndexedArray(map) => map.len(),
71            VariableValue::AssociativeArray(map) => map.len(),
72        }
73    }
74}
75
76bitflags! {
77    /// Attribute flags for a shell variable.
78    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
79    pub struct VariableAttrs: u8 {
80        const EXPORTED  = 0b0000_0001;
81        const READONLY  = 0b0000_0010;
82        const INTEGER   = 0b0000_0100;
83        const LOWERCASE = 0b0000_1000;
84        const UPPERCASE = 0b0001_0000;
85        const NAMEREF   = 0b0010_0000;
86    }
87}
88
89/// A shell variable with metadata.
90#[derive(Debug, Clone)]
91pub struct Variable {
92    pub value: VariableValue,
93    pub attrs: VariableAttrs,
94}
95
96/// A persistent file descriptor redirection established by `exec`.
97#[derive(Debug, Clone)]
98pub(crate) enum PersistentFd {
99    /// FD writes to this VFS path.
100    OutputFile(String),
101    /// FD reads from this VFS path.
102    InputFile(String),
103    /// FD is open for both reading and writing on this VFS path.
104    ReadWriteFile(String),
105    /// FD points to /dev/null (reads empty, writes discarded).
106    DevNull,
107    /// FD is closed.
108    Closed,
109    /// FD is a duplicate of a standard fd (0=stdin, 1=stdout, 2=stderr).
110    DupStdFd(i32),
111}
112
113impl Variable {
114    /// Convenience: is this variable exported?
115    pub fn exported(&self) -> bool {
116        self.attrs.contains(VariableAttrs::EXPORTED)
117    }
118
119    /// Convenience: is this variable readonly?
120    pub fn readonly(&self) -> bool {
121        self.attrs.contains(VariableAttrs::READONLY)
122    }
123}
124
125/// Execution limits.
126#[derive(Debug, Clone)]
127pub struct ExecutionLimits {
128    pub max_call_depth: usize,
129    pub max_command_count: usize,
130    pub max_loop_iterations: usize,
131    pub max_execution_time: Duration,
132    pub max_output_size: usize,
133    pub max_string_length: usize,
134    pub max_glob_results: usize,
135    pub max_substitution_depth: usize,
136    pub max_heredoc_size: usize,
137    pub max_brace_expansion: usize,
138    pub max_array_elements: usize,
139}
140
141impl Default for ExecutionLimits {
142    fn default() -> Self {
143        Self {
144            max_call_depth: 50,
145            max_command_count: 10_000,
146            max_loop_iterations: 10_000,
147            max_execution_time: Duration::from_secs(30),
148            max_output_size: 10 * 1024 * 1024,
149            max_string_length: 10 * 1024 * 1024,
150            max_glob_results: 100_000,
151            max_substitution_depth: 50,
152            max_heredoc_size: 10 * 1024 * 1024,
153            max_brace_expansion: 10_000,
154            max_array_elements: 100_000,
155        }
156    }
157}
158
159/// Execution counters, reset per `exec()` call.
160#[derive(Debug, Clone)]
161pub struct ExecutionCounters {
162    pub command_count: usize,
163    pub call_depth: usize,
164    pub output_size: usize,
165    pub start_time: Instant,
166    pub substitution_depth: usize,
167}
168
169impl Default for ExecutionCounters {
170    fn default() -> Self {
171        Self {
172            command_count: 0,
173            call_depth: 0,
174            output_size: 0,
175            start_time: Instant::now(),
176            substitution_depth: 0,
177        }
178    }
179}
180
181impl ExecutionCounters {
182    pub fn reset(&mut self) {
183        *self = Self::default();
184    }
185}
186
187/// Shell options controlled by `set -o` / `set +o` and single-letter flags.
188#[derive(Debug, Clone, Default)]
189pub struct ShellOpts {
190    pub errexit: bool,
191    pub nounset: bool,
192    pub pipefail: bool,
193    pub xtrace: bool,
194    pub verbose: bool,
195    pub noexec: bool,
196    pub noclobber: bool,
197    pub allexport: bool,
198    pub noglob: bool,
199    pub posix: bool,
200    pub vi_mode: bool,
201    pub emacs_mode: bool,
202}
203
204/// Shopt options (`shopt -s`/`-u` flags).
205#[derive(Debug, Clone)]
206pub struct ShoptOpts {
207    pub nullglob: bool,
208    pub globstar: bool,
209    pub dotglob: bool,
210    pub globskipdots: bool,
211    pub failglob: bool,
212    pub nocaseglob: bool,
213    pub nocasematch: bool,
214    pub lastpipe: bool,
215    pub expand_aliases: bool,
216    pub xpg_echo: bool,
217    pub extglob: bool,
218    pub progcomp: bool,
219    pub hostcomplete: bool,
220    pub complete_fullquote: bool,
221    pub sourcepath: bool,
222    pub promptvars: bool,
223    pub interactive_comments: bool,
224    pub cmdhist: bool,
225    pub lithist: bool,
226    pub autocd: bool,
227    pub cdspell: bool,
228    pub dirspell: bool,
229    pub direxpand: bool,
230    pub checkhash: bool,
231    pub checkjobs: bool,
232    pub checkwinsize: bool,
233    pub extquote: bool,
234    pub force_fignore: bool,
235    pub globasciiranges: bool,
236    pub gnu_errfmt: bool,
237    pub histappend: bool,
238    pub histreedit: bool,
239    pub histverify: bool,
240    pub huponexit: bool,
241    pub inherit_errexit: bool,
242    pub login_shell: bool,
243    pub mailwarn: bool,
244    pub no_empty_cmd_completion: bool,
245    pub progcomp_alias: bool,
246    pub shift_verbose: bool,
247    pub execfail: bool,
248    pub cdable_vars: bool,
249    pub localvar_inherit: bool,
250    pub localvar_unset: bool,
251    pub extdebug: bool,
252    pub patsub_replacement: bool,
253    pub assoc_expand_once: bool,
254    pub varredir_close: bool,
255}
256
257impl Default for ShoptOpts {
258    fn default() -> Self {
259        Self {
260            nullglob: false,
261            globstar: false,
262            dotglob: false,
263            globskipdots: true,
264            failglob: false,
265            nocaseglob: false,
266            nocasematch: false,
267            lastpipe: false,
268            expand_aliases: false,
269            xpg_echo: false,
270            extglob: true,
271            progcomp: true,
272            hostcomplete: true,
273            complete_fullquote: true,
274            sourcepath: true,
275            promptvars: true,
276            interactive_comments: true,
277            cmdhist: true,
278            lithist: false,
279            autocd: false,
280            cdspell: false,
281            dirspell: false,
282            direxpand: false,
283            checkhash: false,
284            checkjobs: false,
285            checkwinsize: true,
286            extquote: true,
287            force_fignore: true,
288            globasciiranges: true,
289            gnu_errfmt: false,
290            histappend: false,
291            histreedit: false,
292            histverify: false,
293            huponexit: false,
294            inherit_errexit: false,
295            login_shell: false,
296            mailwarn: false,
297            no_empty_cmd_completion: false,
298            progcomp_alias: false,
299            shift_verbose: false,
300            execfail: false,
301            cdable_vars: false,
302            localvar_inherit: false,
303            localvar_unset: false,
304            extdebug: false,
305            patsub_replacement: true,
306            assoc_expand_once: false,
307            varredir_close: false,
308        }
309    }
310}
311
312/// Stub for function definitions (execution in a future phase).
313#[derive(Debug, Clone)]
314pub struct FunctionDef {
315    pub body: ast::FunctionBody,
316}
317
318/// A single frame on the function call stack, used to expose
319/// `FUNCNAME`, `BASH_SOURCE`, and `BASH_LINENO` arrays.
320#[derive(Debug, Clone)]
321pub struct CallFrame {
322    pub func_name: String,
323    pub source: String,
324    pub lineno: usize,
325}
326
327/// The interpreter's mutable state, persistent across `exec()` calls.
328pub struct InterpreterState {
329    pub fs: Arc<dyn VirtualFs>,
330    pub env: HashMap<String, Variable>,
331    pub cwd: String,
332    pub functions: HashMap<String, FunctionDef>,
333    pub last_exit_code: i32,
334    pub commands: HashMap<String, Arc<dyn VirtualCommand>>,
335    pub shell_opts: ShellOpts,
336    pub shopt_opts: ShoptOpts,
337    pub limits: ExecutionLimits,
338    pub counters: ExecutionCounters,
339    pub network_policy: NetworkPolicy,
340    pub(crate) should_exit: bool,
341    pub(crate) loop_depth: usize,
342    pub(crate) control_flow: Option<ControlFlow>,
343    pub positional_params: Vec<String>,
344    pub shell_name: String,
345    /// Simple PRNG state for $RANDOM.
346    pub(crate) random_seed: u32,
347    /// Stack of restore maps for `local` variable scoping in functions.
348    pub(crate) local_scopes: Vec<HashMap<String, Option<Variable>>>,
349    /// How many function calls deep we are (for `local`/`return` validation).
350    pub(crate) in_function_depth: usize,
351    /// Registered trap handlers: signal/event name → command string.
352    pub(crate) traps: HashMap<String, String>,
353    /// True while executing a trap handler (prevents recursive re-trigger).
354    pub(crate) in_trap: bool,
355    /// Nesting depth for contexts where `set -e` should NOT trigger an exit.
356    /// Incremented when entering if/while/until conditions, `&&`/`||` left sides, or `!` pipelines.
357    pub(crate) errexit_suppressed: usize,
358    /// Byte offset into the current stdin stream, used by `read` to consume
359    /// successive lines from piped input across loop iterations.
360    pub(crate) stdin_offset: usize,
361    /// Directory stack for `pushd`/`popd`/`dirs`.
362    pub(crate) dir_stack: Vec<String>,
363    /// Cached command-name → resolved-path mappings for `hash`.
364    pub(crate) command_hash: HashMap<String, String>,
365    /// Alias name → expansion string for `alias`/`unalias`.
366    pub(crate) aliases: HashMap<String, String>,
367    /// Current line number, updated per-statement from AST source positions.
368    pub(crate) current_lineno: usize,
369    /// Shell start time for `$SECONDS`.
370    pub(crate) shell_start_time: Instant,
371    /// Last argument of the previous simple command (`$_`).
372    pub(crate) last_argument: String,
373    /// Function call stack for `FUNCNAME`, `BASH_SOURCE`, `BASH_LINENO`.
374    pub(crate) call_stack: Vec<CallFrame>,
375    /// Configurable `$MACHTYPE` value.
376    pub(crate) machtype: String,
377    /// Configurable `$HOSTTYPE` value.
378    pub(crate) hosttype: String,
379    /// Persistent FD redirections set by `exec` (e.g. `exec > file`).
380    pub(crate) persistent_fds: HashMap<i32, PersistentFd>,
381    /// Next auto-allocated FD number for `{varname}>file` syntax.
382    pub(crate) next_auto_fd: i32,
383    /// Counter for generating unique process substitution temp file names.
384    pub(crate) proc_sub_counter: u64,
385    /// Pre-allocated temp file paths for redirect process substitutions, keyed by
386    /// the pointer address of the `IoFileRedirectTarget` AST node.  This ensures
387    /// each redirect resolves to its own pre-allocated path regardless of the order
388    /// in which `get_stdin_from_redirects` / `apply_output_redirects` visit them.
389    pub(crate) proc_sub_prealloc: HashMap<usize, String>,
390    /// Binary data from the previous pipeline stage, set by `execute_pipeline()`
391    /// and consumed by `dispatch_command()` to populate `CommandContext::stdin_bytes`.
392    pub(crate) pipe_stdin_bytes: Option<Vec<u8>>,
393    /// Stderr accumulated from command substitutions during word expansion.
394    /// Drained by the enclosing command execution into its `ExecResult.stderr`.
395    pub(crate) pending_cmdsub_stderr: String,
396}
397
398// ── Parsing ──────────────────────────────────────────────────────────
399
400pub(crate) fn parser_options() -> brush_parser::ParserOptions {
401    brush_parser::ParserOptions {
402        sh_mode: false,
403        posix_mode: false,
404        enable_extended_globbing: true,
405        tilde_expansion: true,
406    }
407}
408
409/// Parse a shell input string into an AST.
410pub fn parse(input: &str) -> Result<ast::Program, RustBashError> {
411    let tokens =
412        brush_parser::tokenize_str(input).map_err(|e| RustBashError::Parse(e.to_string()))?;
413
414    if tokens.is_empty() {
415        return Ok(ast::Program {
416            complete_commands: vec![],
417        });
418    }
419
420    let options = parser_options();
421    let source_info = brush_parser::SourceInfo {
422        source: input.to_string(),
423    };
424
425    brush_parser::parse_tokens(&tokens, &options, &source_info)
426        .map_err(|e| RustBashError::Parse(e.to_string()))
427}
428
429/// Set a variable in the interpreter state, respecting readonly, nameref,
430/// and attribute transforms (INTEGER, LOWERCASE, UPPERCASE).
431pub(crate) fn set_variable(
432    state: &mut InterpreterState,
433    name: &str,
434    value: String,
435) -> Result<(), RustBashError> {
436    if value.len() > state.limits.max_string_length {
437        return Err(RustBashError::LimitExceeded {
438            limit_name: "max_string_length",
439            limit_value: state.limits.max_string_length,
440            actual_value: value.len(),
441        });
442    }
443
444    // Resolve nameref chain to find the actual target variable.
445    let target = resolve_nameref(name, state)?;
446
447    // If the resolved target is an array subscript (e.g. from a nameref to "a[2]"),
448    // set the array element directly.
449    if let Some(bracket_pos) = target.find('[')
450        && target.ends_with(']')
451    {
452        let arr_name = &target[..bracket_pos];
453        let index_raw = &target[bracket_pos + 1..target.len() - 1];
454        // Expand variables and strip quotes from the index.
455        let word = brush_parser::ast::Word {
456            value: index_raw.to_string(),
457            loc: None,
458        };
459        let expanded_key = crate::interpreter::expansion::expand_word_to_string_mut(&word, state)?;
460
461        if let Some(var) = state.env.get(arr_name)
462            && var.readonly()
463        {
464            return Err(RustBashError::Execution(format!(
465                "{arr_name}: readonly variable"
466            )));
467        }
468
469        // Determine variable type and evaluate index before mutable borrow.
470        let is_assoc = state
471            .env
472            .get(arr_name)
473            .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
474        let numeric_idx = if !is_assoc {
475            crate::interpreter::arithmetic::eval_arithmetic(&expanded_key, state).unwrap_or(0)
476        } else {
477            0
478        };
479
480        match state.env.get_mut(arr_name) {
481            Some(var) => match &mut var.value {
482                VariableValue::AssociativeArray(map) => {
483                    map.insert(expanded_key, value);
484                }
485                VariableValue::IndexedArray(map) => {
486                    let actual_idx = if numeric_idx < 0 {
487                        let max_key = map.keys().next_back().copied().unwrap_or(0);
488                        let resolved = max_key as i64 + 1 + numeric_idx;
489                        if resolved < 0 {
490                            0usize
491                        } else {
492                            resolved as usize
493                        }
494                    } else {
495                        numeric_idx as usize
496                    };
497                    map.insert(actual_idx, value);
498                }
499                VariableValue::Scalar(s) => {
500                    if numeric_idx == 0 || numeric_idx == -1 {
501                        *s = value;
502                    }
503                }
504            },
505            None => {
506                // Create as indexed array with the element.
507                let idx = expanded_key.parse::<usize>().unwrap_or(0);
508                let mut map = std::collections::BTreeMap::new();
509                map.insert(idx, value);
510                state.env.insert(
511                    arr_name.to_string(),
512                    Variable {
513                        value: VariableValue::IndexedArray(map),
514                        attrs: VariableAttrs::empty(),
515                    },
516                );
517            }
518        }
519        return Ok(());
520    }
521
522    // SECONDS assignment resets the shell timer.
523    if target == "SECONDS" {
524        if let Ok(offset) = value.parse::<u64>() {
525            // `SECONDS=N` sets the timer so that $SECONDS reads as N right now.
526            // We achieve this by moving shell_start_time backwards by N seconds.
527            state.shell_start_time = Instant::now() - std::time::Duration::from_secs(offset);
528        } else {
529            state.shell_start_time = Instant::now();
530        }
531        return Ok(());
532    }
533
534    if let Some(var) = state.env.get(&target)
535        && var.readonly()
536    {
537        return Err(RustBashError::Execution(format!(
538            "{target}: readonly variable"
539        )));
540    }
541
542    // Get attributes of target for transforms.
543    let attrs = state
544        .env
545        .get(&target)
546        .map(|v| v.attrs)
547        .unwrap_or(VariableAttrs::empty());
548
549    // INTEGER: evaluate value as arithmetic expression.
550    let value = if attrs.contains(VariableAttrs::INTEGER) {
551        let result = crate::interpreter::arithmetic::eval_arithmetic(&value, state)?;
552        result.to_string()
553    } else {
554        value
555    };
556
557    // Case transforms (lowercase takes precedence if both set, but both shouldn't be).
558    let value = if attrs.contains(VariableAttrs::LOWERCASE) {
559        value.to_lowercase()
560    } else if attrs.contains(VariableAttrs::UPPERCASE) {
561        value.to_uppercase()
562    } else {
563        value
564    };
565
566    match state.env.get_mut(&target) {
567        Some(var) => {
568            match &mut var.value {
569                VariableValue::IndexedArray(map) => {
570                    map.insert(0, value);
571                }
572                VariableValue::AssociativeArray(map) => {
573                    map.insert("0".to_string(), value);
574                }
575                VariableValue::Scalar(s) => *s = value,
576            }
577            // allexport: auto-export on every assignment
578            if state.shell_opts.allexport {
579                var.attrs.insert(VariableAttrs::EXPORTED);
580            }
581        }
582        None => {
583            let attrs = if state.shell_opts.allexport {
584                VariableAttrs::EXPORTED
585            } else {
586                VariableAttrs::empty()
587            };
588            state.env.insert(
589                target,
590                Variable {
591                    value: VariableValue::Scalar(value),
592                    attrs,
593                },
594            );
595        }
596    }
597    Ok(())
598}
599
600/// Set an array element in the interpreter state, creating the array if needed.
601/// Resolves nameref before operating.
602pub(crate) fn set_array_element(
603    state: &mut InterpreterState,
604    name: &str,
605    index: usize,
606    value: String,
607) -> Result<(), RustBashError> {
608    let target = resolve_nameref(name, state)?;
609    if let Some(var) = state.env.get(&target)
610        && var.readonly()
611    {
612        return Err(RustBashError::Execution(format!(
613            "{target}: readonly variable"
614        )));
615    }
616
617    // Apply attribute transforms (INTEGER, LOWERCASE, UPPERCASE).
618    let attrs = state
619        .env
620        .get(&target)
621        .map(|v| v.attrs)
622        .unwrap_or(VariableAttrs::empty());
623    let value = if attrs.contains(VariableAttrs::INTEGER) {
624        crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
625    } else {
626        value
627    };
628    let value = if attrs.contains(VariableAttrs::LOWERCASE) {
629        value.to_lowercase()
630    } else if attrs.contains(VariableAttrs::UPPERCASE) {
631        value.to_uppercase()
632    } else {
633        value
634    };
635
636    let limit = state.limits.max_array_elements;
637    match state.env.get_mut(&target) {
638        Some(var) => match &mut var.value {
639            VariableValue::IndexedArray(map) => {
640                if !map.contains_key(&index) && map.len() >= limit {
641                    return Err(RustBashError::LimitExceeded {
642                        limit_name: "max_array_elements",
643                        limit_value: limit,
644                        actual_value: map.len() + 1,
645                    });
646                }
647                map.insert(index, value);
648            }
649            VariableValue::Scalar(_) => {
650                let mut map = BTreeMap::new();
651                map.insert(index, value);
652                var.value = VariableValue::IndexedArray(map);
653            }
654            VariableValue::AssociativeArray(_) => {
655                return Err(RustBashError::Execution(format!(
656                    "{target}: cannot use numeric index on associative array"
657                )));
658            }
659        },
660        None => {
661            let mut map = BTreeMap::new();
662            map.insert(index, value);
663            state.env.insert(
664                target,
665                Variable {
666                    value: VariableValue::IndexedArray(map),
667                    attrs: VariableAttrs::empty(),
668                },
669            );
670        }
671    }
672    Ok(())
673}
674
675/// Set an associative array element. Resolves nameref before operating.
676pub(crate) fn set_assoc_element(
677    state: &mut InterpreterState,
678    name: &str,
679    key: String,
680    value: String,
681) -> Result<(), RustBashError> {
682    let target = resolve_nameref(name, state)?;
683    if let Some(var) = state.env.get(&target)
684        && var.readonly()
685    {
686        return Err(RustBashError::Execution(format!(
687            "{target}: readonly variable"
688        )));
689    }
690
691    // Apply attribute transforms (INTEGER, LOWERCASE, UPPERCASE).
692    let attrs = state
693        .env
694        .get(&target)
695        .map(|v| v.attrs)
696        .unwrap_or(VariableAttrs::empty());
697    let value = if attrs.contains(VariableAttrs::INTEGER) {
698        crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
699    } else {
700        value
701    };
702    let value = if attrs.contains(VariableAttrs::LOWERCASE) {
703        value.to_lowercase()
704    } else if attrs.contains(VariableAttrs::UPPERCASE) {
705        value.to_uppercase()
706    } else {
707        value
708    };
709
710    let limit = state.limits.max_array_elements;
711    match state.env.get_mut(&target) {
712        Some(var) => match &mut var.value {
713            VariableValue::AssociativeArray(map) => {
714                if !map.contains_key(&key) && map.len() >= limit {
715                    return Err(RustBashError::LimitExceeded {
716                        limit_name: "max_array_elements",
717                        limit_value: limit,
718                        actual_value: map.len() + 1,
719                    });
720                }
721                map.insert(key, value);
722            }
723            _ => {
724                return Err(RustBashError::Execution(format!(
725                    "{target}: not an associative array"
726                )));
727            }
728        },
729        None => {
730            return Err(RustBashError::Execution(format!(
731                "{target}: not an associative array"
732            )));
733        }
734    }
735    Ok(())
736}
737
738/// Generate next pseudo-random number (xorshift32, range 0..32767).
739pub(crate) fn next_random(state: &mut InterpreterState) -> u16 {
740    let mut s = state.random_seed;
741    if s == 0 {
742        s = 12345;
743    }
744    s ^= s << 13;
745    s ^= s >> 17;
746    s ^= s << 5;
747    state.random_seed = s;
748    (s & 0x7FFF) as u16
749}
750
751/// Resolve a nameref chain: follow NAMEREF attributes until a non-nameref variable
752/// (or missing variable) is found. Returns the final target name.
753/// Errors on circular references (chain longer than 10).
754pub(crate) fn resolve_nameref(
755    name: &str,
756    state: &InterpreterState,
757) -> Result<String, RustBashError> {
758    let mut current = name.to_string();
759    for _ in 0..10 {
760        match state.env.get(&current) {
761            Some(var) if var.attrs.contains(VariableAttrs::NAMEREF) => {
762                current = var.value.as_scalar().to_string();
763            }
764            _ => return Ok(current),
765        }
766    }
767    Err(RustBashError::Execution(format!(
768        "{name}: circular name reference"
769    )))
770}
771
772/// Non-failing nameref resolution: returns the resolved name, or the original
773/// name if the chain is circular.
774pub(crate) fn resolve_nameref_or_self(name: &str, state: &InterpreterState) -> String {
775    resolve_nameref(name, state).unwrap_or_else(|_| name.to_string())
776}
777
778/// Execute a trap handler string, preventing recursive re-trigger of the same trap type.
779pub(crate) fn execute_trap(
780    trap_cmd: &str,
781    state: &mut InterpreterState,
782) -> Result<ExecResult, RustBashError> {
783    let was_in_trap = state.in_trap;
784    state.in_trap = true;
785    let program = parse(trap_cmd)?;
786    let result = walker::execute_program(&program, state);
787    state.in_trap = was_in_trap;
788    result
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    #[test]
796    fn parse_empty_input() {
797        let program = parse("").unwrap();
798        assert!(program.complete_commands.is_empty());
799    }
800
801    #[test]
802    fn parse_simple_command() {
803        let program = parse("echo hello").unwrap();
804        assert_eq!(program.complete_commands.len(), 1);
805    }
806
807    #[test]
808    fn parse_sequential_commands() {
809        let program = parse("echo a; echo b").unwrap();
810        assert!(!program.complete_commands.is_empty());
811    }
812
813    #[test]
814    fn parse_pipeline() {
815        let program = parse("echo hello | cat").unwrap();
816        assert_eq!(program.complete_commands.len(), 1);
817    }
818
819    #[test]
820    fn parse_and_or() {
821        let program = parse("true && echo yes").unwrap();
822        assert_eq!(program.complete_commands.len(), 1);
823    }
824
825    #[test]
826    fn parse_error_on_unclosed_quote() {
827        let result = parse("echo 'unterminated");
828        assert!(result.is_err());
829    }
830
831    #[test]
832    fn expand_simple_text() {
833        let word = ast::Word {
834            value: "hello".to_string(),
835            loc: None,
836        };
837        let state = make_test_state();
838        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello"]);
839    }
840
841    #[test]
842    fn expand_single_quoted_text() {
843        let word = ast::Word {
844            value: "'hello world'".to_string(),
845            loc: None,
846        };
847        let state = make_test_state();
848        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
849    }
850
851    #[test]
852    fn expand_double_quoted_text() {
853        let word = ast::Word {
854            value: "\"hello world\"".to_string(),
855            loc: None,
856        };
857        let state = make_test_state();
858        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
859    }
860
861    #[test]
862    fn expand_escaped_character() {
863        let word = ast::Word {
864            value: "hello\\ world".to_string(),
865            loc: None,
866        };
867        let state = make_test_state();
868        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
869    }
870
871    fn make_test_state() -> InterpreterState {
872        use crate::vfs::InMemoryFs;
873        InterpreterState {
874            fs: Arc::new(InMemoryFs::new()),
875            env: HashMap::new(),
876            cwd: "/".to_string(),
877            functions: HashMap::new(),
878            last_exit_code: 0,
879            commands: HashMap::new(),
880            shell_opts: ShellOpts::default(),
881            shopt_opts: ShoptOpts::default(),
882            limits: ExecutionLimits::default(),
883            counters: ExecutionCounters::default(),
884            network_policy: NetworkPolicy::default(),
885            should_exit: false,
886            loop_depth: 0,
887            control_flow: None,
888            positional_params: Vec::new(),
889            shell_name: "rust-bash".to_string(),
890            random_seed: 42,
891            local_scopes: Vec::new(),
892            in_function_depth: 0,
893            traps: HashMap::new(),
894            in_trap: false,
895            errexit_suppressed: 0,
896            stdin_offset: 0,
897            dir_stack: Vec::new(),
898            command_hash: HashMap::new(),
899            aliases: HashMap::new(),
900            current_lineno: 0,
901            shell_start_time: Instant::now(),
902            last_argument: String::new(),
903            call_stack: Vec::new(),
904            machtype: "x86_64-pc-linux-gnu".to_string(),
905            hosttype: "x86_64".to_string(),
906            persistent_fds: HashMap::new(),
907            next_auto_fd: 10,
908            proc_sub_counter: 0,
909            proc_sub_prealloc: HashMap::new(),
910            pipe_stdin_bytes: None,
911            pending_cmdsub_stderr: String::new(),
912        }
913    }
914}