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