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
286pub fn get_vendor_autoload_dirs(_engine_state: &EngineState) -> Vec<PathBuf> {
287    // load order for autoload dirs
288    // /Library/Application Support/nushell/vendor/autoload on macOS
289    // <dir>/nushell/vendor/autoload for every dir in XDG_DATA_DIRS in reverse order on platforms other than windows. If XDG_DATA_DIRS is not set, it falls back to <PREFIX>/share if PREFIX ends in local, or <PREFIX>/local/share:<PREFIX>/share otherwise. If PREFIX is not set, fall back to /usr/local/share:/usr/share.
290    // %ProgramData%\nushell\vendor\autoload on windows
291    // NU_VENDOR_AUTOLOAD_DIR from compile time, if env var is set at compile time
292    // <$nu.data_dir>/vendor/autoload
293    // NU_VENDOR_AUTOLOAD_DIR at runtime, if env var is set
294
295    let into_autoload_path_fn = |mut path: PathBuf| {
296        path.push("nushell");
297        path.push("vendor");
298        path.push("autoload");
299        path
300    };
301
302    let mut dirs = Vec::new();
303
304    let mut append_fn = |path: PathBuf| {
305        if !dirs.contains(&path) {
306            dirs.push(path)
307        }
308    };
309
310    #[cfg(target_os = "macos")]
311    std::iter::once("/Library/Application Support")
312        .map(PathBuf::from)
313        .map(into_autoload_path_fn)
314        .for_each(&mut append_fn);
315    #[cfg(unix)]
316    {
317        use std::os::unix::ffi::OsStrExt;
318
319        std::env::var_os("XDG_DATA_DIRS")
320            .or_else(|| {
321                option_env!("PREFIX").map(|prefix| {
322                    if prefix.ends_with("local") {
323                        std::ffi::OsString::from(format!("{prefix}/share"))
324                    } else {
325                        std::ffi::OsString::from(format!("{prefix}/local/share:{prefix}/share"))
326                    }
327                })
328            })
329            .unwrap_or_else(|| std::ffi::OsString::from("/usr/local/share/:/usr/share/"))
330            .as_encoded_bytes()
331            .split(|b| *b == b':')
332            .map(|split| into_autoload_path_fn(PathBuf::from(std::ffi::OsStr::from_bytes(split))))
333            .rev()
334            .for_each(&mut append_fn);
335    }
336
337    #[cfg(target_os = "windows")]
338    dirs_sys::known_folder(windows_sys::Win32::UI::Shell::FOLDERID_ProgramData)
339        .into_iter()
340        .map(into_autoload_path_fn)
341        .for_each(&mut append_fn);
342
343    if let Some(path) = option_env!("NU_VENDOR_AUTOLOAD_DIR") {
344        append_fn(PathBuf::from(path));
345    }
346
347    if let Some(data_dir) = nu_path::data_dir() {
348        append_fn(into_autoload_path_fn(PathBuf::from(data_dir)));
349    }
350
351    if let Some(path) = std::env::var_os("NU_VENDOR_AUTOLOAD_DIR") {
352        append_fn(PathBuf::from(path));
353    }
354
355    dirs
356}
357
358pub fn get_user_autoload_dirs(_engine_state: &EngineState) -> Vec<PathBuf> {
359    // User autoload directories - Currently just `autoload` in the default
360    // configuration directory
361    let mut dirs = Vec::new();
362
363    let mut append_fn = |path: PathBuf| {
364        if !dirs.contains(&path) {
365            dirs.push(path)
366        }
367    };
368
369    if let Some(config_dir) = nu_path::nu_config_dir() {
370        append_fn(config_dir.join("autoload").into());
371    }
372
373    dirs
374}
375
376fn eval_const_call(
377    working_set: &StateWorkingSet,
378    call: &Call,
379    input: PipelineData,
380) -> Result<PipelineData, ShellError> {
381    let decl = working_set.get_decl(call.decl_id);
382
383    if !decl.is_const() {
384        return Err(ShellError::NotAConstCommand { span: call.head });
385    }
386
387    if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") {
388        // It would require re-implementing get_full_help() for const evaluation. Assuming that
389        // getting help messages at parse-time is rare enough, we can simply disallow it.
390        return Err(ShellError::NotAConstHelp { span: call.head });
391    }
392
393    decl.run_const(working_set, &call.into(), input)
394}
395
396pub fn eval_const_subexpression(
397    working_set: &StateWorkingSet,
398    block: &Block,
399    mut input: PipelineData,
400    span: Span,
401) -> Result<PipelineData, ShellError> {
402    for pipeline in block.pipelines.iter() {
403        for element in pipeline.elements.iter() {
404            if element.redirection.is_some() {
405                return Err(ShellError::NotAConstant { span });
406            }
407
408            input = eval_constant_with_input(working_set, &element.expr, input)?
409        }
410    }
411
412    Ok(input)
413}
414
415pub fn eval_constant_with_input(
416    working_set: &StateWorkingSet,
417    expr: &Expression,
418    input: PipelineData,
419) -> Result<PipelineData, ShellError> {
420    match &expr.expr {
421        Expr::Call(call) => eval_const_call(working_set, call, input),
422        Expr::Subexpression(block_id) => {
423            let block = working_set.get_block(*block_id);
424            eval_const_subexpression(working_set, block, input, expr.span(&working_set))
425        }
426        _ => eval_constant(working_set, expr).map(|v| PipelineData::value(v, None)),
427    }
428}
429
430/// Evaluate a constant value at parse time
431pub fn eval_constant(
432    working_set: &StateWorkingSet,
433    expr: &Expression,
434) -> Result<Value, ShellError> {
435    // TODO: Allow debugging const eval
436    <EvalConst as Eval>::eval::<WithoutDebug>(working_set, &mut (), expr)
437}
438
439struct EvalConst;
440
441impl Eval for EvalConst {
442    type State<'a> = &'a StateWorkingSet<'a>;
443
444    type MutState = ();
445
446    fn get_config(state: Self::State<'_>, _: &mut ()) -> Arc<Config> {
447        state.get_config().clone()
448    }
449
450    fn eval_var(
451        working_set: &StateWorkingSet,
452        _: &mut (),
453        var_id: VarId,
454        span: Span,
455    ) -> Result<Value, ShellError> {
456        match working_set.get_variable(var_id).const_val.as_ref() {
457            Some(val) => Ok(val.clone()),
458            None => Err(ShellError::NotAConstant { span }),
459        }
460    }
461
462    fn eval_call<D: DebugContext>(
463        working_set: &StateWorkingSet,
464        _: &mut (),
465        call: &Call,
466        span: Span,
467    ) -> Result<Value, ShellError> {
468        // TODO: Allow debugging const eval
469        // TODO: eval.rs uses call.head for the span rather than expr.span
470        eval_const_call(working_set, call, PipelineData::empty())?.into_value(span)
471    }
472
473    fn eval_external_call(
474        _: &StateWorkingSet,
475        _: &mut (),
476        _: &Expression,
477        _: &[ExternalArgument],
478        span: Span,
479    ) -> Result<Value, ShellError> {
480        // TODO: It may be more helpful to give not_a_const_command error
481        Err(ShellError::NotAConstant { span })
482    }
483
484    fn eval_collect<D: DebugContext>(
485        _: &StateWorkingSet,
486        _: &mut (),
487        _var_id: VarId,
488        expr: &Expression,
489    ) -> Result<Value, ShellError> {
490        Err(ShellError::NotAConstant { span: expr.span })
491    }
492
493    fn eval_subexpression<D: DebugContext>(
494        working_set: &StateWorkingSet,
495        _: &mut (),
496        block_id: BlockId,
497        span: Span,
498    ) -> Result<Value, ShellError> {
499        // If parsing errors exist in the subexpression, don't bother to evaluate it.
500        if working_set
501            .parse_errors
502            .iter()
503            .any(|error| span.contains_span(error.span()))
504        {
505            return Err(ShellError::ParseErrorInConstant { span });
506        }
507        // TODO: Allow debugging const eval
508        let block = working_set.get_block(block_id);
509        eval_const_subexpression(working_set, block, PipelineData::empty(), span)?.into_value(span)
510    }
511
512    fn regex_match(
513        _: &StateWorkingSet,
514        _op_span: Span,
515        _: &Value,
516        _: &Value,
517        _: bool,
518        expr_span: Span,
519    ) -> Result<Value, ShellError> {
520        Err(ShellError::NotAConstant { span: expr_span })
521    }
522
523    fn eval_assignment<D: DebugContext>(
524        _: &StateWorkingSet,
525        _: &mut (),
526        _: &Expression,
527        _: &Expression,
528        _: Assignment,
529        _op_span: Span,
530        expr_span: Span,
531    ) -> Result<Value, ShellError> {
532        // TODO: Allow debugging const eval
533        Err(ShellError::NotAConstant { span: expr_span })
534    }
535
536    fn eval_row_condition_or_closure(
537        _: &StateWorkingSet,
538        _: &mut (),
539        _: BlockId,
540        span: Span,
541    ) -> Result<Value, ShellError> {
542        Err(ShellError::NotAConstant { span })
543    }
544
545    fn eval_overlay(_: &StateWorkingSet, span: Span) -> Result<Value, ShellError> {
546        Err(ShellError::NotAConstant { span })
547    }
548
549    fn unreachable(working_set: &StateWorkingSet, expr: &Expression) -> Result<Value, ShellError> {
550        Err(ShellError::NotAConstant {
551            span: expr.span(&working_set),
552        })
553    }
554}