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