Skip to main content

nu_protocol/
eval_const.rs

1//! Implementation of const-evaluation
2//!
3//! This enables you to assign `const`-constants and execute parse-time code dependent on this.
4//! e.g. `source $my_const`
5use crate::{
6    BlockId, Config, HistoryFileFormat, HistoryPath, PipelineData, Record, ShellError, Span, Value,
7    VarId,
8    ast::{Assignment, Block, Call, Expr, Expression, ExternalArgument},
9    debugger::{DebugContext, WithoutDebug},
10    engine::{EngineState, StateWorkingSet},
11    eval_base::Eval,
12    record,
13    shell_error::generic::GenericError,
14};
15use nu_system::os_info::{get_kernel_version, get_os_arch, get_os_family, get_os_name};
16use std::{
17    path::{Path, PathBuf},
18    sync::Arc,
19};
20
21/// Create a Value for `$nu`.
22// Note: When adding new constants to $nu, please update the doc at https://nushell.sh/book/special_variables.html
23// or at least add a TODO/reminder issue in nushell.github.io so we don't lose track of it.
24pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Value {
25    fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
26        #[allow(deprecated)]
27        let cwd = engine_state.current_work_dir();
28
29        if path.exists() {
30            match nu_path::canonicalize_with(path, cwd) {
31                Ok(canon_path) => canon_path,
32                Err(_) => path.to_owned(),
33            }
34        } else {
35            path.to_owned()
36        }
37    }
38
39    let mut record = Record::new();
40
41    let config_path = match nu_path::nu_config_dir() {
42        Some(path) => Ok(canonicalize_path(engine_state, path.as_ref())),
43        None => Err(Value::error(ShellError::ConfigDirNotFound { span }, span)),
44    };
45
46    record.push(
47        "default-config-dir",
48        config_path.as_ref().map_or_else(
49            |e| e.clone(),
50            |path| Value::string(path.to_string_lossy(), span),
51        ),
52    );
53
54    record.push(
55        "config-path",
56        if let Some(path) = engine_state.get_config_path("config-path") {
57            let canon_config_path = canonicalize_path(engine_state, path);
58            Value::string(canon_config_path.to_string_lossy(), span)
59        } else {
60            config_path.clone().map_or_else(
61                |e| e,
62                |mut path| {
63                    path.push("config.nu");
64                    let canon_config_path = canonicalize_path(engine_state, &path);
65                    Value::string(canon_config_path.to_string_lossy(), span)
66                },
67            )
68        },
69    );
70
71    record.push(
72        "env-path",
73        if let Some(path) = engine_state.get_config_path("env-path") {
74            let canon_env_path = canonicalize_path(engine_state, path);
75            Value::string(canon_env_path.to_string_lossy(), span)
76        } else {
77            config_path.clone().map_or_else(
78                |e| e,
79                |mut path| {
80                    path.push("env.nu");
81                    let canon_env_path = canonicalize_path(engine_state, &path);
82                    Value::string(canon_env_path.to_string_lossy(), span)
83                },
84            )
85        },
86    );
87
88    record.push(
89        "history-path",
90        match &engine_state.config.history.path {
91            HistoryPath::Disabled => Value::string("", span),
92            HistoryPath::Custom(custom_path) => {
93                let effective_path = if custom_path.is_dir() {
94                    custom_path.join(engine_state.config.history.file_format.default_file_name())
95                } else {
96                    custom_path.clone()
97                };
98                let canon_hist_path = canonicalize_path(engine_state, &effective_path);
99                Value::string(canon_hist_path.to_string_lossy(), span)
100            }
101            HistoryPath::Default => config_path.clone().map_or_else(
102                |e| e,
103                |mut path| {
104                    match engine_state.config.history.file_format {
105                        HistoryFileFormat::Sqlite => {
106                            path.push("history.sqlite3");
107                        }
108                        HistoryFileFormat::Plaintext => {
109                            path.push("history.txt");
110                        }
111                    }
112                    let canon_hist_path = canonicalize_path(engine_state, &path);
113                    Value::string(canon_hist_path.to_string_lossy(), span)
114                },
115            ),
116        },
117    );
118
119    record.push(
120        "loginshell-path",
121        config_path.clone().map_or_else(
122            |e| e,
123            |mut path| {
124                path.push("login.nu");
125                let canon_login_path = canonicalize_path(engine_state, &path);
126                Value::string(canon_login_path.to_string_lossy(), span)
127            },
128        ),
129    );
130
131    #[cfg(feature = "plugin")]
132    {
133        record.push(
134            "plugin-path",
135            if let Some(path) = &engine_state.plugin_path {
136                let canon_plugin_path = canonicalize_path(engine_state, path);
137                Value::string(canon_plugin_path.to_string_lossy(), span)
138            } else {
139                // If there are no signatures, we should still populate the plugin path
140                config_path.clone().map_or_else(
141                    |e| e,
142                    |mut path| {
143                        path.push("plugin.msgpackz");
144                        let canonical_plugin_path = canonicalize_path(engine_state, &path);
145                        Value::string(canonical_plugin_path.to_string_lossy(), span)
146                    },
147                )
148            },
149        );
150    }
151
152    record.push(
153        "home-dir",
154        if let Some(path) = nu_path::home_dir() {
155            let canon_home_path = canonicalize_path(engine_state, path.as_ref());
156            Value::string(canon_home_path.to_string_lossy(), span)
157        } else {
158            Value::error(
159                ShellError::Generic(GenericError::new(
160                    "setting $nu.home-dir failed",
161                    "Could not get home directory",
162                    span,
163                )),
164                span,
165            )
166        },
167    );
168
169    record.push(
170        "data-dir",
171        if let Some(path) = nu_path::data_dir() {
172            let mut canon_data_path = canonicalize_path(engine_state, path.as_ref());
173            canon_data_path.push("nushell");
174            Value::string(canon_data_path.to_string_lossy(), span)
175        } else {
176            Value::error(
177                ShellError::Generic(GenericError::new(
178                    "setting $nu.data-dir failed",
179                    "Could not get data path",
180                    span,
181                )),
182                span,
183            )
184        },
185    );
186
187    record.push(
188        "cache-dir",
189        if let Some(path) = nu_path::cache_dir() {
190            let mut canon_cache_path = canonicalize_path(engine_state, path.as_ref());
191            canon_cache_path.push("nushell");
192            Value::string(canon_cache_path.to_string_lossy(), span)
193        } else {
194            Value::error(
195                ShellError::Generic(GenericError::new(
196                    "setting $nu.cache-dir failed",
197                    "Could not get cache path",
198                    span,
199                )),
200                span,
201            )
202        },
203    );
204
205    record.push(
206        "vendor-autoload-dirs",
207        Value::list(
208            get_vendor_autoload_dirs(engine_state)
209                .iter()
210                .map(|path| Value::string(path.to_string_lossy(), span))
211                .collect(),
212            span,
213        ),
214    );
215
216    record.push(
217        "user-autoload-dirs",
218        Value::list(
219            get_user_autoload_dirs(engine_state)
220                .iter()
221                .map(|path| Value::string(path.to_string_lossy(), span))
222                .collect(),
223            span,
224        ),
225    );
226
227    record.push("temp-dir", {
228        let canon_temp_path = canonicalize_path(engine_state, &std::env::temp_dir());
229        Value::string(canon_temp_path.to_string_lossy(), span)
230    });
231
232    record.push("pid", Value::int(std::process::id().into(), span));
233
234    record.push("os-info", {
235        let ver = get_kernel_version();
236        Value::record(
237            record! {
238                "name" => Value::string(get_os_name(), span),
239                "arch" => Value::string(get_os_arch(), span),
240                "family" => Value::string(get_os_family(), span),
241                "kernel_version" => Value::string(ver, span),
242            },
243            span,
244        )
245    });
246
247    record.push(
248        "startup-time",
249        Value::duration(engine_state.get_startup_time(), span),
250    );
251
252    record.push(
253        "is-interactive",
254        Value::bool(engine_state.is_interactive, span),
255    );
256
257    record.push("is-login", Value::bool(engine_state.is_login, span));
258
259    record.push(
260        "history-enabled",
261        Value::bool(engine_state.history_enabled, span),
262    );
263
264    record.push(
265        "current-exe",
266        if let Ok(current_exe) = std::env::current_exe() {
267            Value::string(current_exe.to_string_lossy(), span)
268        } else {
269            Value::error(
270                ShellError::Generic(GenericError::new(
271                    "setting $nu.current-exe failed",
272                    "Could not get current executable path",
273                    span,
274                )),
275                span,
276            )
277        },
278    );
279
280    record.push("is-lsp", Value::bool(engine_state.is_lsp, span));
281    record.push("is-mcp", Value::bool(engine_state.is_mcp, span));
282
283    Value::record(record, span)
284}
285
286/// Generates the list of vendor autoload dirs
287///
288/// - *macOS only*: `/Library/Application Support/nushell/vendor/autoload`
289/// - *non-Windows*:
290///   ```nu
291///   if $env.XDG_DATA_DIRS? != null {
292///       $env.XDG_DATA_DIRS
293///       | split row ":"
294///       | reverse
295///   } else if $PREFIX ends-with "local" {
296///       [
297///           $'($PREFIX)/share'
298///       ]
299///   } else {
300///       [
301///           $'($PREFIX)/local/share'
302///           $'($PREFIX)/share'
303///       ]
304///   }
305///   | each {|dir| $'($dir)/nushell/vendor' }
306///   ```
307/// - *Windows only*: `%ProgramData%\nushell\vendor\autoload`
308/// - *compile time*: `$env.NU_VENDOR_AUTOLOAD_DIR` if it is set
309/// - `($nu.data_dir)/vendor/autoload`
310/// - `$env.NU_VENDOR_AUTOLOAD_DIR` if it is set _before_ `nu` is run
311pub fn get_vendor_autoload_dirs(_engine_state: &EngineState) -> Vec<PathBuf> {
312    let into_autoload_path_fn = |mut path: PathBuf| {
313        path.push("nushell");
314        path.push("vendor");
315        path.push("autoload");
316        path
317    };
318
319    let mut dirs = Vec::new();
320
321    let mut append_fn = |path: PathBuf| {
322        if !dirs.contains(&path) {
323            dirs.push(path)
324        }
325    };
326
327    #[cfg(target_os = "macos")]
328    std::iter::once("/Library/Application Support")
329        .map(PathBuf::from)
330        .map(into_autoload_path_fn)
331        .for_each(&mut append_fn);
332    #[cfg(unix)]
333    {
334        use std::os::unix::ffi::OsStrExt;
335
336        std::env::var_os("XDG_DATA_DIRS")
337            .or_else(|| {
338                option_env!("PREFIX").map(|prefix| {
339                    if prefix.ends_with("local") {
340                        std::ffi::OsString::from(format!("{prefix}/share"))
341                    } else {
342                        std::ffi::OsString::from(format!("{prefix}/local/share:{prefix}/share"))
343                    }
344                })
345            })
346            .unwrap_or_else(|| std::ffi::OsString::from("/usr/local/share/:/usr/share/"))
347            .as_encoded_bytes()
348            .split(|b| *b == b':')
349            .map(|split| into_autoload_path_fn(PathBuf::from(std::ffi::OsStr::from_bytes(split))))
350            .rev()
351            .for_each(&mut append_fn);
352    }
353
354    #[cfg(target_os = "windows")]
355    dirs_sys::known_folder(windows_sys::Win32::UI::Shell::FOLDERID_ProgramData)
356        .into_iter()
357        .map(into_autoload_path_fn)
358        .for_each(&mut append_fn);
359
360    if let Some(path) = option_env!("NU_VENDOR_AUTOLOAD_DIR") {
361        append_fn(PathBuf::from(path));
362    }
363
364    if let Some(data_dir) = nu_path::data_dir() {
365        append_fn(into_autoload_path_fn(PathBuf::from(data_dir)));
366    }
367
368    if let Some(path) = std::env::var_os("NU_VENDOR_AUTOLOAD_DIR") {
369        append_fn(PathBuf::from(path));
370    }
371
372    dirs
373}
374
375pub fn get_user_autoload_dirs(_engine_state: &EngineState) -> Vec<PathBuf> {
376    // User autoload directories - Currently just `autoload` in the default
377    // configuration directory
378    let mut dirs = Vec::new();
379
380    let mut append_fn = |path: PathBuf| {
381        if !dirs.contains(&path) {
382            dirs.push(path)
383        }
384    };
385
386    if let Some(config_dir) = nu_path::nu_config_dir() {
387        append_fn(config_dir.join("autoload").into());
388    }
389
390    dirs
391}
392
393fn eval_const_call(
394    working_set: &StateWorkingSet,
395    call: &Call,
396    input: PipelineData,
397) -> Result<PipelineData, ShellError> {
398    let decl = working_set.get_decl(call.decl_id);
399
400    if !decl.is_const() {
401        return Err(ShellError::NotAConstCommand { span: call.head });
402    }
403
404    if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") {
405        // It would require re-implementing get_full_help() for const evaluation. Assuming that
406        // getting help messages at parse-time is rare enough, we can simply disallow it.
407        return Err(ShellError::NotAConstHelp { span: call.head });
408    }
409
410    decl.run_const(working_set, &call.into(), input)
411}
412
413pub fn eval_const_subexpression(
414    working_set: &StateWorkingSet,
415    block: &Block,
416    mut input: PipelineData,
417    span: Span,
418) -> Result<PipelineData, ShellError> {
419    for pipeline in block.pipelines.iter() {
420        for element in pipeline.elements.iter() {
421            if element.redirection.is_some() {
422                return Err(ShellError::NotAConstant { span });
423            }
424
425            input = eval_constant_with_input(working_set, &element.expr, input)?
426        }
427    }
428
429    Ok(input)
430}
431
432pub fn eval_constant_with_input(
433    working_set: &StateWorkingSet,
434    expr: &Expression,
435    input: PipelineData,
436) -> Result<PipelineData, ShellError> {
437    match &expr.expr {
438        Expr::Call(call) => eval_const_call(working_set, call, input),
439        Expr::Subexpression(block_id) => {
440            let block = working_set.get_block(*block_id);
441            eval_const_subexpression(working_set, block, input, expr.span(&working_set))
442        }
443        _ => eval_constant(working_set, expr).map(|v| PipelineData::value(v, None)),
444    }
445}
446
447/// Evaluate a constant value at parse time
448pub fn eval_constant(
449    working_set: &StateWorkingSet,
450    expr: &Expression,
451) -> Result<Value, ShellError> {
452    // TODO: Allow debugging const eval
453    <EvalConst as Eval>::eval::<WithoutDebug>(working_set, &mut (), expr)
454}
455
456struct EvalConst;
457
458impl Eval for EvalConst {
459    type State<'a> = &'a StateWorkingSet<'a>;
460
461    type MutState = ();
462
463    fn get_config(state: Self::State<'_>, _: &mut ()) -> Arc<Config> {
464        state.get_config().clone()
465    }
466
467    fn eval_var(
468        working_set: &StateWorkingSet,
469        _: &mut (),
470        var_id: VarId,
471        span: Span,
472    ) -> Result<Value, ShellError> {
473        match working_set.get_variable(var_id).const_val.as_ref() {
474            Some(val) => Ok(val.clone()),
475            None => Err(ShellError::NotAConstant { span }),
476        }
477    }
478
479    fn eval_call<D: DebugContext>(
480        working_set: &StateWorkingSet,
481        _: &mut (),
482        call: &Call,
483        span: Span,
484    ) -> Result<Value, ShellError> {
485        // TODO: Allow debugging const eval
486        // TODO: eval.rs uses call.head for the span rather than expr.span
487        eval_const_call(working_set, call, PipelineData::empty())?.into_value(span)
488    }
489
490    fn eval_external_call(
491        _: &StateWorkingSet,
492        _: &mut (),
493        _: &Expression,
494        _: &[ExternalArgument],
495        span: Span,
496    ) -> Result<Value, ShellError> {
497        // TODO: It may be more helpful to give not_a_const_command error
498        Err(ShellError::NotAConstant { span })
499    }
500
501    fn eval_collect<D: DebugContext>(
502        _: &StateWorkingSet,
503        _: &mut (),
504        _var_id: VarId,
505        expr: &Expression,
506    ) -> Result<Value, ShellError> {
507        Err(ShellError::NotAConstant { span: expr.span })
508    }
509
510    fn eval_subexpression<D: DebugContext>(
511        working_set: &StateWorkingSet,
512        _: &mut (),
513        block_id: BlockId,
514        span: Span,
515    ) -> Result<Value, ShellError> {
516        // If parsing errors exist in the subexpression, don't bother to evaluate it.
517        if working_set
518            .parse_errors
519            .iter()
520            .any(|error| span.contains_span(error.span()))
521        {
522            return Err(ShellError::ParseErrorInConstant { span });
523        }
524        // TODO: Allow debugging const eval
525        let block = working_set.get_block(block_id);
526        eval_const_subexpression(working_set, block, PipelineData::empty(), span)?.into_value(span)
527    }
528
529    fn regex_match(
530        _: &StateWorkingSet,
531        _op_span: Span,
532        _: &Value,
533        _: &Value,
534        _: bool,
535        expr_span: Span,
536    ) -> Result<Value, ShellError> {
537        Err(ShellError::NotAConstant { span: expr_span })
538    }
539
540    fn eval_assignment<D: DebugContext>(
541        _: &StateWorkingSet,
542        _: &mut (),
543        _: &Expression,
544        _: &Expression,
545        _: Assignment,
546        _op_span: Span,
547        expr_span: Span,
548    ) -> Result<Value, ShellError> {
549        // TODO: Allow debugging const eval
550        Err(ShellError::NotAConstant { span: expr_span })
551    }
552
553    fn eval_row_condition_or_closure(
554        _: &StateWorkingSet,
555        _: &mut (),
556        _: BlockId,
557        span: Span,
558    ) -> Result<Value, ShellError> {
559        Err(ShellError::NotAConstant { span })
560    }
561
562    fn eval_overlay(_: &StateWorkingSet, span: Span) -> Result<Value, ShellError> {
563        Err(ShellError::NotAConstant { span })
564    }
565
566    fn unreachable(working_set: &StateWorkingSet, expr: &Expression) -> Result<Value, ShellError> {
567        Err(ShellError::NotAConstant {
568            span: expr.span(&working_set),
569        })
570    }
571}