Skip to main content

nu_protocol/engine/
stack.rs

1use crate::{
2    Config, ENV_VARIABLE_ID, IntoValue, LAST_VARIABLE_ID, NU_VARIABLE_ID, OutDest, PipelineData,
3    PipelineMetadata, ShellError, Span, Value, VarId,
4    ast::PathMember,
5    engine::{
6        ArgumentStack, DEFAULT_OVERLAY_NAME, EngineState, EnvName, ErrorHandlerStack, Redirection,
7        ScopeBindings, StackCallArgGuard, StackCollectValueGuard, StackIoGuard, StackOutDest,
8        StackWithInvocation,
9    },
10    ir::ScopeRegion,
11    record, report_shell_warning,
12    shell_error::generic::GenericError,
13    truncate_value_to_budget,
14};
15use std::{
16    collections::{HashMap, HashSet},
17    fs::File,
18    path::{Component, MAIN_SEPARATOR},
19    sync::{Arc, Mutex},
20};
21
22/// Shared interactive last-result (`$ans`) storage.
23///
24/// Held in an [`Arc`] so REPL child stacks from [`Stack::with_parent`] see the same
25/// payload and can clear `warn_pending` without mutating an immutable parent frame.
26///
27/// When [`Self::present`] is true, reading `$ans` yields a record with
28/// `exit_code`, `duration`, and `command`. The `last` field is included only when a
29/// payload is stored. When false (never snapshotted after a user command), `$ans`
30/// is `nothing`.
31#[derive(Debug, Default)]
32struct LastResultSlot {
33    /// Whether `$ans` should resolve to a record (vs `nothing`).
34    present: bool,
35    /// Pipeline payload for the `last` field (`None` when payload capture is off).
36    last: Option<Value>,
37    /// Pipeline metadata for replaying `last` (e.g. `ls` path_columns / colors).
38    metadata: Option<PipelineMetadata>,
39    truncated: bool,
40    /// Exit code of the last REPL line (mirrors `$env.LAST_EXIT_CODE`).
41    exit_code: i64,
42    /// Duration of the last REPL line in nanoseconds (Nushell `Duration` value).
43    duration_ns: i64,
44    /// Exact REPL source of the last user line (same buffer reedline stores in history).
45    command: String,
46    /// Set when a store was truncated; moved to `warn_deferred` on first access.
47    warn_pending: bool,
48    /// Set when `$ans` was accessed after a truncated store; reported after output prints.
49    warn_deferred: bool,
50}
51
52/// Environment variables per overlay
53pub type EnvVars = HashMap<String, HashMap<EnvName, Value>>;
54
55/// A runtime value stack used during evaluation
56///
57/// A note on implementation:
58///
59/// We previously set up the stack in a traditional way, where stack frames had parents which would
60/// represent other frames that you might return to when exiting a function.
61///
62/// While experimenting with blocks, we found that we needed to have closure captures of variables
63/// seen outside of the blocks, so that they blocks could be run in a way that was both thread-safe
64/// and followed the restrictions for closures applied to iterators. The end result left us with
65/// closure-captured single stack frames that blocks could see.
66///
67/// Blocks make up the only scope and stack definition abstraction in Nushell. As a result, we were
68/// creating closure captures at any point we wanted to have a Block value we could safely evaluate
69/// in any context. This meant that the parents were going largely unused, with captured variables
70/// taking their place. The end result is this, where we no longer have separate frames, but instead
71/// use the Stack as a way of representing the local and closure-captured state.
72#[derive(Debug, Clone)]
73pub struct Stack {
74    /// Variables
75    pub vars: Vec<(VarId, Value)>,
76    /// Environment variables arranged as a stack to be able to recover values from parent scopes
77    pub env_vars: Vec<Arc<EnvVars>>,
78    /// Tells which environment variables from engine state are hidden, per overlay.
79    pub env_hidden: Arc<HashMap<String, HashSet<EnvName>>>,
80    /// Tracks env vars hidden in this stack context to report repeated `hide-env` calls.
81    ///
82    /// This is separate from `env_hidden`: `env_hidden` controls runtime visibility for engine
83    /// state values, while `env_hide_history` preserves command semantics for repeated hides.
84    pub env_hide_history: Arc<HashMap<String, HashSet<EnvName>>>,
85    /// List of active overlays
86    pub active_overlays: Vec<String>,
87    /// Argument stack for IR evaluation
88    pub arguments: ArgumentStack,
89    /// Error handler stack for IR evaluation
90    pub error_handlers: ErrorHandlerStack,
91    /// Finally handler stack for IR evaluation
92    pub finally_run_handlers: ErrorHandlerStack,
93    pub recursion_count: u64,
94    pub parent_stack: Option<Arc<Stack>>,
95    /// Variables that have been deleted (this is used to hide values from parent stack lookups)
96    pub parent_deletions: Vec<VarId>,
97    /// Variables deleted in this stack
98    pub deletions: Vec<VarId>,
99    /// Locally updated config. Use [`.get_config()`](Self::get_config) to access correctly.
100    pub config: Option<Arc<Config>>,
101    pub(crate) out_dest: StackOutDest,
102    /// When `true`, external processes spawned with `PipelineData::Empty` input
103    /// receive `/dev/null` for stdin instead of inheriting the terminal.
104    pub suppress_stdin: bool,
105    /// Active block-local scope bindings (commands/modules), outer → inner.
106    ///
107    /// Pushed when evaluating a whole block via `eval_ir_block` (closures, custom commands).
108    /// Used by `scope` together with [`Self::ir_scope_regions`].
109    pub active_scope_bindings: Vec<Arc<ScopeBindings>>,
110    /// Scope regions of the IR block currently being evaluated (inlined keyword bodies).
111    pub ir_scope_regions: Vec<ScopeRegion>,
112    /// Current program counter while evaluating IR (for matching [`Self::ir_scope_regions`]).
113    pub ir_instruction_index: Option<usize>,
114    /// Interactive last-result payload for [`LAST_VARIABLE_ID`] (e.g. `$ans`).
115    ///
116    /// Shared across parent/child stacks so REPL iterations (which use
117    /// [`Stack::with_parent`]) can store and clear truncation warnings correctly.
118    last_result: Arc<Mutex<LastResultSlot>>,
119}
120
121impl Default for Stack {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl Stack {
128    /// Create a new stack.
129    ///
130    /// stdout and stderr will be set to [`OutDest::Inherit`]. So, if the last command is an external command,
131    /// then its output will be forwarded to the terminal/stdio streams.
132    ///
133    /// Use [`Stack::collect_value`] afterwards if you need to evaluate an expression to a [`Value`]
134    /// (as opposed to a [`PipelineData`](crate::PipelineData)).
135    pub fn new() -> Self {
136        Self {
137            vars: Vec::new(),
138            env_vars: Vec::new(),
139            env_hidden: Arc::new(HashMap::new()),
140            env_hide_history: Arc::new(HashMap::new()),
141            active_overlays: vec![DEFAULT_OVERLAY_NAME.to_string()],
142            arguments: ArgumentStack::new(),
143            error_handlers: ErrorHandlerStack::new(),
144            finally_run_handlers: ErrorHandlerStack::new(),
145            recursion_count: 0,
146            parent_stack: None,
147            parent_deletions: vec![],
148            deletions: vec![],
149            config: None,
150            out_dest: StackOutDest::new(),
151            suppress_stdin: false,
152            active_scope_bindings: vec![],
153            ir_scope_regions: vec![],
154            ir_instruction_index: None,
155            last_result: Arc::new(Mutex::new(LastResultSlot::default())),
156        }
157    }
158
159    /// Create a new child stack from a parent.
160    ///
161    /// Changes from this child can be merged back into the parent with
162    /// [`Stack::with_changes_from_child`]
163    pub fn with_parent(parent: Arc<Stack>) -> Stack {
164        Stack {
165            // here we are still cloning environment variable-related information
166            env_vars: parent.env_vars.clone(),
167            env_hidden: parent.env_hidden.clone(),
168            env_hide_history: parent.env_hide_history.clone(),
169            active_overlays: parent.active_overlays.clone(),
170            arguments: ArgumentStack::new(),
171            error_handlers: ErrorHandlerStack::new(),
172            finally_run_handlers: ErrorHandlerStack::new(),
173            recursion_count: parent.recursion_count,
174            vars: vec![],
175            parent_deletions: vec![],
176            deletions: vec![],
177            config: parent.config.clone(),
178            out_dest: parent.out_dest.clone(),
179            suppress_stdin: parent.suppress_stdin,
180            // Child inherits outer block bindings so nested `scope` still sees them.
181            active_scope_bindings: parent.active_scope_bindings.clone(),
182            // Nested IR evaluation installs its own regions/pc.
183            ir_scope_regions: vec![],
184            ir_instruction_index: None,
185            // Share last-result with the parent (REPL uses with_parent every iteration).
186            last_result: parent.last_result.clone(),
187            parent_stack: Some(parent),
188        }
189    }
190
191    /// Push block-local scope bindings for the duration of evaluating a whole block.
192    pub fn push_scope_bindings(&mut self, bindings: Arc<ScopeBindings>) {
193        self.active_scope_bindings.push(bindings);
194    }
195
196    /// Pop the most recently pushed whole-block scope bindings.
197    pub fn pop_scope_bindings(&mut self) {
198        let popped = self.active_scope_bindings.pop();
199        debug_assert!(
200            popped.is_some(),
201            "pop_scope_bindings with empty active_scope_bindings (unbalanced push/pop)"
202        );
203    }
204
205    /// Take an [`Arc`] parent, and a child, and apply all the changes from a child back to the parent.
206    ///
207    /// Here it is assumed that `child` was created by a call to [`Stack::with_parent`] with `parent`.
208    ///
209    /// For this to be performant and not clone `parent`, `child` should be the only other
210    /// referencer of `parent`.
211    pub fn with_changes_from_child(parent: Arc<Stack>, child: Stack) -> Stack {
212        // we're going to drop the link to the parent stack on our new stack
213        // so that we can unwrap the Arc as a unique reference
214        drop(child.parent_stack);
215        let mut unique_stack = Arc::unwrap_or_clone(parent);
216
217        unique_stack
218            .vars
219            .retain(|(var, _)| !child.parent_deletions.contains(var));
220        for (var, value) in child.vars {
221            unique_stack.add_var(var, value);
222        }
223        unique_stack.env_vars = child.env_vars;
224        unique_stack.env_hidden = child.env_hidden;
225        unique_stack.env_hide_history = child.env_hide_history;
226        unique_stack.active_overlays = child.active_overlays;
227        unique_stack.config = child.config;
228        // last_result is Arc-shared with the child; no merge needed.
229        unique_stack
230    }
231
232    pub fn with_env(
233        &mut self,
234        env_vars: &[Arc<EnvVars>],
235        env_hidden: &Arc<HashMap<String, HashSet<EnvName>>>,
236    ) {
237        // Do not clone the environment if it hasn't changed
238        if self.env_vars.iter().any(|scope| !scope.is_empty()) {
239            env_vars.clone_into(&mut self.env_vars);
240        }
241
242        if !self.env_hidden.is_empty() {
243            self.env_hidden.clone_from(env_hidden);
244        }
245    }
246
247    /// Lookup a variable, returning None if it is not present
248    fn lookup_var(&self, var_id: VarId) -> Option<Value> {
249        if var_id == LAST_VARIABLE_ID {
250            return self.assemble_ans_record(Span::unknown());
251        }
252
253        for (id, val) in &self.vars {
254            if var_id == *id {
255                return Some(val.clone());
256            }
257        }
258
259        if let Some(stack) = &self.parent_stack
260            && !self.parent_deletions.contains(&var_id)
261        {
262            return stack.lookup_var(var_id);
263        }
264        None
265    }
266
267    fn with_last_result_slot<R>(&self, f: impl FnOnce(&LastResultSlot) -> R) -> R {
268        match self.last_result.lock() {
269            Ok(slot) => f(&slot),
270            // Poison is rare; recover so `$ans` / capture do not hard-panic the REPL.
271            Err(poisoned) => f(&poisoned.into_inner()),
272        }
273    }
274
275    fn with_last_result_slot_mut<R>(&self, f: impl FnOnce(&mut LastResultSlot) -> R) -> R {
276        match self.last_result.lock() {
277            Ok(mut slot) => f(&mut slot),
278            Err(poisoned) => {
279                let mut slot = poisoned.into_inner();
280                // Drop potentially inconsistent state after a panic while locked.
281                *slot = LastResultSlot::default();
282                f(&mut slot)
283            }
284        }
285    }
286
287    /// Build the `$ans` record when the slot is present; otherwise `None` (→ `nothing`).
288    ///
289    /// Omits the `last` field entirely when no payload is stored (e.g. budget is `0`),
290    /// so `$ans` is `{ exit_code, duration, command }` only. `command` is always included
291    /// once the slot is present.
292    fn assemble_ans_record(&self, span: Span) -> Option<Value> {
293        self.with_last_result_slot(|slot| {
294            if !slot.present {
295                return None;
296            }
297            Some(match &slot.last {
298                Some(last) => Value::record(
299                    record! {
300                        "last" => last.clone().with_span(span),
301                        "exit_code" => Value::int(slot.exit_code, span),
302                        "duration" => Value::duration(slot.duration_ns, span),
303                        "command" => Value::string(slot.command.clone(), span),
304                    },
305                    span,
306                ),
307                None => Value::record(
308                    record! {
309                        "exit_code" => Value::int(slot.exit_code, span),
310                        "duration" => Value::duration(slot.duration_ns, span),
311                        "command" => Value::string(slot.command.clone(), span),
312                    },
313                    span,
314                ),
315            })
316        })
317    }
318
319    /// Drop the entire `$ans` slot (full clear).
320    pub fn clear_last_result(&mut self) {
321        self.with_last_result_slot_mut(|slot| {
322            if let Some(old) = slot.last.take() {
323                drop(old);
324            }
325            *slot = LastResultSlot::default();
326        });
327    }
328
329    /// Drop only `$ans.last` and its metadata/truncation flags, freeing payload memory.
330    ///
331    /// Leaves `present`, `exit_code`, `duration`, and `command` unchanged so a budget of
332    /// `0` can still expose timing/exit status/source without retaining the pipeline value.
333    pub fn clear_last_result_payload(&mut self) {
334        self.with_last_result_slot_mut(|slot| {
335            if let Some(old) = slot.last.take() {
336                drop(old);
337            }
338            slot.metadata = None;
339            slot.truncated = false;
340            slot.warn_pending = false;
341            slot.warn_deferred = false;
342        });
343    }
344
345    /// Store `value` as `$ans.last`, enforcing `budget` via truncation.
346    ///
347    /// When `budget == 0`, payload capture is disabled (clears `.last` only; exit code,
348    /// duration, and `command` stay). Preserves pipeline `metadata` (e.g. `path_columns` used for
349    /// `ls` coloring) so replaying `$ans.last` can match the original display.
350    pub fn set_last_result(
351        &mut self,
352        value: Value,
353        metadata: Option<PipelineMetadata>,
354        budget: usize,
355    ) {
356        if budget == 0 {
357            self.clear_last_result_payload();
358            return;
359        }
360
361        let (stored, truncated) = truncate_value_to_budget(value, budget);
362        self.store_last_result_raw(stored, metadata, truncated);
363    }
364
365    /// Install an already-budgeted `$ans.last` value (caller handled truncation).
366    ///
367    /// Marks `$ans` present. Does not reset `exit_code` / `duration` / `command` (those are
368    /// updated by [`Self::snapshot_ans_repl_metadata`] at end of each REPL line).
369    pub fn store_last_result_raw(
370        &mut self,
371        value: Value,
372        metadata: Option<PipelineMetadata>,
373        truncated: bool,
374    ) {
375        self.with_last_result_slot_mut(|slot| {
376            if let Some(old) = slot.last.replace(value) {
377                drop(old);
378            }
379            slot.metadata = metadata;
380            slot.truncated = truncated;
381            slot.warn_pending = truncated;
382            // Fresh store replaces any not-yet-shown deferred warning.
383            slot.warn_deferred = false;
384            slot.present = true;
385        });
386    }
387
388    /// After a REPL user command finishes: refresh `$ans.exit_code`, `$ans.duration`,
389    /// and `$ans.command`.
390    ///
391    /// `command` is the exact reedline buffer for this line (same text history stores).
392    /// Always marks `$ans` present so every user-typed line gets exit code, duration,
393    /// and source (empty Enter / auto-cd do not call this). When `budget == 0`, also
394    /// drops `$ans.last` (and its memory) so the record is `{ exit_code, duration, command }`
395    /// without a `last` field. When budget is positive, any `.last` already stored this
396    /// line (or earlier) is kept.
397    pub fn snapshot_ans_repl_metadata(
398        &mut self,
399        engine_state: &EngineState,
400        duration: std::time::Duration,
401        command: impl Into<String>,
402    ) {
403        let budget = self.get_config(engine_state).max_last_result_size_bytes();
404        let exit_code = self
405            .get_env_var(engine_state, "LAST_EXIT_CODE")
406            .and_then(|v| v.as_int().ok())
407            .unwrap_or(0);
408        let duration_ns = i64::try_from(duration.as_nanos()).unwrap_or(i64::MAX);
409        let command = command.into();
410
411        if budget == 0 {
412            // Payload off: free `.last` memory before refreshing metadata.
413            self.clear_last_result_payload();
414        }
415
416        self.with_last_result_slot_mut(|slot| {
417            slot.exit_code = exit_code;
418            slot.duration_ns = duration_ns;
419            slot.command = command;
420            slot.present = true;
421        });
422    }
423
424    /// Pipeline metadata associated with the stored `$ans.last`, if any.
425    pub fn last_result_metadata(&self) -> Option<PipelineMetadata> {
426        self.with_last_result_slot(|slot| slot.metadata.clone())
427    }
428
429    /// Estimated memory size of the stored `$ans.last` payload (`0` if unset).
430    pub fn last_result_memory_size(&self) -> usize {
431        self.with_last_result_slot(|slot| slot.last.as_ref().map(|v| v.memory_size()).unwrap_or(0))
432    }
433
434    /// Marker key in [`PipelineMetadata::custom`] identifying pipeline data loaded from `$ans`.
435    ///
436    /// Used so IR cell-path follow only reattaches last-result metadata for `$ans.last`,
437    /// not every unrelated record field named `last`.
438    pub const ANS_LAST_RESULT_METADATA_KEY: &str = "ans_last_result";
439
440    /// Build [`PipelineData`] for `$ans`, restoring stored pipeline metadata on the record
441    /// so `$ans.last` cell-path access can reattach it (see IR `FollowCellPath`).
442    pub fn last_result_pipeline_data(&self, span: Span) -> PipelineData {
443        let mut metadata = self.last_result_metadata().unwrap_or_default();
444        // Mark so FollowCellPath can reattach payload metadata only for `$ans.*`.
445        metadata
446            .custom
447            .insert(Self::ANS_LAST_RESULT_METADATA_KEY, Value::bool(true, span));
448        let value = self
449            .assemble_ans_record(span)
450            .unwrap_or_else(|| Value::nothing(span));
451        PipelineData::value(value, Some(metadata))
452    }
453
454    /// Whether the currently stored `$ans.last` was truncated.
455    pub fn last_result_was_truncated(&self) -> bool {
456        self.with_last_result_slot(|slot| slot.truncated)
457    }
458
459    /// On `$ans` access after a truncated store: schedule a warning for after output prints.
460    ///
461    /// Does not print anything. Call [`Self::take_last_result_warn_deferred`] after display
462    /// so the truncated value is shown first, then the warning.
463    pub fn defer_last_result_truncation_warning(&self) {
464        self.with_last_result_slot_mut(|slot| {
465            if slot.warn_pending {
466                slot.warn_pending = false;
467                slot.warn_deferred = true;
468            }
469        });
470    }
471
472    /// Whether a truncation warning is waiting to be shown after print (does not clear).
473    pub fn last_result_warn_pending(&self) -> bool {
474        self.with_last_result_slot(|slot| slot.warn_pending)
475    }
476
477    /// Take the deferred truncation warning flag (clears it).
478    ///
479    /// Returns `true` once after a truncated `$ans` was accessed; intended to be called
480    /// after the pipeline has been printed so the warning appears below the data.
481    pub fn take_last_result_warn_deferred(&self) -> bool {
482        self.with_last_result_slot_mut(|slot| std::mem::take(&mut slot.warn_deferred))
483    }
484
485    /// Report a deferred last-result truncation warning, if any.
486    ///
487    /// Prefer calling this after printing so output is not scrolled away by the warning.
488    pub fn flush_last_result_truncation_warning(&self, engine_state: &EngineState, span: Span) {
489        if !self.take_last_result_warn_deferred() {
490            return;
491        }
492        let limit_bytes = self.get_config(engine_state).max_last_result_size_bytes();
493        report_shell_warning(
494            Some(self),
495            engine_state,
496            &crate::ShellWarning::LastResultTruncated {
497                span,
498                limit_bytes,
499                help: Some(format!(
500                    "Increase $env.config.max_last_result_size or use a smaller command result. Variable name is `${}`.",
501                    crate::LAST_RESULT_VAR_NAME
502                )),
503                // EveryUse: once-per-access is handled by warn_pending/warn_deferred flags.
504                report_mode: crate::ReportMode::EveryUse,
505            },
506        );
507    }
508
509    /// Lookup a variable, erroring if it is not found
510    ///
511    /// The passed-in span will be used to tag the value
512    pub fn get_var(&self, var_id: VarId, span: Span) -> Result<Value, ShellError> {
513        match self.lookup_var(var_id) {
514            Some(v) => Ok(v.with_span(span)),
515            // Unset last-result behaves like `nothing` rather than a missing variable.
516            None if var_id == LAST_VARIABLE_ID => Ok(Value::nothing(span)),
517            None => Err(ShellError::VariableNotFoundAtRuntime { span }),
518        }
519    }
520
521    /// Lookup a variable, erroring if it is not found
522    ///
523    /// While the passed-in span will be used for errors, the returned value
524    /// has the span from where it was originally defined
525    pub fn get_var_with_origin(&self, var_id: VarId, span: Span) -> Result<Value, ShellError> {
526        match self.lookup_var(var_id) {
527            Some(v) => Ok(v),
528            None => {
529                if var_id == NU_VARIABLE_ID || var_id == ENV_VARIABLE_ID {
530                    return Err(ShellError::Generic(GenericError::new(
531                        "Built-in variables `$env` and `$nu` have no metadata",
532                        "no metadata available",
533                        span,
534                    )));
535                }
536                Err(ShellError::VariableNotFoundAtRuntime { span })
537            }
538        }
539    }
540
541    /// Get the local config if set, otherwise the config from the engine state.
542    ///
543    /// This is the canonical way to get [`Config`] when [`Stack`] is available.
544    pub fn get_config(&self, engine_state: &EngineState) -> Arc<Config> {
545        self.config
546            .clone()
547            .unwrap_or_else(|| engine_state.config.clone())
548    }
549
550    /// Update the local config with the config stored in the `config` environment variable. Run
551    /// this after assigning to `$env.config`.
552    ///
553    /// The config will be updated with successfully parsed values even if an error occurs.
554    pub fn update_config(&mut self, engine_state: &EngineState) -> Result<(), ShellError> {
555        if let Some(value) = self.get_env_var(engine_state, "config") {
556            let old = self.get_config(engine_state);
557            let mut config = (*old).clone();
558            let result = config.update_from_value_with_options(
559                &old,
560                value,
561                engine_state.history_locked_after_startup,
562            );
563            // The config value is modified by the update, so we should add it again
564            self.add_env_var("config".into(), config.clone().into_value(value.span()));
565            self.config = Some(config.into());
566            if let Some(warning) = result? {
567                report_shell_warning(Some(self), engine_state, &warning);
568            }
569        } else {
570            self.config = None;
571        }
572        Ok(())
573    }
574
575    pub fn add_var(&mut self, var_id: VarId, value: Value) {
576        //self.vars.insert(var_id, value);
577        for (id, val) in &mut self.vars {
578            if *id == var_id {
579                *val = value;
580                return;
581            }
582        }
583        self.vars.push((var_id, value));
584    }
585
586    /// Return a mutable reference to a variable's value for in-place mutation.
587    ///
588    /// Looks up the variable in the current stack frame first. If not found, pulls it
589    /// from the parent chain into the current frame (cloning it once). This enables
590    /// zero-clone mutation for local `mut` variables: use `get_var_mut` + mutate instead
591    /// of `lookup_var` (clone) + mutate + `add_var` (move back).
592    pub fn get_var_mut(&mut self, var_id: VarId) -> Option<&mut Value> {
593        // Use index-based access to avoid conflicting mutable borrows
594        if let Some(pos) = self.vars.iter().position(|(id, _)| var_id == *id) {
595            return Some(&mut self.vars[pos].1);
596        }
597        // Check parent chain
598        if let Some(parent) = &self.parent_stack
599            && !self.parent_deletions.contains(&var_id)
600        {
601            let value = parent.lookup_var(var_id)?;
602            self.vars.push((var_id, value));
603            return self.vars.last_mut().map(|(_, val)| val);
604        }
605        None
606    }
607
608    /// Upsert a cell path on a variable in place (shared by AST and IR assignment paths).
609    ///
610    /// Errors with [`ShellError::VariableNotFoundAtRuntime`] if the variable is not on
611    /// this stack or its parent chain.
612    pub fn upsert_var_cell_path(
613        &mut self,
614        var_id: VarId,
615        members: &[PathMember],
616        new_value: Value,
617        span: Span,
618    ) -> Result<(), ShellError> {
619        let value = self
620            .get_var_mut(var_id)
621            .ok_or(ShellError::VariableNotFoundAtRuntime { span })?;
622        value.upsert_data_at_cell_path(members, new_value)
623    }
624
625    pub fn remove_var(&mut self, var_id: VarId) {
626        for (idx, (id, _)) in self.vars.iter().enumerate() {
627            if *id == var_id {
628                self.vars.remove(idx);
629                break;
630            }
631        }
632        // even if we did have it in the original layer, we need to make sure to remove it here
633        // as well (since the previous update might have simply hid the parent value)
634        if self.parent_stack.is_some() {
635            self.parent_deletions.push(var_id);
636        }
637        self.deletions.push(var_id);
638    }
639
640    pub fn add_env_var(&mut self, var: String, value: Value) {
641        if let Some(last_overlay) = self.active_overlays.last().cloned() {
642            let env_name = EnvName::from(var);
643            self.clear_env_var_marks_in_active_overlay(&last_overlay, &env_name);
644
645            if let Some(scope) = self.env_vars.last_mut() {
646                let scope = Arc::make_mut(scope);
647                if let Some(env_vars) = scope.get_mut(&last_overlay) {
648                    env_vars.insert(env_name, value);
649                } else {
650                    scope.insert(last_overlay, [(env_name, value)].into_iter().collect());
651                }
652            } else {
653                self.env_vars.push(Arc::new(
654                    [(last_overlay, [(env_name, value)].into_iter().collect())]
655                        .into_iter()
656                        .collect(),
657                ));
658            }
659        } else {
660            // TODO: Remove panic
661            panic!("internal error: no active overlay");
662        }
663    }
664
665    fn clear_env_var_marks_in_active_overlay(&mut self, overlay: &str, env_name: &EnvName) {
666        if let Some(env_hidden) = Arc::make_mut(&mut self.env_hidden).get_mut(overlay) {
667            // Re-assigning re-activates a previously hidden env var in this overlay.
668            env_hidden.remove(env_name);
669        }
670
671        if let Some(hide_history) = Arc::make_mut(&mut self.env_hide_history).get_mut(overlay) {
672            hide_history.remove(env_name);
673        }
674    }
675
676    pub fn set_last_exit_code(&mut self, code: i32, span: Span) {
677        self.add_env_var("LAST_EXIT_CODE".into(), Value::int(code.into(), span));
678    }
679
680    pub fn set_last_error(&mut self, error: &ShellError) {
681        if let Some(code) = error.external_exit_code() {
682            self.set_last_exit_code(code.item, code.span);
683        } else if let Some(code) = error.exit_code() {
684            self.set_last_exit_code(code, Span::unknown());
685        }
686    }
687
688    pub fn last_overlay_name(&self) -> Result<String, ShellError> {
689        self.active_overlays
690            .last()
691            .cloned()
692            .ok_or_else(|| ShellError::NushellFailed {
693                msg: "No active overlay".into(),
694            })
695    }
696
697    /// Like [`captures_to_stack_preserve_out_dest`], but sets the new scope up to collect output into a Value.
698    pub fn captures_to_stack(&self, captures: Vec<(VarId, Value)>) -> Stack {
699        self.captures_to_stack_preserve_out_dest(captures)
700            .collect_value()
701    }
702
703    /// Creates a derived stack for a new scope, with the given captures.
704    ///
705    /// The caller is retained as [`Self::parent_stack`] so outer variables remain visible to
706    /// `scope variables` (and other stack lookups that walk parents). Captured values are still
707    /// copied onto this stack for isolation of the closure’s own locals.
708    pub fn captures_to_stack_preserve_out_dest(&self, captures: Vec<(VarId, Value)>) -> Stack {
709        let mut env_vars = self.env_vars.clone();
710        env_vars.push(Arc::new(HashMap::new()));
711
712        Stack {
713            vars: captures,
714            env_vars,
715            env_hidden: self.env_hidden.clone(),
716            env_hide_history: self.env_hide_history.clone(),
717            active_overlays: self.active_overlays.clone(),
718            arguments: ArgumentStack::new(),
719            error_handlers: ErrorHandlerStack::new(),
720            finally_run_handlers: ErrorHandlerStack::new(),
721            recursion_count: self.recursion_count,
722            // Keep the caller as parent so global/outer locals stay nameable for `scope`
723            // (values are still resolved via the parent chain when not captured).
724            parent_stack: Some(Arc::new(self.clone())),
725            parent_deletions: vec![],
726            deletions: vec![],
727            config: self.config.clone(),
728            out_dest: self.out_dest.clone(),
729            suppress_stdin: self.suppress_stdin,
730            // Inherit caller block bindings so nested closures still see outer local defs.
731            active_scope_bindings: self.active_scope_bindings.clone(),
732            ir_scope_regions: vec![],
733            ir_instruction_index: None,
734            // Share last-result so closures can still read `$ans`.
735            last_result: self.last_result.clone(),
736        }
737    }
738
739    pub fn gather_captures(&self, engine_state: &EngineState, captures: &[(VarId, Span)]) -> Stack {
740        let mut vars = Vec::with_capacity(captures.len());
741
742        let fake_span = Span::new(0, 0);
743
744        for (capture, _) in captures {
745            // Note: this assumes we have calculated captures correctly and that commands
746            // that take in a var decl will manually set this into scope when running the blocks
747            if let Ok(value) = self.get_var(*capture, fake_span) {
748                vars.push((*capture, value));
749            } else if let Some(const_val) = &engine_state.get_var(*capture).const_val {
750                vars.push((*capture, const_val.clone()));
751            }
752        }
753
754        let mut env_vars = self.env_vars.clone();
755        env_vars.push(Arc::new(HashMap::new()));
756
757        Stack {
758            vars,
759            env_vars,
760            env_hidden: self.env_hidden.clone(),
761            env_hide_history: self.env_hide_history.clone(),
762            active_overlays: self.active_overlays.clone(),
763            arguments: ArgumentStack::new(),
764            error_handlers: ErrorHandlerStack::new(),
765            finally_run_handlers: ErrorHandlerStack::new(),
766            recursion_count: self.recursion_count,
767            parent_stack: Some(Arc::new(self.clone())),
768            parent_deletions: vec![],
769            deletions: vec![],
770            config: self.config.clone(),
771            out_dest: self.out_dest.clone(),
772            suppress_stdin: self.suppress_stdin,
773            // Inherit caller block bindings so nested closures still see outer local defs.
774            active_scope_bindings: self.active_scope_bindings.clone(),
775            ir_scope_regions: vec![],
776            ir_instruction_index: None,
777            // Share last-result so closures can still read `$ans`.
778            last_result: self.last_result.clone(),
779        }
780    }
781
782    /// Flatten the env var scope frames into one frame
783    pub fn get_env_vars(&self, engine_state: &EngineState) -> HashMap<String, Value> {
784        let mut result = HashMap::new();
785
786        for active_overlay in self.active_overlays.iter() {
787            if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
788                result.extend(
789                    env_vars
790                        .iter()
791                        .filter(|(k, _)| {
792                            if let Some(env_hidden) = self.env_hidden.get(active_overlay) {
793                                !env_hidden.contains(*k)
794                            } else {
795                                // nothing has been hidden in this overlay
796                                true
797                            }
798                        })
799                        .map(|(k, v)| (k.as_str().to_string(), v.clone()))
800                        .collect::<HashMap<String, Value>>(),
801                );
802            }
803        }
804
805        result.extend(self.get_stack_env_vars());
806
807        result
808    }
809
810    /// Get flattened environment variables only from the stack
811    pub fn get_stack_env_vars(&self) -> HashMap<String, Value> {
812        let mut result = HashMap::new();
813
814        for scope in &self.env_vars {
815            for active_overlay in self.active_overlays.iter() {
816                if let Some(env_vars) = scope.get(active_overlay) {
817                    result.extend(
818                        env_vars
819                            .iter()
820                            .map(|(k, v)| (k.as_str().to_string(), v.clone())),
821                    );
822                }
823            }
824        }
825
826        result
827    }
828
829    /// Get flattened environment variables only from the stack and one overlay
830    pub fn get_stack_overlay_env_vars(&self, overlay_name: &str) -> HashMap<String, Value> {
831        let mut result = HashMap::new();
832
833        for scope in &self.env_vars {
834            if let Some(active_overlay) = self.active_overlays.iter().find(|n| n == &overlay_name)
835                && let Some(env_vars) = scope.get(active_overlay)
836            {
837                result.extend(
838                    env_vars
839                        .iter()
840                        .map(|(k, v)| (k.as_str().to_string(), v.clone())),
841                );
842            }
843        }
844
845        result
846    }
847
848    /// Get hidden envs, but without envs defined previously in `excluded_overlay_name`.
849    pub fn get_hidden_env_vars(
850        &self,
851        excluded_overlay_name: &str,
852        engine_state: &EngineState,
853    ) -> HashMap<String, Value> {
854        let mut result = HashMap::new();
855
856        for overlay_name in self.active_overlays.iter().rev() {
857            if overlay_name == excluded_overlay_name {
858                continue;
859            }
860            if let Some(env_names) = self.env_hidden.get(overlay_name) {
861                for n in env_names {
862                    if result.contains_key(n.as_str()) {
863                        continue;
864                    }
865                    // get env value.
866                    if let Some(Some(v)) = engine_state
867                        .env_vars
868                        .get(overlay_name)
869                        .map(|env_vars| env_vars.get(n))
870                    {
871                        result.insert(n.as_str().to_string(), v.clone());
872                    }
873                }
874            }
875        }
876        result
877    }
878
879    /// Same as get_env_vars, but returns only the names as a HashSet
880    pub fn get_env_var_names(&self, engine_state: &EngineState) -> HashSet<String> {
881        let mut result = HashSet::new();
882
883        for active_overlay in self.active_overlays.iter() {
884            if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
885                result.extend(
886                    env_vars
887                        .keys()
888                        .filter(|k| {
889                            if let Some(env_hidden) = self.env_hidden.get(active_overlay) {
890                                !env_hidden.contains(*k)
891                            } else {
892                                // nothing has been hidden in this overlay
893                                true
894                            }
895                        })
896                        .map(|k| k.as_str().to_string())
897                        .collect::<HashSet<String>>(),
898                );
899            }
900        }
901
902        for scope in &self.env_vars {
903            for active_overlay in self.active_overlays.iter() {
904                if let Some(env_vars) = scope.get(active_overlay) {
905                    result.extend(
906                        env_vars
907                            .keys()
908                            .map(|k| k.as_str().to_string())
909                            .collect::<HashSet<String>>(),
910                    );
911                }
912            }
913        }
914
915        result
916    }
917
918    pub fn get_env_var<'a>(
919        &'a self,
920        engine_state: &'a EngineState,
921        name: &str,
922    ) -> Option<&'a Value> {
923        let env_name = EnvName::from(name);
924
925        for scope in self.env_vars.iter().rev() {
926            for active_overlay in self.active_overlays.iter().rev() {
927                if let Some(env_vars) = scope.get(active_overlay)
928                    && let Some(v) = env_vars.get(&env_name)
929                {
930                    return Some(v);
931                }
932            }
933        }
934
935        for active_overlay in self.active_overlays.iter().rev() {
936            if !self.is_env_hidden_in_overlay(active_overlay, &env_name)
937                && let Some(env_vars) = engine_state.env_vars.get(active_overlay)
938                && let Some(v) = env_vars.get(&env_name)
939            {
940                return Some(v);
941            }
942        }
943        None
944    }
945
946    pub fn has_env_var(&self, engine_state: &EngineState, name: &str) -> bool {
947        let env_name = EnvName::from(name);
948
949        for scope in self.env_vars.iter().rev() {
950            for active_overlay in self.active_overlays.iter().rev() {
951                if let Some(env_vars) = scope.get(active_overlay)
952                    && env_vars.contains_key(&env_name)
953                {
954                    return true;
955                }
956            }
957        }
958
959        for active_overlay in self.active_overlays.iter().rev() {
960            if !self.is_env_hidden_in_overlay(active_overlay, &env_name)
961                && let Some(env_vars) = engine_state.env_vars.get(active_overlay)
962                && env_vars.contains_key(&env_name)
963            {
964                return true;
965            }
966        }
967
968        false
969    }
970
971    /// Removes `name` from the stack. If it was not on the stack and lives in `engine_state`,
972    /// marks it hidden in `env_hidden`. Returns `true` if the variable was found and removed.
973    ///
974    /// Use this for temporary bookkeeping removals (e.g. `FILE_PWD`, canary variables) where
975    /// the goal is to clean up a stack-level value without necessarily hiding the engine-state
976    /// baseline. Use [`Self::hide_env_var`] when the intent is to make the variable invisible
977    /// to subsequent lookups (e.g. `hide-env`).
978    pub fn remove_env_var(&mut self, engine_state: &EngineState, name: &str) -> bool {
979        let env_name = EnvName::from(name);
980
981        self.remove_env_var_from_stack(&env_name)
982            || self.hide_engine_state_env_var(engine_state, &env_name)
983    }
984
985    /// Removes `env_name` from all stack scopes and returns `true` if it was found.
986    /// Does not affect `env_hidden`; use [`Self::hide_env_var`] for full hiding semantics.
987    fn remove_env_var_from_stack(&mut self, env_name: &EnvName) -> bool {
988        self.env_vars
989            .iter_mut()
990            .rev()
991            .map(Arc::make_mut)
992            .find_map(|scope| {
993                self.active_overlays
994                    .iter()
995                    .rev()
996                    .find_map(|active_overlay| scope.get_mut(active_overlay)?.remove(env_name))
997            })
998            .is_some()
999    }
1000
1001    /// Marks `env_name` as hidden in `env_hidden` for the active overlay where it exists in
1002    /// `engine_state`.
1003    ///
1004    /// Returns `true` only when the baseline variable exists and was newly hidden.
1005    fn hide_engine_state_env_var(
1006        &mut self,
1007        engine_state: &EngineState,
1008        env_name: &EnvName,
1009    ) -> bool {
1010        let overlay_containing_env_var = self.active_overlays.iter().rev().find(|active_overlay| {
1011            engine_state
1012                .env_vars
1013                .get(active_overlay.as_str())
1014                .is_some_and(|env_vars| env_vars.contains_key(env_name))
1015        });
1016
1017        let Some(overlay_containing_env_var) = overlay_containing_env_var else {
1018            return false;
1019        };
1020
1021        let env_hidden = Arc::make_mut(&mut self.env_hidden);
1022
1023        if env_hidden
1024            .get(overlay_containing_env_var.as_str())
1025            .is_some_and(|hidden_vars| hidden_vars.contains(env_name))
1026        {
1027            return false;
1028        }
1029
1030        env_hidden
1031            .entry(overlay_containing_env_var.clone())
1032            .or_default()
1033            .insert(env_name.clone());
1034        true
1035    }
1036
1037    /// Records that `env_name` has been hidden in the active overlay and returns `false` if it
1038    /// was already recorded as hidden there.
1039    fn record_env_var_hide_in_active_overlay(&mut self, env_name: &EnvName) -> bool {
1040        let Some(active_overlay) = self.active_overlays.last().cloned() else {
1041            return false;
1042        };
1043
1044        Arc::make_mut(&mut self.env_hide_history)
1045            .entry(active_overlay)
1046            .or_default()
1047            .insert(env_name.clone())
1048    }
1049
1050    fn is_env_var_hide_recorded(&self, env_name: &EnvName) -> bool {
1051        self.active_overlays
1052            .iter()
1053            .rev()
1054            .filter_map(|overlay| self.env_hide_history.get(overlay))
1055            .any(|hidden_vars| hidden_vars.contains(env_name))
1056    }
1057
1058    fn is_env_hidden_in_overlay(&self, overlay: &str, env_name: &EnvName) -> bool {
1059        self.env_hidden
1060            .get(overlay)
1061            .is_some_and(|hidden_vars| hidden_vars.contains(env_name))
1062    }
1063
1064    /// Returns `true` if `name` was hidden in this stack context (e.g. by `hide-env`), either by
1065    /// masking an `engine_state` baseline value or by removing a stack-level value.
1066    ///
1067    /// A variable that was re-added after being hidden is still reported as hidden here, so only
1068    /// use this after a failed lookup to distinguish "hidden" from "never set".
1069    pub fn is_env_var_hidden(&self, name: &str) -> bool {
1070        let env_name = EnvName::from(name);
1071
1072        self.active_overlays
1073            .iter()
1074            .rev()
1075            .any(|overlay| self.is_env_hidden_in_overlay(overlay, &env_name))
1076            || self.is_env_var_hide_recorded(&env_name)
1077    }
1078
1079    /// Hides `name` so it is no longer visible to subsequent lookups. Removes it from the stack
1080    /// and, if no stack shadowing remains, also marks the `engine_state` baseline as hidden in
1081    /// `env_hidden`. Returns `true` if the variable was found.
1082    ///
1083    /// This is the correct method for `hide-env` and `redirect_env`; it ensures that a variable
1084    /// set in engine_state (from a previous REPL merge) cannot be seen after hiding even when a
1085    /// stack-level override (e.g. an empty-string assignment) was present at hide time.
1086    pub fn hide_env_var(&mut self, engine_state: &EngineState, name: &str) -> bool {
1087        let env_name = EnvName::from(name);
1088
1089        // Re-hiding the same env var in the same scope should report not found.
1090        if self.is_env_var_hide_recorded(&env_name) {
1091            return false;
1092        }
1093
1094        if self.remove_env_var_from_stack(&env_name) {
1095            self.record_env_var_hide_in_active_overlay(&env_name);
1096
1097            if !self.has_env_var_in_stack(&env_name) {
1098                self.hide_engine_state_env_var(engine_state, &env_name);
1099            }
1100            return true;
1101        }
1102
1103        if self.hide_engine_state_env_var(engine_state, &env_name) {
1104            self.record_env_var_hide_in_active_overlay(&env_name);
1105            return true;
1106        }
1107
1108        false
1109    }
1110
1111    /// Returns `true` if `name` exists in any stack scope (without consulting `engine_state`).
1112    fn has_env_var_in_stack(&self, name: &EnvName) -> bool {
1113        self.env_vars.iter().rev().any(|scope| {
1114            self.active_overlays
1115                .iter()
1116                .rev()
1117                .filter_map(|active_overlay| scope.get(active_overlay))
1118                .any(|env_vars| env_vars.contains_key(name))
1119        })
1120    }
1121
1122    pub fn has_env_overlay(&self, name: &str, engine_state: &EngineState) -> bool {
1123        for scope in self.env_vars.iter().rev() {
1124            if scope.contains_key(name) {
1125                return true;
1126            }
1127        }
1128
1129        engine_state.env_vars.contains_key(name)
1130    }
1131
1132    pub fn is_overlay_active(&self, name: &str) -> bool {
1133        self.active_overlays.iter().any(|n| n == name)
1134    }
1135
1136    pub fn add_overlay(&mut self, name: String) {
1137        self.active_overlays.retain(|o| o != &name);
1138        self.active_overlays.push(name);
1139    }
1140
1141    pub fn remove_overlay(&mut self, name: &str) {
1142        self.active_overlays.retain(|o| o != name);
1143    }
1144
1145    /// Returns the [`OutDest`] to use for the current command's stdout.
1146    ///
1147    /// This will be the pipe redirection if one is set,
1148    /// otherwise it will be the current file redirection,
1149    /// otherwise it will be the process's stdout indicated by [`OutDest::Inherit`].
1150    pub fn stdout(&self) -> &OutDest {
1151        self.out_dest.stdout()
1152    }
1153
1154    /// Returns the [`OutDest`] to use for the current command's stderr.
1155    ///
1156    /// This will be the pipe redirection if one is set,
1157    /// otherwise it will be the current file redirection,
1158    /// otherwise it will be the process's stderr indicated by [`OutDest::Inherit`].
1159    pub fn stderr(&self) -> &OutDest {
1160        self.out_dest.stderr()
1161    }
1162
1163    /// Returns the [`OutDest`] of the pipe redirection applied to the current command's stdout.
1164    pub fn pipe_stdout(&self) -> Option<&OutDest> {
1165        self.out_dest.pipe_stdout.as_ref()
1166    }
1167
1168    /// Returns the [`OutDest`] of the pipe redirection applied to the current command's stderr.
1169    pub fn pipe_stderr(&self) -> Option<&OutDest> {
1170        self.out_dest.pipe_stderr.as_ref()
1171    }
1172
1173    /// Returns the stdout destination of the innermost active custom-command invocation, if any.
1174    ///
1175    /// This is the destination of that command's *return value*. It stays stable even when
1176    /// intermediate expressions temporarily set [`OutDest::Value`] (e.g. `if (…)`), so callers
1177    /// can answer "where does *this command* go?" from anywhere in the body.
1178    ///
1179    /// See also [`Self::is_stdout_redirected`] and [`StackWithInvocation`].
1180    pub fn invocation_stdout(&self) -> Option<&OutDest> {
1181        self.out_dest.invocation_stdout.last()
1182    }
1183
1184    /// Whether the current custom command's return value is redirected away from display.
1185    ///
1186    /// Uses the active [`Self::invocation_stdout`] frame when inside a custom command so the
1187    /// answer is stable across nested `if` / `let` collection. Outside a custom command, falls
1188    /// back to [`Self::stdout`].
1189    ///
1190    /// Semantics match [`OutDest::is_redirected`] (only [`OutDest::Print`] is not redirected).
1191    /// This is the engine-side helper behind the `is-redirected` command.
1192    #[must_use]
1193    pub fn is_stdout_redirected(&self) -> bool {
1194        self.invocation_stdout()
1195            .unwrap_or_else(|| self.stdout())
1196            .is_redirected()
1197    }
1198
1199    /// Wrap this stack with an invocation-stdout frame for a custom command about to run.
1200    ///
1201    /// Push the destination of the call's *return value* (typically
1202    /// `caller_stack.stdout().clone()` after redirections are applied). The frame is popped when
1203    /// the returned [`StackWithInvocation`] is dropped.
1204    ///
1205    /// # Why a separate frame?
1206    ///
1207    /// Intermediate evaluation sets [`OutDest::Value`] via [`Self::start_collect_value`]. Without
1208    /// an invocation frame, queries like `is-redirected` inside `if (…)` would always see
1209    /// `Value` and report redirected—even when the enclosing custom command's result is printed.
1210    pub fn with_invocation_stdout(self, dest: OutDest) -> StackWithInvocation {
1211        StackWithInvocation::new(self, dest)
1212    }
1213
1214    /// Temporarily set the pipe stdout redirection to [`OutDest::Value`].
1215    ///
1216    /// This is used before evaluating an expression into a `Value`.
1217    pub fn start_collect_value(&mut self) -> StackCollectValueGuard<'_> {
1218        StackCollectValueGuard::new(self)
1219    }
1220
1221    /// Temporarily use the output redirections in the parent scope.
1222    ///
1223    /// This is used before evaluating an argument to a call.
1224    pub fn use_call_arg_out_dest(&mut self) -> StackCallArgGuard<'_> {
1225        StackCallArgGuard::new(self)
1226    }
1227
1228    /// Temporarily apply redirections to stdout and/or stderr.
1229    pub fn push_redirection(
1230        &mut self,
1231        stdout: Option<Redirection>,
1232        stderr: Option<Redirection>,
1233    ) -> StackIoGuard<'_> {
1234        StackIoGuard::new(self, stdout, stderr)
1235    }
1236
1237    /// Mark stdout for the last command as [`OutDest::Value`].
1238    ///
1239    /// This will irreversibly alter the output redirections, and so it only makes sense to use this on an owned `Stack`
1240    /// (which is why this function does not take `&mut self`).
1241    ///
1242    /// See [`Stack::start_collect_value`] which can temporarily set stdout as [`OutDest::Value`] for a mutable `Stack` reference.
1243    pub fn collect_value(mut self) -> Self {
1244        self.out_dest.pipe_stdout = Some(OutDest::Value);
1245        self.out_dest.pipe_stderr = None;
1246        self
1247    }
1248
1249    /// Mark both stdout and stderr for the last command as [`OutDest::Value`].
1250    ///
1251    /// This captures all output (stdout and stderr) instead of letting it inherit
1252    /// to the process's terminal. Useful for programmatic contexts like MCP servers
1253    /// where all output must be captured and returned.
1254    ///
1255    /// This will irreversibly alter the output redirections, and so it only makes sense to use this on an owned `Stack`
1256    /// (which is why this function does not take `&mut self`).
1257    pub fn capture_all(mut self) -> Self {
1258        self.out_dest.pipe_stdout = Some(OutDest::Value);
1259        self.out_dest.pipe_stderr = Some(OutDest::Value);
1260        self
1261    }
1262
1263    /// Clears any pipe and file redirections and resets stdout and stderr to [`OutDest::Inherit`].
1264    ///
1265    /// This will irreversibly reset the output redirections, and so it only makes sense to use this on an owned `Stack`
1266    /// (which is why this function does not take `&mut self`).
1267    pub fn reset_out_dest(mut self) -> Self {
1268        self.out_dest = StackOutDest::new();
1269        self
1270    }
1271
1272    /// Redirects stdout and stderr to [`OutDest::Null`], discarding all output.
1273    ///
1274    /// Use this for background evaluation tasks (e.g., completion) that must
1275    /// never write to the terminal while reedline owns it.
1276    pub fn suppress_output(mut self) -> Self {
1277        self.out_dest.stdout = OutDest::Null;
1278        self.out_dest.stderr = OutDest::Null;
1279        self
1280    }
1281
1282    /// Causes external processes spawned with empty input to receive
1283    /// `/dev/null` for stdin instead of inheriting the terminal.
1284    ///
1285    /// Use this together with [`suppress_output`](Self::suppress_output) for
1286    /// background tasks (e.g. completion threads).  Without it, subprocesses
1287    /// spawned by closure-based completers (carapace, fish_complete, etc.)
1288    /// inherit the live terminal fd and can race with reedline's reads,
1289    /// causing `Input/output error` (EIO).
1290    pub fn suppress_stdin(mut self) -> Self {
1291        self.suppress_stdin = true;
1292        self
1293    }
1294
1295    /// Clears any pipe redirections, keeping the current stdout and stderr.
1296    ///
1297    /// This will irreversibly reset some of the output redirections, and so it only makes sense to use this on an owned `Stack`
1298    /// (which is why this function does not take `&mut self`).
1299    pub fn reset_pipes(mut self) -> Self {
1300        self.out_dest.pipe_stdout = None;
1301        self.out_dest.pipe_stderr = None;
1302        self
1303    }
1304
1305    /// Replaces the default stdout of the stack with a given file.
1306    ///
1307    /// This method configures the default stdout to redirect to a specified file.
1308    /// It is primarily useful for applications using `nu` as a language, where the stdout of
1309    /// external commands that are not explicitly piped can be redirected to a file.
1310    ///
1311    /// # Using Pipes
1312    ///
1313    /// For use in third-party applications pipes might be very useful as they allow using the
1314    /// stdout of external commands for different uses.
1315    /// For example the [`os_pipe`](https://docs.rs/os_pipe) crate provides an elegant way to
1316    /// access the stdout.
1317    ///
1318    /// ```
1319    /// # use std::{fs::File, io::{self, Read}, thread, error};
1320    /// # use nu_protocol::engine::Stack;
1321    /// #
1322    /// let (mut reader, writer) = os_pipe::pipe().unwrap();
1323    /// // Use a thread to avoid blocking the execution of the called command.
1324    /// let reader = thread::spawn(move || {
1325    ///     let mut buf: Vec<u8> = Vec::new();
1326    ///     reader.read_to_end(&mut buf)?;
1327    ///     Ok::<_, io::Error>(buf)
1328    /// });
1329    ///
1330    /// #[cfg(windows)]
1331    /// let file = std::os::windows::io::OwnedHandle::from(writer).into();
1332    /// #[cfg(unix)]
1333    /// let file = std::os::unix::io::OwnedFd::from(writer).into();
1334    ///
1335    /// let stack = Stack::new().stdout_file(file);
1336    ///
1337    /// // Execute some nu code.
1338    ///
1339    /// drop(stack); // drop the stack so that the writer will be dropped too
1340    /// let buf = reader.join().unwrap().unwrap();
1341    /// // Do with your buffer whatever you want.
1342    /// ```
1343    pub fn stdout_file(mut self, file: File) -> Self {
1344        self.out_dest.stdout = OutDest::File(Arc::new(file));
1345        self
1346    }
1347
1348    /// Replaces the default stderr of the stack with a given file.
1349    ///
1350    /// For more info, see [`stdout_file`](Self::stdout_file).
1351    pub fn stderr_file(mut self, file: File) -> Self {
1352        self.out_dest.stderr = OutDest::File(Arc::new(file));
1353        self
1354    }
1355
1356    /// Set the PWD environment variable to `path`.
1357    ///
1358    /// This method accepts `path` with trailing slashes, but they're removed
1359    /// before writing the value into PWD.
1360    pub fn set_cwd(&mut self, path: impl AsRef<std::path::Path>) -> Result<(), ShellError> {
1361        // Helper function to create a simple generic error.
1362        // Its messages are not especially helpful, but these errors don't occur often, so it's probably fine.
1363        fn error(msg: &str) -> Result<(), ShellError> {
1364            Err(ShellError::Generic(GenericError::new_internal(
1365                msg.to_string(),
1366                "",
1367            )))
1368        }
1369
1370        let path = path.as_ref();
1371
1372        if !path.is_absolute() {
1373            if let Some(Component::Prefix(_)) = path.components().next() {
1374                return Err(ShellError::Generic(
1375                    GenericError::new_internal("Cannot set $env.PWD to a prefix-only path", "")
1376                        .with_help(format!(
1377                            "Try to use {}{MAIN_SEPARATOR} instead",
1378                            path.display()
1379                        )),
1380                ));
1381            }
1382
1383            error("Cannot set $env.PWD to a non-absolute path")
1384        } else if !path.exists() {
1385            error("Cannot set $env.PWD to a non-existent directory")
1386        } else if !path.is_dir() {
1387            error("Cannot set $env.PWD to a non-directory")
1388        } else {
1389            // Strip trailing slashes, if any.
1390            let path = nu_path::strip_trailing_slash(path);
1391            let value = Value::string(path.to_string_lossy(), Span::unknown());
1392            self.add_env_var("PWD".into(), value);
1393            Ok(())
1394        }
1395    }
1396}
1397
1398#[cfg(test)]
1399mod test {
1400    use std::sync::Arc;
1401
1402    use crate::{Span, Value, VarId, engine::EngineState};
1403
1404    use super::Stack;
1405
1406    #[test]
1407    fn test_children_see_inner_values() {
1408        let mut original = Stack::new();
1409        original.add_var(VarId::new(0), Value::test_string("hello"));
1410
1411        let cloned = Stack::with_parent(Arc::new(original));
1412        assert_eq!(
1413            cloned.get_var(VarId::new(0), Span::test_data()),
1414            Ok(Value::test_string("hello"))
1415        );
1416    }
1417
1418    #[test]
1419    fn test_children_dont_see_deleted_values() {
1420        let mut original = Stack::new();
1421        original.add_var(VarId::new(0), Value::test_string("hello"));
1422
1423        let mut cloned = Stack::with_parent(Arc::new(original));
1424        cloned.remove_var(VarId::new(0));
1425
1426        assert_eq!(
1427            cloned.get_var(VarId::new(0), Span::test_data()),
1428            Err(crate::ShellError::VariableNotFoundAtRuntime {
1429                span: Span::test_data()
1430            })
1431        );
1432    }
1433
1434    #[test]
1435    fn test_children_changes_override_parent() {
1436        let mut original = Stack::new();
1437        original.add_var(VarId::new(0), Value::test_string("hello"));
1438
1439        let mut cloned = Stack::with_parent(Arc::new(original));
1440        cloned.add_var(VarId::new(0), Value::test_string("there"));
1441        assert_eq!(
1442            cloned.get_var(VarId::new(0), Span::test_data()),
1443            Ok(Value::test_string("there"))
1444        );
1445
1446        cloned.remove_var(VarId::new(0));
1447        // the underlying value shouldn't magically re-appear
1448        assert_eq!(
1449            cloned.get_var(VarId::new(0), Span::test_data()),
1450            Err(crate::ShellError::VariableNotFoundAtRuntime {
1451                span: Span::test_data()
1452            })
1453        );
1454    }
1455    #[test]
1456    fn test_children_changes_persist_in_offspring() {
1457        let mut original = Stack::new();
1458        original.add_var(VarId::new(0), Value::test_string("hello"));
1459
1460        let mut cloned = Stack::with_parent(Arc::new(original));
1461        cloned.add_var(VarId::new(1), Value::test_string("there"));
1462
1463        cloned.remove_var(VarId::new(0));
1464        let cloned = Stack::with_parent(Arc::new(cloned));
1465
1466        assert_eq!(
1467            cloned.get_var(VarId::new(0), Span::test_data()),
1468            Err(crate::ShellError::VariableNotFoundAtRuntime {
1469                span: Span::test_data()
1470            })
1471        );
1472
1473        assert_eq!(
1474            cloned.get_var(VarId::new(1), Span::test_data()),
1475            Ok(Value::test_string("there"))
1476        );
1477    }
1478
1479    #[test]
1480    fn test_merging_children_back_to_parent() {
1481        let mut original = Stack::new();
1482        let engine_state = EngineState::new();
1483        original.add_var(VarId::new(0), Value::test_string("hello"));
1484
1485        let original_arc = Arc::new(original);
1486        let mut cloned = Stack::with_parent(original_arc.clone());
1487        cloned.add_var(VarId::new(1), Value::test_string("there"));
1488
1489        cloned.remove_var(VarId::new(0));
1490
1491        cloned.add_env_var(
1492            "ADDED_IN_CHILD".to_string(),
1493            Value::test_string("New Env Var"),
1494        );
1495
1496        let original = Stack::with_changes_from_child(original_arc, cloned);
1497
1498        assert_eq!(
1499            original.get_var(VarId::new(0), Span::test_data()),
1500            Err(crate::ShellError::VariableNotFoundAtRuntime {
1501                span: Span::test_data()
1502            })
1503        );
1504
1505        assert_eq!(
1506            original.get_var(VarId::new(1), Span::test_data()),
1507            Ok(Value::test_string("there"))
1508        );
1509
1510        assert_eq!(
1511            original
1512                .get_env_var(&engine_state, "ADDED_IN_CHILD")
1513                .cloned(),
1514            Some(Value::test_string("New Env Var")),
1515        );
1516    }
1517
1518    #[test]
1519    fn test_get_var_mut_local_in_place() {
1520        use crate::ast::PathMember;
1521        use crate::casing::Casing;
1522        use crate::record;
1523
1524        let mut stack = Stack::new();
1525        let var_id = VarId::new(0);
1526        stack.add_var(
1527            var_id,
1528            Value::test_record(record! { "a" => Value::test_int(1) }),
1529        );
1530
1531        let path = vec![PathMember::test_string("a", false, Casing::Sensitive)];
1532        stack
1533            .upsert_var_cell_path(var_id, &path, Value::test_int(2), Span::test_data())
1534            .expect("upsert should succeed");
1535
1536        assert_eq!(
1537            stack.get_var(var_id, Span::test_data()),
1538            Ok(Value::test_record(record! { "a" => Value::test_int(2) }))
1539        );
1540        // Still a single local binding (no extra shadow entries).
1541        assert_eq!(stack.vars.len(), 1);
1542    }
1543
1544    #[test]
1545    fn test_get_var_mut_pulls_from_parent() {
1546        use crate::ast::PathMember;
1547        use crate::casing::Casing;
1548        use crate::record;
1549
1550        let mut parent = Stack::new();
1551        let var_id = VarId::new(0);
1552        parent.add_var(
1553            var_id,
1554            Value::test_record(record! { "a" => Value::test_int(1) }),
1555        );
1556
1557        let mut child = Stack::with_parent(Arc::new(parent));
1558        assert!(child.vars.is_empty());
1559
1560        let path = vec![PathMember::test_string("a", false, Casing::Sensitive)];
1561        child
1562            .upsert_var_cell_path(var_id, &path, Value::test_int(9), Span::test_data())
1563            .expect("upsert should succeed");
1564
1565        // Value was pulled into the child frame, then mutated.
1566        assert_eq!(child.vars.len(), 1);
1567        assert_eq!(
1568            child.get_var(var_id, Span::test_data()),
1569            Ok(Value::test_record(record! { "a" => Value::test_int(9) }))
1570        );
1571
1572        // Second mutation hits the local copy.
1573        child
1574            .upsert_var_cell_path(var_id, &path, Value::test_int(10), Span::test_data())
1575            .expect("second upsert should succeed");
1576        assert_eq!(child.vars.len(), 1);
1577        assert_eq!(
1578            child.get_var(var_id, Span::test_data()),
1579            Ok(Value::test_record(record! { "a" => Value::test_int(10) }))
1580        );
1581    }
1582
1583    #[test]
1584    fn test_upsert_var_cell_path_missing_and_deleted() {
1585        use crate::ast::PathMember;
1586        use crate::casing::Casing;
1587
1588        let mut stack = Stack::new();
1589        let var_id = VarId::new(0);
1590        let path = vec![PathMember::test_string("a", false, Casing::Sensitive)];
1591
1592        assert!(matches!(
1593            stack.upsert_var_cell_path(var_id, &path, Value::test_int(1), Span::test_data()),
1594            Err(crate::ShellError::VariableNotFoundAtRuntime { .. })
1595        ));
1596
1597        let mut parent = Stack::new();
1598        parent.add_var(var_id, Value::test_int(1));
1599        let mut child = Stack::with_parent(Arc::new(parent));
1600        child.remove_var(var_id);
1601
1602        assert!(matches!(
1603            child.upsert_var_cell_path(var_id, &path, Value::test_int(2), Span::test_data()),
1604            Err(crate::ShellError::VariableNotFoundAtRuntime { .. })
1605        ));
1606        assert!(child.get_var_mut(var_id).is_none());
1607    }
1608}