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