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, Box<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}
394
395// ── Parsing ──────────────────────────────────────────────────────────
396
397pub(crate) fn parser_options() -> brush_parser::ParserOptions {
398    brush_parser::ParserOptions {
399        sh_mode: false,
400        posix_mode: false,
401        enable_extended_globbing: true,
402        tilde_expansion: true,
403    }
404}
405
406/// Parse a shell input string into an AST.
407pub fn parse(input: &str) -> Result<ast::Program, RustBashError> {
408    let tokens =
409        brush_parser::tokenize_str(input).map_err(|e| RustBashError::Parse(e.to_string()))?;
410
411    if tokens.is_empty() {
412        return Ok(ast::Program {
413            complete_commands: vec![],
414        });
415    }
416
417    let options = parser_options();
418    let source_info = brush_parser::SourceInfo {
419        source: input.to_string(),
420    };
421
422    brush_parser::parse_tokens(&tokens, &options, &source_info)
423        .map_err(|e| RustBashError::Parse(e.to_string()))
424}
425
426/// Set a variable in the interpreter state, respecting readonly, nameref,
427/// and attribute transforms (INTEGER, LOWERCASE, UPPERCASE).
428pub(crate) fn set_variable(
429    state: &mut InterpreterState,
430    name: &str,
431    value: String,
432) -> Result<(), RustBashError> {
433    if value.len() > state.limits.max_string_length {
434        return Err(RustBashError::LimitExceeded {
435            limit_name: "max_string_length",
436            limit_value: state.limits.max_string_length,
437            actual_value: value.len(),
438        });
439    }
440
441    // Resolve nameref chain to find the actual target variable.
442    let target = resolve_nameref(name, state)?;
443
444    // If the resolved target is an array subscript (e.g. from a nameref to "a[2]"),
445    // set the array element directly.
446    if let Some(bracket_pos) = target.find('[')
447        && target.ends_with(']')
448    {
449        let arr_name = &target[..bracket_pos];
450        let index_raw = &target[bracket_pos + 1..target.len() - 1];
451        // Expand variables and strip quotes from the index.
452        let word = brush_parser::ast::Word {
453            value: index_raw.to_string(),
454            loc: None,
455        };
456        let expanded_key = crate::interpreter::expansion::expand_word_to_string_mut(&word, state)?;
457
458        if let Some(var) = state.env.get(arr_name)
459            && var.readonly()
460        {
461            return Err(RustBashError::Execution(format!(
462                "{arr_name}: readonly variable"
463            )));
464        }
465
466        // Determine variable type and evaluate index before mutable borrow.
467        let is_assoc = state
468            .env
469            .get(arr_name)
470            .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
471        let numeric_idx = if !is_assoc {
472            crate::interpreter::arithmetic::eval_arithmetic(&expanded_key, state).unwrap_or(0)
473        } else {
474            0
475        };
476
477        match state.env.get_mut(arr_name) {
478            Some(var) => match &mut var.value {
479                VariableValue::AssociativeArray(map) => {
480                    map.insert(expanded_key, value);
481                }
482                VariableValue::IndexedArray(map) => {
483                    let actual_idx = if numeric_idx < 0 {
484                        let max_key = map.keys().next_back().copied().unwrap_or(0);
485                        let resolved = max_key as i64 + 1 + numeric_idx;
486                        if resolved < 0 {
487                            0usize
488                        } else {
489                            resolved as usize
490                        }
491                    } else {
492                        numeric_idx as usize
493                    };
494                    map.insert(actual_idx, value);
495                }
496                VariableValue::Scalar(s) => {
497                    if numeric_idx == 0 || numeric_idx == -1 {
498                        *s = value;
499                    }
500                }
501            },
502            None => {
503                // Create as indexed array with the element.
504                let idx = expanded_key.parse::<usize>().unwrap_or(0);
505                let mut map = std::collections::BTreeMap::new();
506                map.insert(idx, value);
507                state.env.insert(
508                    arr_name.to_string(),
509                    Variable {
510                        value: VariableValue::IndexedArray(map),
511                        attrs: VariableAttrs::empty(),
512                    },
513                );
514            }
515        }
516        return Ok(());
517    }
518
519    // SECONDS assignment resets the shell timer.
520    if target == "SECONDS" {
521        if let Ok(offset) = value.parse::<u64>() {
522            // `SECONDS=N` sets the timer so that $SECONDS reads as N right now.
523            // We achieve this by moving shell_start_time backwards by N seconds.
524            state.shell_start_time = Instant::now() - std::time::Duration::from_secs(offset);
525        } else {
526            state.shell_start_time = Instant::now();
527        }
528        return Ok(());
529    }
530
531    if let Some(var) = state.env.get(&target)
532        && var.readonly()
533    {
534        return Err(RustBashError::Execution(format!(
535            "{target}: readonly variable"
536        )));
537    }
538
539    // Get attributes of target for transforms.
540    let attrs = state
541        .env
542        .get(&target)
543        .map(|v| v.attrs)
544        .unwrap_or(VariableAttrs::empty());
545
546    // INTEGER: evaluate value as arithmetic expression.
547    let value = if attrs.contains(VariableAttrs::INTEGER) {
548        let result = crate::interpreter::arithmetic::eval_arithmetic(&value, state)?;
549        result.to_string()
550    } else {
551        value
552    };
553
554    // Case transforms (lowercase takes precedence if both set, but both shouldn't be).
555    let value = if attrs.contains(VariableAttrs::LOWERCASE) {
556        value.to_lowercase()
557    } else if attrs.contains(VariableAttrs::UPPERCASE) {
558        value.to_uppercase()
559    } else {
560        value
561    };
562
563    match state.env.get_mut(&target) {
564        Some(var) => {
565            match &mut var.value {
566                VariableValue::IndexedArray(map) => {
567                    map.insert(0, value);
568                }
569                VariableValue::AssociativeArray(map) => {
570                    map.insert("0".to_string(), value);
571                }
572                VariableValue::Scalar(s) => *s = value,
573            }
574            // allexport: auto-export on every assignment
575            if state.shell_opts.allexport {
576                var.attrs.insert(VariableAttrs::EXPORTED);
577            }
578        }
579        None => {
580            let attrs = if state.shell_opts.allexport {
581                VariableAttrs::EXPORTED
582            } else {
583                VariableAttrs::empty()
584            };
585            state.env.insert(
586                target,
587                Variable {
588                    value: VariableValue::Scalar(value),
589                    attrs,
590                },
591            );
592        }
593    }
594    Ok(())
595}
596
597/// Set an array element in the interpreter state, creating the array if needed.
598/// Resolves nameref before operating.
599pub(crate) fn set_array_element(
600    state: &mut InterpreterState,
601    name: &str,
602    index: usize,
603    value: String,
604) -> Result<(), RustBashError> {
605    let target = resolve_nameref(name, state)?;
606    if let Some(var) = state.env.get(&target)
607        && var.readonly()
608    {
609        return Err(RustBashError::Execution(format!(
610            "{target}: readonly variable"
611        )));
612    }
613
614    // Apply attribute transforms (INTEGER, LOWERCASE, UPPERCASE).
615    let attrs = state
616        .env
617        .get(&target)
618        .map(|v| v.attrs)
619        .unwrap_or(VariableAttrs::empty());
620    let value = if attrs.contains(VariableAttrs::INTEGER) {
621        crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
622    } else {
623        value
624    };
625    let value = if attrs.contains(VariableAttrs::LOWERCASE) {
626        value.to_lowercase()
627    } else if attrs.contains(VariableAttrs::UPPERCASE) {
628        value.to_uppercase()
629    } else {
630        value
631    };
632
633    let limit = state.limits.max_array_elements;
634    match state.env.get_mut(&target) {
635        Some(var) => match &mut var.value {
636            VariableValue::IndexedArray(map) => {
637                if !map.contains_key(&index) && map.len() >= limit {
638                    return Err(RustBashError::LimitExceeded {
639                        limit_name: "max_array_elements",
640                        limit_value: limit,
641                        actual_value: map.len() + 1,
642                    });
643                }
644                map.insert(index, value);
645            }
646            VariableValue::Scalar(_) => {
647                let mut map = BTreeMap::new();
648                map.insert(index, value);
649                var.value = VariableValue::IndexedArray(map);
650            }
651            VariableValue::AssociativeArray(_) => {
652                return Err(RustBashError::Execution(format!(
653                    "{target}: cannot use numeric index on associative array"
654                )));
655            }
656        },
657        None => {
658            let mut map = BTreeMap::new();
659            map.insert(index, value);
660            state.env.insert(
661                target,
662                Variable {
663                    value: VariableValue::IndexedArray(map),
664                    attrs: VariableAttrs::empty(),
665                },
666            );
667        }
668    }
669    Ok(())
670}
671
672/// Set an associative array element. Resolves nameref before operating.
673pub(crate) fn set_assoc_element(
674    state: &mut InterpreterState,
675    name: &str,
676    key: String,
677    value: String,
678) -> Result<(), RustBashError> {
679    let target = resolve_nameref(name, state)?;
680    if let Some(var) = state.env.get(&target)
681        && var.readonly()
682    {
683        return Err(RustBashError::Execution(format!(
684            "{target}: readonly variable"
685        )));
686    }
687
688    // Apply attribute transforms (INTEGER, LOWERCASE, UPPERCASE).
689    let attrs = state
690        .env
691        .get(&target)
692        .map(|v| v.attrs)
693        .unwrap_or(VariableAttrs::empty());
694    let value = if attrs.contains(VariableAttrs::INTEGER) {
695        crate::interpreter::arithmetic::eval_arithmetic(&value, state)?.to_string()
696    } else {
697        value
698    };
699    let value = if attrs.contains(VariableAttrs::LOWERCASE) {
700        value.to_lowercase()
701    } else if attrs.contains(VariableAttrs::UPPERCASE) {
702        value.to_uppercase()
703    } else {
704        value
705    };
706
707    let limit = state.limits.max_array_elements;
708    match state.env.get_mut(&target) {
709        Some(var) => match &mut var.value {
710            VariableValue::AssociativeArray(map) => {
711                if !map.contains_key(&key) && map.len() >= limit {
712                    return Err(RustBashError::LimitExceeded {
713                        limit_name: "max_array_elements",
714                        limit_value: limit,
715                        actual_value: map.len() + 1,
716                    });
717                }
718                map.insert(key, value);
719            }
720            _ => {
721                return Err(RustBashError::Execution(format!(
722                    "{target}: not an associative array"
723                )));
724            }
725        },
726        None => {
727            return Err(RustBashError::Execution(format!(
728                "{target}: not an associative array"
729            )));
730        }
731    }
732    Ok(())
733}
734
735/// Generate next pseudo-random number (xorshift32, range 0..32767).
736pub(crate) fn next_random(state: &mut InterpreterState) -> u16 {
737    let mut s = state.random_seed;
738    if s == 0 {
739        s = 12345;
740    }
741    s ^= s << 13;
742    s ^= s >> 17;
743    s ^= s << 5;
744    state.random_seed = s;
745    (s & 0x7FFF) as u16
746}
747
748/// Resolve a nameref chain: follow NAMEREF attributes until a non-nameref variable
749/// (or missing variable) is found. Returns the final target name.
750/// Errors on circular references (chain longer than 10).
751pub(crate) fn resolve_nameref(
752    name: &str,
753    state: &InterpreterState,
754) -> Result<String, RustBashError> {
755    let mut current = name.to_string();
756    for _ in 0..10 {
757        match state.env.get(&current) {
758            Some(var) if var.attrs.contains(VariableAttrs::NAMEREF) => {
759                current = var.value.as_scalar().to_string();
760            }
761            _ => return Ok(current),
762        }
763    }
764    Err(RustBashError::Execution(format!(
765        "{name}: circular name reference"
766    )))
767}
768
769/// Non-failing nameref resolution: returns the resolved name, or the original
770/// name if the chain is circular.
771pub(crate) fn resolve_nameref_or_self(name: &str, state: &InterpreterState) -> String {
772    resolve_nameref(name, state).unwrap_or_else(|_| name.to_string())
773}
774
775/// Execute a trap handler string, preventing recursive re-trigger of the same trap type.
776pub(crate) fn execute_trap(
777    trap_cmd: &str,
778    state: &mut InterpreterState,
779) -> Result<ExecResult, RustBashError> {
780    let was_in_trap = state.in_trap;
781    state.in_trap = true;
782    let program = parse(trap_cmd)?;
783    let result = walker::execute_program(&program, state);
784    state.in_trap = was_in_trap;
785    result
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    #[test]
793    fn parse_empty_input() {
794        let program = parse("").unwrap();
795        assert!(program.complete_commands.is_empty());
796    }
797
798    #[test]
799    fn parse_simple_command() {
800        let program = parse("echo hello").unwrap();
801        assert_eq!(program.complete_commands.len(), 1);
802    }
803
804    #[test]
805    fn parse_sequential_commands() {
806        let program = parse("echo a; echo b").unwrap();
807        assert!(!program.complete_commands.is_empty());
808    }
809
810    #[test]
811    fn parse_pipeline() {
812        let program = parse("echo hello | cat").unwrap();
813        assert_eq!(program.complete_commands.len(), 1);
814    }
815
816    #[test]
817    fn parse_and_or() {
818        let program = parse("true && echo yes").unwrap();
819        assert_eq!(program.complete_commands.len(), 1);
820    }
821
822    #[test]
823    fn parse_error_on_unclosed_quote() {
824        let result = parse("echo 'unterminated");
825        assert!(result.is_err());
826    }
827
828    #[test]
829    fn expand_simple_text() {
830        let word = ast::Word {
831            value: "hello".to_string(),
832            loc: None,
833        };
834        let state = make_test_state();
835        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello"]);
836    }
837
838    #[test]
839    fn expand_single_quoted_text() {
840        let word = ast::Word {
841            value: "'hello world'".to_string(),
842            loc: None,
843        };
844        let state = make_test_state();
845        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
846    }
847
848    #[test]
849    fn expand_double_quoted_text() {
850        let word = ast::Word {
851            value: "\"hello world\"".to_string(),
852            loc: None,
853        };
854        let state = make_test_state();
855        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
856    }
857
858    #[test]
859    fn expand_escaped_character() {
860        let word = ast::Word {
861            value: "hello\\ world".to_string(),
862            loc: None,
863        };
864        let state = make_test_state();
865        assert_eq!(expand_word(&word, &state).unwrap(), vec!["hello world"]);
866    }
867
868    fn make_test_state() -> InterpreterState {
869        use crate::vfs::InMemoryFs;
870        InterpreterState {
871            fs: Arc::new(InMemoryFs::new()),
872            env: HashMap::new(),
873            cwd: "/".to_string(),
874            functions: HashMap::new(),
875            last_exit_code: 0,
876            commands: HashMap::new(),
877            shell_opts: ShellOpts::default(),
878            shopt_opts: ShoptOpts::default(),
879            limits: ExecutionLimits::default(),
880            counters: ExecutionCounters::default(),
881            network_policy: NetworkPolicy::default(),
882            should_exit: false,
883            loop_depth: 0,
884            control_flow: None,
885            positional_params: Vec::new(),
886            shell_name: "rust-bash".to_string(),
887            random_seed: 42,
888            local_scopes: Vec::new(),
889            in_function_depth: 0,
890            traps: HashMap::new(),
891            in_trap: false,
892            errexit_suppressed: 0,
893            stdin_offset: 0,
894            dir_stack: Vec::new(),
895            command_hash: HashMap::new(),
896            aliases: HashMap::new(),
897            current_lineno: 0,
898            shell_start_time: Instant::now(),
899            last_argument: String::new(),
900            call_stack: Vec::new(),
901            machtype: "x86_64-pc-linux-gnu".to_string(),
902            hosttype: "x86_64".to_string(),
903            persistent_fds: HashMap::new(),
904            next_auto_fd: 10,
905            proc_sub_counter: 0,
906            proc_sub_prealloc: HashMap::new(),
907            pipe_stdin_bytes: None,
908        }
909    }
910}