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