Skip to main content

nu_protocol/engine/
engine_state.rs

1use crate::{
2    BlockId, Config, DeclId, FileId, GetSpan, Handlers, HistoryConfig, JobId, Module, ModuleId,
3    OverlayId, ShellError, SignalAction, Signals, Signature, Span, SpanId, Type, Value, VarId,
4    VirtualPathId,
5    ast::{Block, Expr},
6    debugger::{Debugger, NoopDebugger},
7    engine::{
8        CachedFile, Command, DEFAULT_OVERLAY_NAME, EnvName, EnvVars, OverlayFrame, PromptState,
9        ScopeFrame, Stack, StateDelta, Variable, Visibility,
10        description::{Doccomments, build_desc},
11    },
12    eval_const::create_nu_constant,
13    report_error::ReportLog,
14    shell_error::{generic::GenericError, io::IoError},
15};
16use fancy_regex::Regex;
17use lru::LruCache;
18use nu_config::NushellConfigDirs;
19use nu_path::AbsolutePathBuf;
20use std::{
21    collections::{HashMap, HashSet},
22    num::NonZeroUsize,
23    path::PathBuf,
24    sync::{
25        Arc, Mutex, MutexGuard, PoisonError,
26        atomic::{AtomicBool, AtomicU32, Ordering},
27        mpsc::Sender,
28        mpsc::channel,
29    },
30};
31
32type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
33
34#[cfg(feature = "plugin")]
35use crate::{PluginRegistryFile, PluginRegistryItem, RegisteredPlugin};
36
37use super::{CurrentJob, Jobs, Mail, Mailbox, ThreadJob};
38
39/// Configure whether the current working directory may be updated when [`EngineState::merge_env`]
40/// is called.
41///
42/// During testing, this is causing issues, so this may disable it.
43pub static UPDATE_CWD: AtomicBool = AtomicBool::new(true);
44
45#[derive(Clone, Debug)]
46pub enum VirtualPath {
47    File(FileId),
48    Dir(Vec<VirtualPathId>),
49}
50
51#[derive(Debug, Default)]
52pub struct ReplState {
53    pub buffer: String,
54    // A byte position, as `EditCommand::MoveToPosition` is also a byte position
55    pub cursor_pos: usize,
56    /// Immediately accept the buffer on the next loop.
57    pub accept: bool,
58}
59
60#[derive(Debug)]
61pub struct IsDebugging(AtomicBool);
62
63impl IsDebugging {
64    pub fn new(val: bool) -> Self {
65        IsDebugging(AtomicBool::new(val))
66    }
67}
68
69impl Clone for IsDebugging {
70    fn clone(&self) -> Self {
71        IsDebugging(AtomicBool::new(self.0.load(Ordering::Relaxed)))
72    }
73}
74
75/// The core global engine state. This includes all global definitions as well as any global state that
76/// will persist for the whole session.
77///
78/// Declarations, variables, blocks, and other forms of data are held in the global state and referenced
79/// elsewhere using their IDs. These IDs are simply their index into the global state. This allows us to
80/// more easily handle creating blocks, binding variables and callsites, and more, because each of these
81/// will refer to the corresponding IDs rather than their definitions directly. At runtime, this means
82/// less copying and smaller structures.
83///
84/// Many of the larger objects in this structure are stored within `Arc` to decrease the cost of
85/// cloning `EngineState`. While `Arc`s are generally immutable, they can be modified using
86/// `Arc::make_mut`, which automatically clones to a new allocation if there are other copies of
87/// the `Arc` already in use, but will let us modify the `Arc` directly if we have the only
88/// reference to it.
89///
90/// Note that the runtime stack is not part of this global state. Runtime stacks are handled differently,
91/// but they also rely on using IDs rather than full definitions.
92#[derive(Clone, derive_more::Debug)]
93pub struct EngineState {
94    files: Vec<CachedFile>,
95    pub(super) virtual_paths: Vec<(String, VirtualPath)>,
96    vars: Vec<Variable>,
97    #[debug("{:?}", decls.iter().map(|c| c.name()).collect::<Vec<_>>())]
98    decls: Arc<Vec<Box<dyn Command + 'static>>>,
99    // The Vec is wrapped in Arc so that if we don't need to modify the list, we can just clone
100    // the reference and not have to clone each individual Arc inside. These lists can be
101    // especially long, so it helps
102    #[debug("{:?}", blocks.iter().map(|b| &b.signature.name))]
103    pub(super) blocks: Arc<Vec<Arc<Block>>>,
104    #[debug("{:?}", modules.iter().map(|m| String::from_utf8_lossy(&m.name)))]
105    pub(super) modules: Arc<Vec<Arc<Module>>>,
106    pub spans: Vec<Span>,
107    doccomments: Doccomments,
108    pub scope: ScopeFrame,
109    signals: Signals,
110    pub signal_handlers: Option<Handlers>,
111    pub env_vars: Arc<EnvVars>,
112    pub previous_env_vars: Arc<HashMap<EnvName, Value>>,
113    pub config: Arc<Config>,
114    pub pipeline_externals_state: Arc<(AtomicU32, AtomicU32)>,
115    pub repl_state: Arc<Mutex<ReplState>>,
116    /// Shared source of truth for the interactive prompt's rendered content. The
117    /// REPL fills it each cycle from `$env.PROMPT_COMMAND` and friends; a
118    /// background job's `commandline set-prompt` overwrites individual segments
119    /// and triggers an in-place repaint. Shared with every job that clones this
120    /// engine state.
121    pub prompt_state: Arc<PromptState>,
122    pub table_decl_id: Option<DeclId>,
123    #[cfg(feature = "plugin")]
124    pub plugin_path: Option<PathBuf>,
125    #[cfg(feature = "plugin")]
126    #[debug("{:?}", plugins.iter().map(|rp| rp.identity().name()).collect::<Vec<_>>())]
127    plugins: Vec<Arc<dyn RegisteredPlugin>>,
128    /// Resolved configuration directories and file paths.
129    ///
130    /// Populated once at startup by `nu_config::resolve_paths()` in `main.rs`.
131    /// All downstream code (config-file loading, `$nu` constant generation,
132    /// history backend, etc.) reads from this struct.
133    pub config_dirs: NushellConfigDirs,
134
135    pub history_enabled: bool,
136    pub history_session_id: i64,
137    /// Whether the startup-only `$env.config.history.*` options are locked from further
138    /// changes (currently `path`, `max_size`, `file_format`, `isolation`).
139    ///
140    /// Set to `true` once the REPL has finished initializing reedline's history backend.
141    /// After that point, changing any of these options would have no effect on the live
142    /// history, so attempts to mutate them are rejected with an error instead of being
143    /// silently ignored.
144    pub history_locked_after_startup: bool,
145    // Path to the file Nushell is currently evaluating, or None if we're in an interactive session.
146    pub file: Option<PathBuf>,
147    pub regex_cache: Arc<Mutex<LruCache<String, Regex>>>,
148    pub is_interactive: bool,
149    /// When true with [`Self::is_interactive`], REPL user-line evaluation may store `$ans`.
150    ///
151    /// Set only around true REPL command evaluation (`do_run_cmd`), not config/env/banner
152    /// startup, so startup scripts do not overwrite interactive last-result.
153    pub capture_repl_last_result: bool,
154    pub is_login: bool,
155    pub is_lsp: bool,
156    pub is_mcp: bool,
157    startup_time: i64,
158    is_debugging: IsDebugging,
159    pub debugger: Arc<Mutex<Box<dyn Debugger>>>,
160    pub report_log: Arc<Mutex<ReportLog>>,
161
162    pub jobs: Arc<Mutex<Jobs>>,
163
164    // The job being executed with this engine state, or None if main thread
165    pub current_job: CurrentJob,
166
167    pub root_job_sender: Sender<Mail>,
168
169    // When there are background jobs running, the interactive behavior of `exit` changes depending on
170    // the value of this flag:
171    // - if this is false, then a warning about running jobs is shown and `exit` enables this flag
172    // - if this is true, then `exit` will `std::process::exit`
173    //
174    // This ensures that running exit twice will terminate the program correctly
175    pub exit_warning_given: Arc<AtomicBool>,
176}
177
178// The max number of compiled regexes to keep around in a LRU cache, arbitrarily chosen
179const REGEX_CACHE_SIZE: usize = 100; // must be nonzero, otherwise will panic
180
181pub const NU_VARIABLE_ID: VarId = VarId::new(0);
182pub const IN_VARIABLE_ID: VarId = VarId::new(1);
183pub const ENV_VARIABLE_ID: VarId = VarId::new(2);
184/// Interactive last-result special variable.
185///
186/// The user-facing name is [`LAST_RESULT_VAR_NAME`] (e.g. `$ans`). Change that constant
187/// if the public name should differ; keep this ID stable.
188pub const LAST_VARIABLE_ID: VarId = VarId::new(3);
189/// Identifier for the last-result special variable **without** the `$` sigil.
190///
191/// Change this single constant to rename the binding site-wide (e.g. `"ans"` → `$ans`).
192/// The name is reserved (cannot be rebound with `let` / `mut` / `const`). Not user-configurable.
193///
194/// With a positive `max_last_result_size`, `$ans` is `{ last, exit_code, duration, command }`.
195/// With size `0`, `$ans` is `{ exit_code, duration, command }` (`last` omitted). `command` is
196/// the exact last REPL source (same buffer reedline stores in history). Resolved from
197/// the stack's last-result slot, not from captured locals — keep capture-discovery
198/// `>` checks in sync with this being the last special var id.
199pub const LAST_RESULT_VAR_NAME: &str = "ans";
200// NOTE: If you add more specials after LAST_VARIABLE_ID, update capture discovery
201// (`var_id > LAST_VARIABLE_ID`) and any other special-id thresholds.
202
203// The first span is unknown span
204pub const UNKNOWN_SPAN_ID: SpanId = SpanId::new(0);
205
206impl EngineState {
207    pub fn new() -> Self {
208        let (send, recv) = channel::<Mail>();
209
210        Self {
211            files: vec![],
212            virtual_paths: vec![],
213            vars: vec![
214                Variable::new(Span::new(0, 0), Type::Any, false),
215                Variable::new(Span::new(0, 0), Type::Any, false),
216                Variable::new(Span::new(0, 0), Type::Any, false),
217                Variable::new(Span::new(0, 0), Type::Any, false),
218                Variable::new(Span::new(0, 0), Type::Any, false),
219            ],
220            decls: Arc::new(vec![]),
221            blocks: Arc::new(vec![]),
222            modules: Arc::new(vec![Arc::new(Module::new(
223                DEFAULT_OVERLAY_NAME.as_bytes().to_vec(),
224            ))]),
225            spans: vec![Span::unknown()],
226            doccomments: Doccomments::new(),
227            // make sure we have some default overlay:
228            scope: ScopeFrame::with_empty_overlay(
229                DEFAULT_OVERLAY_NAME.as_bytes().to_vec(),
230                ModuleId::new(0),
231                false,
232            ),
233            signal_handlers: None,
234            signals: Signals::empty(),
235            env_vars: Arc::new(
236                [(DEFAULT_OVERLAY_NAME.to_string(), HashMap::new())]
237                    .into_iter()
238                    .collect(),
239            ),
240            previous_env_vars: Arc::new(HashMap::new()),
241            config: Arc::new(Config::default()),
242            pipeline_externals_state: Arc::new((AtomicU32::new(0), AtomicU32::new(0))),
243            repl_state: Arc::new(Mutex::new(ReplState {
244                buffer: "".to_string(),
245                cursor_pos: 0,
246                accept: false,
247            })),
248            prompt_state: Arc::new(PromptState::new()),
249            table_decl_id: None,
250            #[cfg(feature = "plugin")]
251            plugin_path: None,
252            #[cfg(feature = "plugin")]
253            plugins: vec![],
254            config_dirs: NushellConfigDirs::empty(),
255            history_enabled: true,
256            history_session_id: 0,
257            history_locked_after_startup: false,
258            file: None,
259            regex_cache: Arc::new(Mutex::new(LruCache::new(
260                NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
261            ))),
262            is_interactive: false,
263            capture_repl_last_result: false,
264            is_login: false,
265            is_lsp: false,
266            is_mcp: false,
267            startup_time: -1,
268            is_debugging: IsDebugging::new(false),
269            debugger: Arc::new(Mutex::new(Box::new(NoopDebugger))),
270            report_log: Arc::default(),
271            jobs: Arc::new(Mutex::new(Jobs::default())),
272            current_job: CurrentJob {
273                id: JobId::new(0),
274                background_thread_job: None,
275                mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
276            },
277            root_job_sender: send,
278            exit_warning_given: Arc::new(AtomicBool::new(false)),
279        }
280    }
281
282    pub fn signals(&self) -> &Signals {
283        &self.signals
284    }
285
286    /// Return a compiled regex, reusing the process-wide LRU cache when possible.
287    ///
288    /// On lock contention (or a poisoned mutex), compiles without touching the cache.
289    pub fn get_cached_regex(&self, pattern: &str) -> Result<Regex, fancy_regex::Error> {
290        match self.regex_cache.try_lock() {
291            Ok(mut cache) => cache
292                .try_get_or_insert_ref(pattern, || Regex::new(pattern))
293                .cloned(),
294            Err(_) => Regex::new(pattern),
295        }
296    }
297
298    /// Compile `pattern` via [`Self::get_cached_regex`], mapping failures to
299    /// [`ShellError::InvalidValue`] at `span`.
300    pub fn compile_regex(&self, pattern: &str, span: Span) -> Result<Regex, ShellError> {
301        self.get_cached_regex(pattern)
302            .map_err(|err| invalid_regex_value(pattern, err, span))
303    }
304
305    pub fn reset_signals(&mut self) {
306        self.signals.reset();
307        if let Some(ref handlers) = self.signal_handlers {
308            handlers.run(SignalAction::Reset);
309        }
310    }
311
312    pub fn set_signals(&mut self, signals: Signals) {
313        self.signals = signals;
314    }
315
316    /// Merges a `StateDelta` onto the current state. These deltas come from a system, like the parser, that
317    /// creates a new set of definitions and visible symbols in the current scope. We make this transactional
318    /// as there are times when we want to run the parser and immediately throw away the results (namely:
319    /// syntax highlighting and completions).
320    ///
321    /// When we want to preserve what the parser has created, we can take its output (the `StateDelta`) and
322    /// use this function to merge it into the global state.
323    pub fn merge_delta(&mut self, mut delta: StateDelta) -> Result<(), ShellError> {
324        // Take the mutable reference and extend the permanent state from the working set
325        self.files.extend(delta.files);
326        self.virtual_paths.extend(delta.virtual_paths);
327        self.vars.extend(delta.vars);
328        self.spans.extend(delta.spans);
329        self.doccomments.merge_with(delta.doccomments);
330
331        // Avoid potentially cloning the Arcs if we aren't adding anything
332        if !delta.decls.is_empty() {
333            Arc::make_mut(&mut self.decls).extend(delta.decls);
334        }
335        if !delta.blocks.is_empty() {
336            Arc::make_mut(&mut self.blocks).extend(delta.blocks);
337        }
338        if !delta.modules.is_empty() {
339            Arc::make_mut(&mut self.modules).extend(delta.modules);
340        }
341
342        let first = delta.scope.remove(0);
343
344        for (delta_name, delta_overlay) in first.clone().overlays {
345            if let Some((_, existing_overlay)) = self
346                .scope
347                .overlays
348                .iter_mut()
349                .find(|(name, _)| name == &delta_name)
350            {
351                // Updating existing overlay
352                for item in delta_overlay.decls.into_iter() {
353                    existing_overlay.decls.insert(item.0, item.1);
354                }
355                for item in delta_overlay.vars.into_iter() {
356                    existing_overlay.insert_variable(item.0, item.1);
357                }
358                for item in delta_overlay.modules.into_iter() {
359                    existing_overlay.modules.insert(item.0, item.1);
360                }
361
362                existing_overlay
363                    .visibility
364                    .merge_with(delta_overlay.visibility);
365            } else {
366                // New overlay was added to the delta
367                self.scope.overlays.push((delta_name, delta_overlay));
368            }
369        }
370
371        let mut activated_ids = self.translate_overlay_ids(&first);
372
373        let mut removed_ids = vec![];
374
375        for name in &first.removed_overlays {
376            if let Some(overlay_id) = self.find_overlay(name) {
377                removed_ids.push(overlay_id);
378            }
379        }
380
381        // Remove overlays removed in delta
382        self.scope
383            .active_overlays
384            .retain(|id| !removed_ids.contains(id));
385
386        // Move overlays activated in the delta to be first
387        self.scope
388            .active_overlays
389            .retain(|id| !activated_ids.contains(id));
390        self.scope.active_overlays.append(&mut activated_ids);
391
392        #[cfg(feature = "plugin")]
393        if !delta.plugins.is_empty() {
394            for plugin in std::mem::take(&mut delta.plugins) {
395                // Connect plugins to the signal handlers
396                if let Some(handlers) = &self.signal_handlers {
397                    plugin.clone().configure_signal_handler(handlers)?;
398                }
399
400                // Replace plugins that overlap in identity.
401                if let Some(existing) = self
402                    .plugins
403                    .iter_mut()
404                    .find(|p| p.identity().name() == plugin.identity().name())
405                {
406                    // Stop the existing plugin, so that the new plugin definitely takes over
407                    existing.stop()?;
408                    *existing = plugin;
409                } else {
410                    self.plugins.push(plugin);
411                }
412            }
413        }
414
415        #[cfg(feature = "plugin")]
416        if !delta.plugin_registry_items.is_empty() {
417            // Update the plugin file with the new signatures.
418            if self.plugin_path.is_some() {
419                self.update_plugin_file(std::mem::take(&mut delta.plugin_registry_items))?;
420            }
421        }
422
423        Ok(())
424    }
425
426    /// Merge the environment from the runtime Stack into the engine state
427    pub fn merge_env(&mut self, stack: &mut Stack) -> Result<(), ShellError> {
428        for mut scope in stack.env_vars.drain(..) {
429            for (overlay_name, mut env) in Arc::make_mut(&mut scope).drain() {
430                if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) {
431                    // Updating existing overlay
432                    env_vars.extend(env.drain());
433                } else {
434                    // Pushing a new overlay
435                    Arc::make_mut(&mut self.env_vars).insert(overlay_name, env);
436                }
437            }
438        }
439
440        if UPDATE_CWD.load(Ordering::Relaxed) {
441            let cwd = self.cwd(Some(stack))?;
442            std::env::set_current_dir(cwd)
443                .map_err(|err| IoError::new_internal(err, "Could not set current dir"))?;
444        }
445
446        if let Some(config) = stack.config.take() {
447            // If config was updated in the stack, replace it.
448            self.config = config;
449
450            // Make plugin GC config changes take effect immediately.
451            #[cfg(feature = "plugin")]
452            self.update_plugin_gc_configs(&self.config.plugin_gc);
453        }
454
455        Ok(())
456    }
457
458    /// Clean up unused variables from a Stack to prevent memory leaks.
459    /// This removes variables that are no longer referenced by any overlay or alias.
460    pub fn cleanup_stack_variables(&mut self, stack: &mut Stack) {
461        let mut shadowed_vars = HashSet::new();
462        for (_, frame) in self.scope.overlays.iter_mut() {
463            shadowed_vars.extend(frame.shadowed_vars.drain(..));
464        }
465
466        if shadowed_vars.is_empty() {
467            return;
468        }
469
470        // Collect VarIds still referenced by alias definitions so we don't
471        // remove them — doing so would cause a VariableNotFoundAtRuntime error
472        // when the alias is invoked after the variable has been re-bound.
473        let mut alias_var_ids = HashSet::new();
474        for decl in self.decls.iter() {
475            if let Some(alias) = decl.as_alias() {
476                collect_alias_var_ids(&alias.wrapped_call, &mut alias_var_ids);
477            }
478        }
479
480        // Remove variables from stack that are shadowed and not referenced by any alias
481        stack.vars.retain(|(var_id, _)| {
482            !shadowed_vars.contains(var_id) || alias_var_ids.contains(var_id)
483        });
484    }
485
486    pub fn active_overlay_ids<'a, 'b>(
487        &'b self,
488        removed_overlays: &'a [Vec<u8>],
489    ) -> impl DoubleEndedIterator<Item = &'b OverlayId> + 'a
490    where
491        'b: 'a,
492    {
493        self.scope.active_overlays.iter().filter(|id| {
494            !removed_overlays
495                .iter()
496                .any(|name| name == self.get_overlay_name(**id))
497        })
498    }
499
500    pub fn active_overlays<'a, 'b>(
501        &'b self,
502        removed_overlays: &'a [Vec<u8>],
503    ) -> impl DoubleEndedIterator<Item = &'b OverlayFrame> + 'a
504    where
505        'b: 'a,
506    {
507        self.active_overlay_ids(removed_overlays)
508            .map(|id| self.get_overlay(*id))
509    }
510
511    pub fn active_overlay_names<'a, 'b>(
512        &'b self,
513        removed_overlays: &'a [Vec<u8>],
514    ) -> impl DoubleEndedIterator<Item = &'b [u8]> + 'a
515    where
516        'b: 'a,
517    {
518        self.active_overlay_ids(removed_overlays)
519            .map(|id| self.get_overlay_name(*id))
520    }
521
522    /// Translate overlay IDs from other to IDs in self
523    fn translate_overlay_ids(&self, other: &ScopeFrame) -> Vec<OverlayId> {
524        let other_names = other.active_overlays.iter().map(|other_id| {
525            &other
526                .overlays
527                .get(other_id.get())
528                .expect("internal error: missing overlay")
529                .0
530        });
531
532        other_names
533            .map(|other_name| {
534                self.find_overlay(other_name)
535                    .expect("internal error: missing overlay")
536            })
537            .collect()
538    }
539
540    pub fn last_overlay_name(&self, removed_overlays: &[Vec<u8>]) -> &[u8] {
541        self.active_overlay_names(removed_overlays)
542            .last()
543            .expect("internal error: no active overlays")
544    }
545
546    pub fn last_overlay(&self, removed_overlays: &[Vec<u8>]) -> &OverlayFrame {
547        self.active_overlay_ids(removed_overlays)
548            .last()
549            .map(|id| self.get_overlay(*id))
550            .expect("internal error: no active overlays")
551    }
552
553    pub fn get_overlay_name(&self, overlay_id: OverlayId) -> &[u8] {
554        &self
555            .scope
556            .overlays
557            .get(overlay_id.get())
558            .expect("internal error: missing overlay")
559            .0
560    }
561
562    pub fn get_overlay(&self, overlay_id: OverlayId) -> &OverlayFrame {
563        &self
564            .scope
565            .overlays
566            .get(overlay_id.get())
567            .expect("internal error: missing overlay")
568            .1
569    }
570
571    pub fn render_env_vars(&self) -> HashMap<&str, &Value> {
572        let mut result: HashMap<&str, &Value> = HashMap::new();
573
574        for overlay_name in self.active_overlay_names(&[]) {
575            let name = String::from_utf8_lossy(overlay_name);
576            if let Some(env_vars) = self.env_vars.get(name.as_ref()) {
577                result.extend(env_vars.iter().map(|(k, v)| (k.as_str(), v)));
578            }
579        }
580
581        result
582    }
583
584    pub fn add_env_var(&mut self, name: String, val: Value) {
585        let overlay_name = String::from_utf8_lossy(self.last_overlay_name(&[])).to_string();
586
587        if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) {
588            env_vars.insert(EnvName::from(name), val);
589        } else {
590            Arc::make_mut(&mut self.env_vars).insert(
591                overlay_name,
592                [(EnvName::from(name), val)].into_iter().collect(),
593            );
594        }
595    }
596
597    pub fn get_env_var(&self, name: &str) -> Option<&Value> {
598        for overlay_id in self.scope.active_overlays.iter().rev() {
599            let overlay_name = String::from_utf8_lossy(self.get_overlay_name(*overlay_id));
600            if let Some(env_vars) = self.env_vars.get(overlay_name.as_ref())
601                && let Some(val) = env_vars.get(&EnvName::from(name))
602            {
603                return Some(val);
604            }
605        }
606
607        None
608    }
609
610    #[cfg(feature = "plugin")]
611    pub fn plugins(&self) -> &[Arc<dyn RegisteredPlugin>] {
612        &self.plugins
613    }
614
615    #[cfg(feature = "plugin")]
616    fn update_plugin_file(&self, updated_items: Vec<PluginRegistryItem>) -> Result<(), ShellError> {
617        // Updating the signatures plugin file with the added signatures
618        use std::fs::File;
619
620        let plugin_path = self.plugin_path.as_ref().ok_or_else(|| {
621            ShellError::Generic(
622                GenericError::new_internal("Plugin file path not set", "")
623                    .with_help("you may be running nu with --no-config-file"),
624            )
625        })?;
626
627        // Read the current contents of the plugin file if it exists
628        let mut contents = match File::open(plugin_path.as_path()) {
629            Ok(mut plugin_file) => PluginRegistryFile::read_from(&mut plugin_file, None),
630            Err(err) => {
631                if err.kind() == std::io::ErrorKind::NotFound {
632                    Ok(PluginRegistryFile::default())
633                } else {
634                    Err(ShellError::Io(IoError::new_internal_with_path(
635                        err,
636                        "Failed to open plugin file",
637                        PathBuf::from(plugin_path),
638                    )))
639                }
640            }
641        }?;
642
643        // Update the given signatures
644        for item in updated_items {
645            contents.upsert_plugin(item);
646        }
647
648        // Write it to the same path
649        let plugin_file = File::create(plugin_path.as_path()).map_err(|err| {
650            IoError::new_internal_with_path(
651                err,
652                "Failed to write plugin file",
653                PathBuf::from(plugin_path),
654            )
655        })?;
656
657        contents.write_to(plugin_file, None)
658    }
659
660    /// Update plugins with new garbage collection config
661    #[cfg(feature = "plugin")]
662    fn update_plugin_gc_configs(&self, plugin_gc: &crate::PluginGcConfigs) {
663        for plugin in &self.plugins {
664            plugin.set_gc_config(plugin_gc.get(plugin.identity().name()));
665        }
666    }
667
668    pub fn num_files(&self) -> usize {
669        self.files.len()
670    }
671
672    pub fn num_virtual_paths(&self) -> usize {
673        self.virtual_paths.len()
674    }
675
676    pub fn num_vars(&self) -> usize {
677        self.vars.len()
678    }
679
680    pub fn num_decls(&self) -> usize {
681        self.decls.len()
682    }
683
684    pub fn num_blocks(&self) -> usize {
685        self.blocks.len()
686    }
687
688    pub fn num_modules(&self) -> usize {
689        self.modules.len()
690    }
691
692    pub fn num_spans(&self) -> usize {
693        self.spans.len()
694    }
695    pub fn print_vars(&self) {
696        for var in self.vars.iter().enumerate() {
697            println!("var{}: {:?}", var.0, var.1);
698        }
699    }
700
701    pub fn print_decls(&self) {
702        for decl in self.decls.iter().enumerate() {
703            println!("decl{}: {:?}", decl.0, decl.1.signature());
704        }
705    }
706
707    pub fn print_blocks(&self) {
708        for block in self.blocks.iter().enumerate() {
709            println!("block{}: {:?}", block.0, block.1);
710        }
711    }
712
713    pub fn print_contents(&self) {
714        for cached_file in self.files.iter() {
715            let string = String::from_utf8_lossy(&cached_file.content);
716            println!("{string}");
717        }
718    }
719
720    /// Find the [`DeclId`](crate::DeclId) corresponding to a declaration with `name`.
721    ///
722    /// Searches within active overlays, and filtering out overlays in `removed_overlays`.
723    pub fn find_decl(&self, name: &[u8], removed_overlays: &[Vec<u8>]) -> Option<DeclId> {
724        let mut visibility: Visibility = Visibility::new();
725
726        for overlay_frame in self.active_overlays(removed_overlays).rev() {
727            visibility.append(&overlay_frame.visibility);
728
729            if let Some(decl_id) = overlay_frame.get_decl(name)
730                && visibility.is_decl_id_visible(&decl_id)
731            {
732                return Some(decl_id);
733            }
734        }
735
736        None
737    }
738
739    /// Find the name of the declaration corresponding to `decl_id`.
740    ///
741    /// Searches within active overlays, and filtering out overlays in `removed_overlays`.
742    pub fn find_decl_name(&self, decl_id: DeclId, removed_overlays: &[Vec<u8>]) -> Option<&[u8]> {
743        let mut visibility: Visibility = Visibility::new();
744
745        for overlay_frame in self.active_overlays(removed_overlays).rev() {
746            visibility.append(&overlay_frame.visibility);
747
748            if visibility.is_decl_id_visible(&decl_id) {
749                for (name, id) in overlay_frame.decls.iter() {
750                    if id == &decl_id {
751                        return Some(name);
752                    }
753                }
754            }
755        }
756
757        None
758    }
759
760    /// Find the [`OverlayId`](crate::OverlayId) corresponding to `name`.
761    ///
762    /// Searches all overlays, not just active overlays. To search only in active overlays, use [`find_active_overlay`](EngineState::find_active_overlay)
763    pub fn find_overlay(&self, name: &[u8]) -> Option<OverlayId> {
764        self.scope.find_overlay(name)
765    }
766
767    /// Find the [`OverlayId`](crate::OverlayId) of the active overlay corresponding to `name`.
768    ///
769    /// Searches only active overlays. To search in all overlays, use [`find_overlay`](EngineState::find_active_overlay)
770    pub fn find_active_overlay(&self, name: &[u8]) -> Option<OverlayId> {
771        self.scope.find_active_overlay(name)
772    }
773
774    /// Find the [`ModuleId`](crate::ModuleId) corresponding to `name`.
775    ///
776    /// Searches within active overlays, and filtering out overlays in `removed_overlays`.
777    pub fn find_module(&self, name: &[u8], removed_overlays: &[Vec<u8>]) -> Option<ModuleId> {
778        for overlay_frame in self.active_overlays(removed_overlays).rev() {
779            if let Some(module_id) = overlay_frame.modules.get(name) {
780                return Some(*module_id);
781            }
782        }
783
784        None
785    }
786
787    pub fn get_module_comments(&self, module_id: ModuleId) -> Option<&[Span]> {
788        self.doccomments.get_module_comments(module_id)
789    }
790
791    #[cfg(feature = "plugin")]
792    pub fn plugin_decls(&self) -> impl Iterator<Item = &Box<dyn Command + 'static>> {
793        let mut unique_plugin_decls = HashMap::new();
794
795        // Make sure there are no duplicate decls: Newer one overwrites the older one
796        for decl in self.decls.iter().filter(|d| d.is_plugin()) {
797            unique_plugin_decls.insert(decl.name(), decl);
798        }
799
800        let mut plugin_decls: Vec<(&str, &Box<dyn Command>)> =
801            unique_plugin_decls.into_iter().collect();
802
803        // Sort the plugins by name so we don't end up with a random plugin file each time
804        plugin_decls.sort_by(|a, b| a.0.cmp(b.0));
805        plugin_decls.into_iter().map(|(_, decl)| decl)
806    }
807
808    pub fn which_module_has_decl(
809        &self,
810        decl_name: &[u8],
811        removed_overlays: &[Vec<u8>],
812    ) -> Option<&[u8]> {
813        for overlay_frame in self.active_overlays(removed_overlays).rev() {
814            for (module_name, module_id) in overlay_frame.modules.iter() {
815                let module = self.get_module(*module_id);
816                if module.has_decl(decl_name) {
817                    return Some(module_name);
818                }
819            }
820        }
821
822        None
823    }
824
825    /// Apply a function to all commands. The function accepts a command name and its DeclId
826    pub fn traverse_commands(&self, mut f: impl FnMut(&[u8], DeclId)) {
827        for overlay_frame in self.active_overlays(&[]).rev() {
828            for (name, decl_id) in &overlay_frame.decls {
829                if overlay_frame.visibility.is_decl_id_visible(decl_id) {
830                    f(name, *decl_id);
831                }
832            }
833        }
834    }
835
836    pub fn get_span_contents(&self, span: Span) -> &[u8] {
837        self.try_get_file_contents(span).unwrap_or(&[0u8; 0])
838    }
839
840    pub fn try_get_file_contents(&self, span: Span) -> Option<&[u8]> {
841        self.files.iter().find_map(|file| {
842            if file.covered_span.contains_span(span) {
843                let start = span.start - file.covered_span.start;
844                let end = span.end - file.covered_span.start;
845                Some(&file.content[start..end])
846            } else {
847                None
848            }
849        })
850    }
851
852    /// If the span's content starts with the given prefix, return two subspans
853    /// corresponding to this prefix, and the rest of the content.
854    pub fn span_match_prefix(&self, span: Span, prefix: &[u8]) -> Option<(Span, Span)> {
855        let contents = self.get_span_contents(span);
856
857        if contents.starts_with(prefix) {
858            span.split_at(prefix.len())
859        } else {
860            None
861        }
862    }
863
864    /// If the span's content ends with the given postfix, return two subspans
865    /// corresponding to the rest of the content, and this postfix.
866    pub fn span_match_postfix(&self, span: Span, prefix: &[u8]) -> Option<(Span, Span)> {
867        let contents = self.get_span_contents(span);
868
869        if contents.ends_with(prefix) {
870            span.split_at(span.len() - prefix.len())
871        } else {
872            None
873        }
874    }
875
876    /// Get the global config from the engine state.
877    ///
878    /// Use [`Stack::get_config()`] instead whenever the `Stack` is available, as it takes into
879    /// account local changes to `$env.config`.
880    pub fn get_config(&self) -> &Arc<Config> {
881        &self.config
882    }
883
884    pub fn set_config(&mut self, conf: impl Into<Arc<Config>>) {
885        let conf = conf.into();
886
887        #[cfg(feature = "plugin")]
888        if conf.plugin_gc != self.config.plugin_gc {
889            // Make plugin GC config changes take effect immediately.
890            self.update_plugin_gc_configs(&conf.plugin_gc);
891        }
892
893        self.config = conf;
894    }
895
896    /// Fetch the configuration for a plugin
897    ///
898    /// The `plugin` must match the registered name of a plugin.  For `plugin add
899    /// nu_plugin_example` the plugin name to use will be `"example"`
900    pub fn get_plugin_config(&self, plugin: &str) -> Option<&Value> {
901        self.config.plugins.get(plugin)
902    }
903
904    /// Returns the configuration settings for command history or `None` if history is disabled
905    pub fn history_config(&self) -> Option<HistoryConfig> {
906        self.history_enabled.then(|| self.config.history.clone())
907    }
908
909    /// Resolve the history file path using the already-resolved config dirs.
910    ///
911    /// Returns `None` when history is disabled or the history path is set to
912    /// [`HistoryPath::Disabled`].
913    pub fn history_path(&self) -> Option<std::path::PathBuf> {
914        self.history_config()?
915            .file_path(&self.config_dirs.config_home)
916    }
917
918    pub fn get_var(&self, var_id: VarId) -> &Variable {
919        self.vars
920            .get(var_id.get())
921            .expect("internal error: missing variable")
922    }
923
924    pub fn get_constant(&self, var_id: VarId) -> Option<&Value> {
925        let var = self.get_var(var_id);
926        var.const_val.as_ref()
927    }
928
929    pub fn generate_nu_constant(&mut self) {
930        self.vars[NU_VARIABLE_ID.get()].const_val = Some(create_nu_constant(self, Span::unknown()));
931    }
932
933    pub fn get_decl(&self, decl_id: DeclId) -> &dyn Command {
934        self.decls
935            .get(decl_id.get())
936            .expect("internal error: missing declaration")
937            .as_ref()
938    }
939
940    /// Get all commands within scope, sorted by the commands' names
941    pub fn get_decls_sorted(&self, include_hidden: bool) -> Vec<(Vec<u8>, DeclId)> {
942        let mut decls_map = HashMap::new();
943
944        for overlay_frame in self.active_overlays(&[]) {
945            let new_decls = if include_hidden {
946                overlay_frame.decls.clone()
947            } else {
948                overlay_frame
949                    .decls
950                    .clone()
951                    .into_iter()
952                    .filter(|(_, id)| overlay_frame.visibility.is_decl_id_visible(id))
953                    .collect()
954            };
955
956            decls_map.extend(new_decls);
957        }
958
959        let mut decls: Vec<(Vec<u8>, DeclId)> = decls_map.into_iter().collect();
960
961        decls.sort_by(|a, b| a.0.cmp(&b.0));
962        decls
963    }
964
965    pub fn get_signature(&self, decl: &dyn Command) -> Signature {
966        if let Some(block_id) = decl.block_id() {
967            *self.blocks[block_id.get()].signature.clone()
968        } else {
969            decl.signature()
970        }
971    }
972
973    /// Get signatures of all commands within scope with their decl ids.
974    pub fn get_signatures_and_declids(&self, include_hidden: bool) -> Vec<(Signature, DeclId)> {
975        self.get_decls_sorted(include_hidden)
976            .into_iter()
977            .map(|(_, id)| {
978                let decl = self.get_decl(id);
979
980                (self.get_signature(decl).update_from_command(decl), id)
981            })
982            .collect()
983    }
984
985    pub fn get_block(&self, block_id: BlockId) -> &Arc<Block> {
986        self.blocks
987            .get(block_id.get())
988            .expect("internal error: missing block")
989    }
990
991    /// Optionally get a block by id, if it exists
992    ///
993    /// Prefer to use [`.get_block()`](Self::get_block) in most cases - `BlockId`s that don't exist
994    /// are normally a compiler error. This only exists to stop plugins from crashing the engine if
995    /// they send us something invalid.
996    pub fn try_get_block(&self, block_id: BlockId) -> Option<&Arc<Block>> {
997        self.blocks.get(block_id.get())
998    }
999
1000    pub fn get_module(&self, module_id: ModuleId) -> &Module {
1001        self.modules
1002            .get(module_id.get())
1003            .expect("internal error: missing module")
1004    }
1005
1006    pub fn get_virtual_path(&self, virtual_path_id: VirtualPathId) -> &(String, VirtualPath) {
1007        self.virtual_paths
1008            .get(virtual_path_id.get())
1009            .expect("internal error: missing virtual path")
1010    }
1011
1012    pub fn next_span_start(&self) -> usize {
1013        if let Some(cached_file) = self.files.last() {
1014            cached_file.covered_span.end
1015        } else {
1016            0
1017        }
1018    }
1019
1020    pub fn files(
1021        &self,
1022    ) -> impl DoubleEndedIterator<Item = &CachedFile> + ExactSizeIterator<Item = &CachedFile> {
1023        self.files.iter()
1024    }
1025
1026    pub fn add_file(&mut self, filename: Arc<str>, content: Arc<[u8]>) -> FileId {
1027        let next_span_start = self.next_span_start();
1028        let next_span_end = next_span_start + content.len();
1029
1030        let covered_span = Span::new(next_span_start, next_span_end);
1031
1032        self.files.push(CachedFile {
1033            name: filename,
1034            content,
1035            covered_span,
1036        });
1037
1038        FileId::new(self.num_files() - 1)
1039    }
1040
1041    pub fn build_desc(&self, spans: &[Span]) -> (String, String) {
1042        let comment_lines: Vec<&[u8]> = spans
1043            .iter()
1044            .map(|span| self.get_span_contents(*span))
1045            .collect();
1046        build_desc(&comment_lines)
1047    }
1048
1049    pub fn build_module_desc(&self, module_id: ModuleId) -> Option<(String, String)> {
1050        self.get_module_comments(module_id)
1051            .map(|comment_spans| self.build_desc(comment_spans))
1052    }
1053
1054    /// Returns the current working directory, which is guaranteed to be canonicalized.
1055    ///
1056    /// Returns an empty String if $env.PWD doesn't exist.
1057    #[deprecated(since = "0.92.3", note = "please use `EngineState::cwd()` instead")]
1058    pub fn current_work_dir(&self) -> String {
1059        self.cwd(None)
1060            .map(|path| path.to_string_lossy().to_string())
1061            .unwrap_or_default()
1062    }
1063
1064    /// Returns the current working directory, which is guaranteed to be an
1065    /// absolute path without trailing slashes (unless it's the root path), but
1066    /// might contain symlink components.
1067    ///
1068    /// If `stack` is supplied, also considers modifications to the working
1069    /// directory on the stack that have yet to be merged into the engine state.
1070    pub fn cwd(&self, stack: Option<&Stack>) -> Result<AbsolutePathBuf, ShellError> {
1071        // Helper function to create a simple generic error.
1072        fn error(msg: &str, cwd: impl AsRef<nu_path::Path>) -> ShellError {
1073            ShellError::Generic(
1074                GenericError::new_internal(
1075                    msg.to_string(),
1076                    format!("$env.PWD = {}", cwd.as_ref().display()),
1077                )
1078                .with_help("Use `cd` to reset $env.PWD into a good state"),
1079            )
1080        }
1081
1082        // Retrieve $env.PWD from the stack or the engine state.
1083        let pwd = if let Some(stack) = stack {
1084            stack.get_env_var(self, "PWD")
1085        } else {
1086            self.get_env_var("PWD")
1087        };
1088
1089        let pwd = pwd.ok_or_else(|| error("$env.PWD not found", ""))?;
1090
1091        if let Ok(pwd) = pwd.as_str() {
1092            let path = AbsolutePathBuf::try_from(pwd)
1093                .map_err(|_| error("$env.PWD is not an absolute path", pwd))?;
1094
1095            // Technically, a root path counts as "having trailing slashes", but
1096            // for the purpose of PWD, a root path is acceptable.
1097            if path.parent().is_some() && nu_path::has_trailing_slash(path.as_ref()) {
1098                Err(error("$env.PWD contains trailing slashes", &path))
1099            } else if !path.exists() {
1100                Err(error("$env.PWD points to a non-existent directory", &path))
1101            } else if !path.is_dir() {
1102                Err(error("$env.PWD points to a non-directory", &path))
1103            } else {
1104                Ok(path)
1105            }
1106        } else {
1107            Err(error("$env.PWD is not a string", format!("{pwd:?}")))
1108        }
1109    }
1110
1111    /// Like `EngineState::cwd()`, but returns a String instead of a PathBuf for convenience.
1112    pub fn cwd_as_string(&self, stack: Option<&Stack>) -> Result<String, ShellError> {
1113        let cwd = self.cwd(stack)?;
1114        cwd.into_os_string()
1115            .into_string()
1116            .map_err(|err| ShellError::NonUtf8Custom {
1117                msg: format!("The current working directory is not a valid utf-8 string: {err:?}"),
1118                span: Span::unknown(),
1119            })
1120    }
1121
1122    // TODO: see if we can completely get rid of this
1123    pub fn get_file_contents(&self) -> &[CachedFile] {
1124        &self.files
1125    }
1126
1127    pub fn get_startup_time(&self) -> i64 {
1128        self.startup_time
1129    }
1130
1131    pub fn set_startup_time(&mut self, startup_time: i64) {
1132        self.startup_time = startup_time;
1133    }
1134
1135    pub fn activate_debugger(
1136        &self,
1137        debugger: Box<dyn Debugger>,
1138    ) -> Result<(), PoisonDebuggerError<'_>> {
1139        let mut locked_debugger = self.debugger.lock()?;
1140        *locked_debugger = debugger;
1141        locked_debugger.activate();
1142        self.is_debugging.0.store(true, Ordering::Relaxed);
1143        Ok(())
1144    }
1145
1146    pub fn deactivate_debugger(&self) -> Result<Box<dyn Debugger>, PoisonDebuggerError<'_>> {
1147        let mut locked_debugger = self.debugger.lock()?;
1148        locked_debugger.deactivate();
1149        let ret = std::mem::replace(&mut *locked_debugger, Box::new(NoopDebugger));
1150        self.is_debugging.0.store(false, Ordering::Relaxed);
1151        Ok(ret)
1152    }
1153
1154    pub fn is_debugging(&self) -> bool {
1155        self.is_debugging.0.load(Ordering::Relaxed)
1156    }
1157
1158    pub fn recover_from_panic(&mut self) {
1159        if Mutex::is_poisoned(&self.repl_state) {
1160            self.repl_state = Arc::new(Mutex::new(ReplState {
1161                buffer: "".to_string(),
1162                cursor_pos: 0,
1163                accept: false,
1164            }));
1165        }
1166        if Mutex::is_poisoned(&self.jobs) {
1167            self.jobs = Arc::new(Mutex::new(Jobs::default()));
1168        }
1169        if Mutex::is_poisoned(&self.regex_cache) {
1170            self.regex_cache = Arc::new(Mutex::new(LruCache::new(
1171                NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
1172            )));
1173        }
1174    }
1175
1176    /// Reset mutable per-session state after cloning a shared engine template.
1177    pub fn make_session_state_unique(&mut self) {
1178        let (send, recv) = channel();
1179
1180        self.pipeline_externals_state = Arc::new((AtomicU32::new(0), AtomicU32::new(0)));
1181        self.repl_state = Default::default();
1182        self.report_log = Default::default();
1183        self.prompt_state = Arc::new(PromptState::new());
1184        self.jobs = Default::default();
1185        self.current_job = CurrentJob {
1186            id: JobId::new(0),
1187            background_thread_job: None,
1188            mailbox: Arc::new(Mutex::new(Mailbox::new(recv))),
1189        };
1190        self.root_job_sender = send;
1191        self.exit_warning_given = Default::default();
1192        self.regex_cache = Arc::new(Mutex::new(LruCache::new(
1193            NonZeroUsize::new(REGEX_CACHE_SIZE).expect("tried to create cache of size zero"),
1194        )));
1195        self.is_debugging = IsDebugging::new(false);
1196        self.debugger = Arc::new(Mutex::new(Box::new(NoopDebugger)));
1197    }
1198
1199    /// Add new span and return its ID
1200    pub fn add_span(&mut self, span: Span) -> SpanId {
1201        self.spans.push(span);
1202        SpanId::new(self.num_spans() - 1)
1203    }
1204
1205    /// Find ID of a span (should be avoided if possible)
1206    pub fn find_span_id(&self, span: Span) -> Option<SpanId> {
1207        self.spans
1208            .iter()
1209            .position(|sp| sp == &span)
1210            .map(SpanId::new)
1211    }
1212
1213    // Determines whether the current state is being held by a background job
1214    pub fn is_background_job(&self) -> bool {
1215        self.current_job.background_thread_job.is_some()
1216    }
1217
1218    // Gets the thread job entry
1219    pub fn current_thread_job(&self) -> Option<&ThreadJob> {
1220        self.current_job.background_thread_job.as_ref()
1221    }
1222}
1223
1224/// Collect all `VarId`s referenced by `Expr::Var` nodes in `expr`.
1225/// This is used by [`EngineState::cleanup_stack_variables`] to avoid removing
1226/// variables that are still referenced by alias definitions.
1227fn collect_alias_var_ids(expr: &crate::ast::Expression, var_ids: &mut HashSet<VarId>) {
1228    let mut queue = vec![expr];
1229
1230    while let Some(e) = queue.pop() {
1231        match &e.expr {
1232            Expr::Var(id) => {
1233                var_ids.insert(*id);
1234            }
1235            Expr::Call(call) => {
1236                for arg in &call.arguments {
1237                    if let Some(sub_expr) = arg.expr() {
1238                        queue.push(sub_expr);
1239                    }
1240                }
1241            }
1242            Expr::ExternalCall(head, args) => {
1243                queue.push(head);
1244                for arg in args.iter() {
1245                    queue.push(arg.expr());
1246                }
1247            }
1248            Expr::FullCellPath(fcp) => queue.push(&fcp.head),
1249            _ => {}
1250        }
1251    }
1252}
1253
1254impl GetSpan for &EngineState {
1255    /// Get existing span
1256    fn get_span(&self, span_id: SpanId) -> Span {
1257        *self
1258            .spans
1259            .get(span_id.get())
1260            .expect("internal error: missing span")
1261    }
1262}
1263
1264impl Default for EngineState {
1265    fn default() -> Self {
1266        Self::new()
1267    }
1268}
1269
1270/// Map a fancy-regex compile failure to [`ShellError::InvalidValue`].
1271///
1272/// Shared by [`EngineState::compile_regex`] and call sites that compile with
1273/// options that bypass the LRU cache (for example a custom backtrack limit).
1274pub fn invalid_regex_value(pattern: &str, err: fancy_regex::Error, span: Span) -> ShellError {
1275    ShellError::InvalidValue {
1276        valid: "a valid regular expression".into(),
1277        actual: format!("'{pattern}' ({err})"),
1278        span,
1279    }
1280}
1281
1282#[cfg(test)]
1283mod engine_state_tests {
1284    use crate::engine::StateWorkingSet;
1285    use std::str::{Utf8Error, from_utf8};
1286
1287    use super::*;
1288
1289    #[test]
1290    fn add_file_gives_id() {
1291        let engine_state = EngineState::new();
1292        let mut engine_state = StateWorkingSet::new(&engine_state);
1293        let id = engine_state.add_file("test.nu", &[]);
1294
1295        assert_eq!(id, FileId::new(0));
1296    }
1297
1298    #[test]
1299    fn get_cached_regex_reuses_compiled_patterns() {
1300        let engine_state = EngineState::new();
1301        let pattern = r"[^\w]+";
1302
1303        let first = engine_state
1304            .get_cached_regex(pattern)
1305            .expect("pattern should compile");
1306        assert!(first.is_match("!!!").unwrap_or(false));
1307
1308        {
1309            let cache = engine_state.regex_cache.lock().expect("cache lock");
1310            assert_eq!(cache.len(), 1);
1311            assert!(cache.peek(pattern).is_some());
1312        }
1313
1314        let second = engine_state
1315            .get_cached_regex(pattern)
1316            .expect("pattern should compile from cache");
1317        assert!(second.is_match("!!!").unwrap_or(false));
1318
1319        {
1320            let cache = engine_state.regex_cache.lock().expect("cache lock");
1321            assert_eq!(cache.len(), 1, "second lookup should not grow the cache");
1322        }
1323    }
1324
1325    #[test]
1326    fn get_cached_regex_does_not_store_invalid_patterns() {
1327        let engine_state = EngineState::new();
1328
1329        assert!(engine_state.get_cached_regex("(").is_err());
1330
1331        let cache = engine_state.regex_cache.lock().expect("cache lock");
1332        assert_eq!(cache.len(), 0);
1333    }
1334
1335    #[test]
1336    fn add_file_gives_id_including_parent() {
1337        let mut engine_state = EngineState::new();
1338        let parent_id = engine_state.add_file("test.nu".into(), Arc::new([]));
1339
1340        let mut working_set = StateWorkingSet::new(&engine_state);
1341        let working_set_id = working_set.add_file("child.nu", &[]);
1342
1343        assert_eq!(parent_id, FileId::new(0));
1344        assert_eq!(working_set_id, FileId::new(1));
1345    }
1346
1347    #[test]
1348    fn merge_states() -> Result<(), ShellError> {
1349        let mut engine_state = EngineState::new();
1350        engine_state.add_file("test.nu".into(), Arc::new([]));
1351
1352        let delta = {
1353            let mut working_set = StateWorkingSet::new(&engine_state);
1354            let _ = working_set.add_file("child.nu", &[]);
1355            working_set.render()
1356        };
1357
1358        engine_state.merge_delta(delta)?;
1359
1360        assert_eq!(engine_state.num_files(), 2);
1361        assert_eq!(&*engine_state.files[0].name, "test.nu");
1362        assert_eq!(&*engine_state.files[1].name, "child.nu");
1363
1364        Ok(())
1365    }
1366
1367    #[test]
1368    fn list_variables() -> Result<(), Utf8Error> {
1369        let varname = "something";
1370        let varname_with_sigil = "$".to_owned() + varname;
1371        let engine_state = EngineState::new();
1372        let mut working_set = StateWorkingSet::new(&engine_state);
1373        working_set.add_variable(
1374            varname.as_bytes().into(),
1375            Span { start: 0, end: 1 },
1376            Type::Int,
1377            false,
1378        );
1379        let variables = working_set
1380            .list_variables()
1381            .into_iter()
1382            .map(from_utf8)
1383            .collect::<Result<Vec<&str>, Utf8Error>>()?;
1384        assert_eq!(variables, vec![varname_with_sigil]);
1385        Ok(())
1386    }
1387
1388    #[test]
1389    fn get_plugin_config() {
1390        let mut engine_state = EngineState::new();
1391
1392        assert!(
1393            engine_state.get_plugin_config("example").is_none(),
1394            "Unexpected plugin configuration"
1395        );
1396
1397        let mut plugins = HashMap::new();
1398        plugins.insert("example".into(), Value::string("value", Span::test_data()));
1399
1400        let mut config = Config::clone(engine_state.get_config());
1401        config.plugins = plugins;
1402
1403        engine_state.set_config(config);
1404
1405        assert!(
1406            engine_state.get_plugin_config("example").is_some(),
1407            "Plugin configuration not found"
1408        );
1409    }
1410}
1411
1412#[cfg(test)]
1413mod test_cwd {
1414    //! Here're the test cases we need to cover:
1415    //!
1416    //! `EngineState::cwd()` computes the result from `self.env_vars["PWD"]` and
1417    //! optionally `stack.env_vars["PWD"]`.
1418    //!
1419    //! PWD may be unset in either `env_vars`.
1420    //! PWD should NOT be an empty string.
1421    //! PWD should NOT be a non-string value.
1422    //! PWD should NOT be a relative path.
1423    //! PWD should NOT contain trailing slashes.
1424    //! PWD may point to a directory or a symlink to directory.
1425    //! PWD should NOT point to a file or a symlink to file.
1426    //! PWD should NOT point to non-existent entities in the filesystem.
1427
1428    use crate::{
1429        Value,
1430        engine::{EngineState, Stack},
1431    };
1432    use nu_path::{AbsolutePath, Path, assert_path_eq};
1433    use tempfile::{NamedTempFile, TempDir};
1434
1435    /// Creates a symlink. Works on both Unix and Windows.
1436    #[cfg(any(unix, windows))]
1437    fn symlink(
1438        original: impl AsRef<AbsolutePath>,
1439        link: impl AsRef<AbsolutePath>,
1440    ) -> std::io::Result<()> {
1441        let original = original.as_ref();
1442        let link = link.as_ref();
1443
1444        #[cfg(unix)]
1445        {
1446            std::os::unix::fs::symlink(original, link)
1447        }
1448        #[cfg(windows)]
1449        {
1450            if original.is_dir() {
1451                std::os::windows::fs::symlink_dir(original, link)
1452            } else {
1453                std::os::windows::fs::symlink_file(original, link)
1454            }
1455        }
1456    }
1457
1458    /// Create an engine state initialized with the given PWD.
1459    fn engine_state_with_pwd(path: impl AsRef<Path>) -> EngineState {
1460        let mut engine_state = EngineState::new();
1461        engine_state.add_env_var(
1462            "PWD".into(),
1463            Value::test_string(path.as_ref().to_str().unwrap()),
1464        );
1465        engine_state
1466    }
1467
1468    /// Create a stack initialized with the given PWD.
1469    fn stack_with_pwd(path: impl AsRef<Path>) -> Stack {
1470        let mut stack = Stack::new();
1471        stack.add_env_var(
1472            "PWD".into(),
1473            Value::test_string(path.as_ref().to_str().unwrap()),
1474        );
1475        stack
1476    }
1477
1478    #[test]
1479    fn pwd_not_set() {
1480        let engine_state = EngineState::new();
1481        engine_state.cwd(None).unwrap_err();
1482    }
1483
1484    #[test]
1485    fn pwd_is_empty_string() {
1486        let engine_state = engine_state_with_pwd("");
1487        engine_state.cwd(None).unwrap_err();
1488    }
1489
1490    #[test]
1491    fn pwd_is_non_string_value() {
1492        let mut engine_state = EngineState::new();
1493        engine_state.add_env_var("PWD".into(), Value::test_glob("*"));
1494        engine_state.cwd(None).unwrap_err();
1495    }
1496
1497    #[test]
1498    fn pwd_is_relative_path() {
1499        let engine_state = engine_state_with_pwd("./foo");
1500
1501        engine_state.cwd(None).unwrap_err();
1502    }
1503
1504    #[test]
1505    fn pwd_has_trailing_slash() {
1506        let dir = TempDir::new().unwrap();
1507        let engine_state = engine_state_with_pwd(dir.path().join(""));
1508
1509        engine_state.cwd(None).unwrap_err();
1510    }
1511
1512    #[test]
1513    fn pwd_points_to_root() {
1514        #[cfg(windows)]
1515        let root = Path::new(r"C:\");
1516        #[cfg(not(windows))]
1517        let root = Path::new("/");
1518
1519        let engine_state = engine_state_with_pwd(root);
1520        let cwd = engine_state.cwd(None).unwrap();
1521        assert_path_eq!(cwd, root);
1522    }
1523
1524    #[test]
1525    fn pwd_points_to_normal_file() {
1526        let file = NamedTempFile::new().unwrap();
1527        let engine_state = engine_state_with_pwd(file.path());
1528
1529        engine_state.cwd(None).unwrap_err();
1530    }
1531
1532    #[test]
1533    fn pwd_points_to_normal_directory() {
1534        let dir = TempDir::new().unwrap();
1535        let engine_state = engine_state_with_pwd(dir.path());
1536
1537        let cwd = engine_state.cwd(None).unwrap();
1538        assert_path_eq!(cwd, dir.path());
1539    }
1540
1541    #[test]
1542    fn pwd_points_to_symlink_to_file() {
1543        let file = NamedTempFile::new().unwrap();
1544        let temp_file = AbsolutePath::try_new(file.path()).unwrap();
1545        let dir = TempDir::new().unwrap();
1546        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1547
1548        let link = temp.join("link");
1549        symlink(temp_file, &link).unwrap();
1550        let engine_state = engine_state_with_pwd(&link);
1551
1552        engine_state.cwd(None).unwrap_err();
1553    }
1554
1555    #[test]
1556    fn pwd_points_to_symlink_to_directory() {
1557        let dir = TempDir::new().unwrap();
1558        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1559
1560        let link = temp.join("link");
1561        symlink(temp, &link).unwrap();
1562        let engine_state = engine_state_with_pwd(&link);
1563
1564        let cwd = engine_state.cwd(None).unwrap();
1565        assert_path_eq!(cwd, link);
1566    }
1567
1568    #[test]
1569    fn pwd_points_to_broken_symlink() {
1570        let dir = TempDir::new().unwrap();
1571        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1572        let other_dir = TempDir::new().unwrap();
1573        let other_temp = AbsolutePath::try_new(other_dir.path()).unwrap();
1574
1575        let link = temp.join("link");
1576        symlink(other_temp, &link).unwrap();
1577        let engine_state = engine_state_with_pwd(&link);
1578
1579        drop(other_dir);
1580        engine_state.cwd(None).unwrap_err();
1581    }
1582
1583    #[test]
1584    fn pwd_points_to_nonexistent_entity() {
1585        let engine_state = engine_state_with_pwd(TempDir::new().unwrap().path());
1586
1587        engine_state.cwd(None).unwrap_err();
1588    }
1589
1590    #[test]
1591    fn stack_pwd_not_set() {
1592        let dir = TempDir::new().unwrap();
1593        let engine_state = engine_state_with_pwd(dir.path());
1594        let stack = Stack::new();
1595
1596        let cwd = engine_state.cwd(Some(&stack)).unwrap();
1597        assert_eq!(cwd, dir.path());
1598    }
1599
1600    #[test]
1601    fn stack_pwd_is_empty_string() {
1602        let dir = TempDir::new().unwrap();
1603        let engine_state = engine_state_with_pwd(dir.path());
1604        let stack = stack_with_pwd("");
1605
1606        engine_state.cwd(Some(&stack)).unwrap_err();
1607    }
1608
1609    #[test]
1610    fn stack_pwd_points_to_normal_directory() {
1611        let dir1 = TempDir::new().unwrap();
1612        let dir2 = TempDir::new().unwrap();
1613        let engine_state = engine_state_with_pwd(dir1.path());
1614        let stack = stack_with_pwd(dir2.path());
1615
1616        let cwd = engine_state.cwd(Some(&stack)).unwrap();
1617        assert_path_eq!(cwd, dir2.path());
1618    }
1619
1620    #[test]
1621    fn stack_pwd_points_to_normal_directory_with_symlink_components() {
1622        let dir = TempDir::new().unwrap();
1623        let temp = AbsolutePath::try_new(dir.path()).unwrap();
1624
1625        // `/tmp/dir/link` points to `/tmp/dir`, then we set PWD to `/tmp/dir/link/foo`
1626        let link = temp.join("link");
1627        symlink(temp, &link).unwrap();
1628        let foo = link.join("foo");
1629        std::fs::create_dir(temp.join("foo")).unwrap();
1630        let engine_state = EngineState::new();
1631        let stack = stack_with_pwd(&foo);
1632
1633        let cwd = engine_state.cwd(Some(&stack)).unwrap();
1634        assert_path_eq!(cwd, foo);
1635    }
1636}