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