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