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                    #[cfg(feature = "sqlite")]
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    record.push("is-lsp", Value::bool(engine_state.is_lsp, 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}