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(
42            ShellError::ConfigDirNotFound { span: Some(span) },
43            span,
44        )),
45    };
46
47    record.push(
48        "default-config-dir",
49        config_path.as_ref().map_or_else(
50            |e| e.clone(),
51            |path| Value::string(path.to_string_lossy(), span),
52        ),
53    );
54
55    record.push(
56        "config-path",
57        if let Some(path) = engine_state.get_config_path("config-path") {
58            let canon_config_path = canonicalize_path(engine_state, path);
59            Value::string(canon_config_path.to_string_lossy(), span)
60        } else {
61            config_path.clone().map_or_else(
62                |e| e,
63                |mut path| {
64                    path.push("config.nu");
65                    let canon_config_path = canonicalize_path(engine_state, &path);
66                    Value::string(canon_config_path.to_string_lossy(), span)
67                },
68            )
69        },
70    );
71
72    record.push(
73        "env-path",
74        if let Some(path) = engine_state.get_config_path("env-path") {
75            let canon_env_path = canonicalize_path(engine_state, path);
76            Value::string(canon_env_path.to_string_lossy(), span)
77        } else {
78            config_path.clone().map_or_else(
79                |e| e,
80                |mut path| {
81                    path.push("env.nu");
82                    let canon_env_path = canonicalize_path(engine_state, &path);
83                    Value::string(canon_env_path.to_string_lossy(), span)
84                },
85            )
86        },
87    );
88
89    record.push(
90        "history-path",
91        config_path.clone().map_or_else(
92            |e| e,
93            |mut path| {
94                match engine_state.config.history.file_format {
95                    HistoryFileFormat::Sqlite => {
96                        path.push("history.sqlite3");
97                    }
98                    HistoryFileFormat::Plaintext => {
99                        path.push("history.txt");
100                    }
101                }
102                let canon_hist_path = canonicalize_path(engine_state, &path);
103                Value::string(canon_hist_path.to_string_lossy(), span)
104            },
105        ),
106    );
107
108    record.push(
109        "loginshell-path",
110        config_path.clone().map_or_else(
111            |e| e,
112            |mut path| {
113                path.push("login.nu");
114                let canon_login_path = canonicalize_path(engine_state, &path);
115                Value::string(canon_login_path.to_string_lossy(), span)
116            },
117        ),
118    );
119
120    #[cfg(feature = "plugin")]
121    {
122        record.push(
123            "plugin-path",
124            if let Some(path) = &engine_state.plugin_path {
125                let canon_plugin_path = canonicalize_path(engine_state, path);
126                Value::string(canon_plugin_path.to_string_lossy(), span)
127            } else {
128                // If there are no signatures, we should still populate the plugin path
129                config_path.clone().map_or_else(
130                    |e| e,
131                    |mut path| {
132                        path.push("plugin.msgpackz");
133                        let canonical_plugin_path = canonicalize_path(engine_state, &path);
134                        Value::string(canonical_plugin_path.to_string_lossy(), span)
135                    },
136                )
137            },
138        );
139    }
140
141    record.push(
142        "home-path",
143        if let Some(path) = nu_path::home_dir() {
144            let canon_home_path = canonicalize_path(engine_state, path.as_ref());
145            Value::string(canon_home_path.to_string_lossy(), span)
146        } else {
147            Value::error(
148                ShellError::GenericError {
149                    error: "setting $nu.home-path failed".into(),
150                    msg: "Could not get home path".into(),
151                    span: Some(span),
152                    help: None,
153                    inner: vec![],
154                },
155                span,
156            )
157        },
158    );
159
160    record.push(
161        "data-dir",
162        if let Some(path) = nu_path::data_dir() {
163            let mut canon_data_path = canonicalize_path(engine_state, path.as_ref());
164            canon_data_path.push("nushell");
165            Value::string(canon_data_path.to_string_lossy(), span)
166        } else {
167            Value::error(
168                ShellError::GenericError {
169                    error: "setting $nu.data-dir failed".into(),
170                    msg: "Could not get data path".into(),
171                    span: Some(span),
172                    help: None,
173                    inner: vec![],
174                },
175                span,
176            )
177        },
178    );
179
180    record.push(
181        "cache-dir",
182        if let Some(path) = nu_path::cache_dir() {
183            let mut canon_cache_path = canonicalize_path(engine_state, path.as_ref());
184            canon_cache_path.push("nushell");
185            Value::string(canon_cache_path.to_string_lossy(), span)
186        } else {
187            Value::error(
188                ShellError::GenericError {
189                    error: "setting $nu.cache-dir failed".into(),
190                    msg: "Could not get cache path".into(),
191                    span: Some(span),
192                    help: None,
193                    inner: vec![],
194                },
195                span,
196            )
197        },
198    );
199
200    record.push(
201        "vendor-autoload-dirs",
202        Value::list(
203            get_vendor_autoload_dirs(engine_state)
204                .iter()
205                .map(|path| Value::string(path.to_string_lossy(), span))
206                .collect(),
207            span,
208        ),
209    );
210
211    record.push(
212        "user-autoload-dirs",
213        Value::list(
214            get_user_autoload_dirs(engine_state)
215                .iter()
216                .map(|path| Value::string(path.to_string_lossy(), span))
217                .collect(),
218            span,
219        ),
220    );
221
222    record.push("temp-path", {
223        let canon_temp_path = canonicalize_path(engine_state, &std::env::temp_dir());
224        Value::string(canon_temp_path.to_string_lossy(), span)
225    });
226
227    record.push("pid", Value::int(std::process::id().into(), span));
228
229    record.push("os-info", {
230        let ver = get_kernel_version();
231        Value::record(
232            record! {
233                "name" => Value::string(get_os_name(), span),
234                "arch" => Value::string(get_os_arch(), span),
235                "family" => Value::string(get_os_family(), span),
236                "kernel_version" => Value::string(ver, span),
237            },
238            span,
239        )
240    });
241
242    record.push(
243        "startup-time",
244        Value::duration(engine_state.get_startup_time(), span),
245    );
246
247    record.push(
248        "is-interactive",
249        Value::bool(engine_state.is_interactive, span),
250    );
251
252    record.push("is-login", Value::bool(engine_state.is_login, span));
253
254    record.push(
255        "history-enabled",
256        Value::bool(engine_state.history_enabled, span),
257    );
258
259    record.push(
260        "current-exe",
261        if let Ok(current_exe) = std::env::current_exe() {
262            Value::string(current_exe.to_string_lossy(), span)
263        } else {
264            Value::error(
265                ShellError::GenericError {
266                    error: "setting $nu.current-exe failed".into(),
267                    msg: "Could not get current executable path".into(),
268                    span: Some(span),
269                    help: None,
270                    inner: vec![],
271                },
272                span,
273            )
274        },
275    );
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}