Skip to main content

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, HistoryPath, 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    shell_error::generic::GenericError,
13};
14use nu_system::os_info::{get_kernel_version, get_os_arch, get_os_family, get_os_name};
15use std::{
16    path::{Path, PathBuf},
17    sync::Arc,
18};
19
20/// Create a Value for `$nu`.
21// Note: When adding new constants to $nu, please update the doc at https://nushell.sh/book/special_variables.html
22// or at least add a TODO/reminder issue in nushell.github.io so we don't lose track of it.
23pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Value {
24    fn canonicalize_path(engine_state: &EngineState, path: &Path) -> PathBuf {
25        #[allow(deprecated)]
26        let cwd = engine_state.current_work_dir();
27
28        if path.exists() {
29            match nu_path::canonicalize_with(path, cwd) {
30                Ok(canon_path) => canon_path,
31                Err(_) => path.to_owned(),
32            }
33        } else {
34            path.to_owned()
35        }
36    }
37
38    let mut record = Record::new();
39
40    let config_home = &engine_state.config_dirs.config_home;
41    let canon_config_home = canonicalize_path(engine_state, config_home);
42
43    record.push(
44        "default-config-dir",
45        Value::string(canon_config_home.to_string_lossy(), span),
46    );
47
48    record.push(
49        "config-path",
50        Value::string(
51            canonicalize_path(engine_state, engine_state.config_dirs.config_file.as_path())
52                .to_string_lossy(),
53            span,
54        ),
55    );
56
57    record.push(
58        "env-path",
59        Value::string(
60            canonicalize_path(engine_state, engine_state.config_dirs.env_file.as_path())
61                .to_string_lossy(),
62            span,
63        ),
64    );
65
66    record.push(
67        "history-path",
68        match &engine_state.config.history.path {
69            HistoryPath::Disabled => Value::string("", span),
70            HistoryPath::Custom(custom_path) => {
71                let effective_path = if custom_path.is_dir() {
72                    custom_path.join(engine_state.config.history.file_format.default_file_name())
73                } else {
74                    custom_path.clone()
75                };
76                let canon_hist_path = canonicalize_path(engine_state, &effective_path);
77                Value::string(canon_hist_path.to_string_lossy(), span)
78            }
79            HistoryPath::Default => {
80                // Use the same resolution path as history backends so `$nu.history-path`
81                // always matches the file reedline opens.
82                let hist_path = engine_state
83                    .config
84                    .history
85                    .file_path(config_home)
86                    .unwrap_or_else(|| {
87                        config_home
88                            .join(engine_state.config.history.file_format.default_file_name())
89                    });
90                let canon_hist_path = canonicalize_path(engine_state, &hist_path);
91                Value::string(canon_hist_path.to_string_lossy(), span)
92            }
93        },
94    );
95
96    record.push(
97        "loginshell-path",
98        Value::string(
99            canonicalize_path(engine_state, &config_home.join("login.nu")).to_string_lossy(),
100            span,
101        ),
102    );
103
104    #[cfg(feature = "plugin")]
105    {
106        // Prefer the live plugin_path (set once at startup from config_dirs).
107        let plugin_path = engine_state
108            .plugin_path
109            .as_deref()
110            .unwrap_or_else(|| engine_state.config_dirs.plugin_file.as_path());
111        record.push(
112            "plugin-path",
113            Value::string(
114                canonicalize_path(engine_state, plugin_path).to_string_lossy(),
115                span,
116            ),
117        );
118    }
119
120    record.push(
121        "home-dir",
122        Value::string(
123            canonicalize_path(engine_state, &engine_state.config_dirs.home_dir).to_string_lossy(),
124            span,
125        ),
126    );
127
128    record.push(
129        "data-dir",
130        Value::string(
131            canonicalize_path(engine_state, &engine_state.config_dirs.data_home).to_string_lossy(),
132            span,
133        ),
134    );
135
136    record.push(
137        "cache-dir",
138        Value::string(
139            canonicalize_path(engine_state, &engine_state.config_dirs.cache_home).to_string_lossy(),
140            span,
141        ),
142    );
143
144    record.push(
145        "vendor-autoload-dirs",
146        Value::list(
147            engine_state
148                .config_dirs
149                .vendor_autoload_dirs
150                .iter()
151                .map(|path| Value::string(path.to_string_lossy(), span))
152                .collect(),
153            span,
154        ),
155    );
156
157    record.push(
158        "user-autoload-dirs",
159        Value::list(
160            engine_state
161                .config_dirs
162                .user_autoload_dirs
163                .iter()
164                .map(|path| Value::string(path.to_string_lossy(), span))
165                .collect(),
166            span,
167        ),
168    );
169
170    record.push("temp-dir", {
171        let canon_temp_path = canonicalize_path(engine_state, &std::env::temp_dir());
172        Value::string(canon_temp_path.to_string_lossy(), span)
173    });
174
175    record.push("pid", Value::int(std::process::id().into(), span));
176
177    record.push("os-info", {
178        let ver = get_kernel_version();
179        Value::record(
180            record! {
181                "name" => Value::string(get_os_name(), span),
182                "arch" => Value::string(get_os_arch(), span),
183                "family" => Value::string(get_os_family(), span),
184                "kernel_version" => Value::string(ver, span),
185            },
186            span,
187        )
188    });
189
190    record.push(
191        "startup-time",
192        Value::duration(engine_state.get_startup_time(), span),
193    );
194
195    record.push(
196        "is-interactive",
197        Value::bool(engine_state.is_interactive, span),
198    );
199
200    record.push("is-login", Value::bool(engine_state.is_login, span));
201
202    record.push(
203        "history-enabled",
204        Value::bool(engine_state.history_enabled, span),
205    );
206
207    record.push(
208        "current-exe",
209        if let Ok(current_exe) = std::env::current_exe() {
210            Value::string(current_exe.to_string_lossy(), span)
211        } else {
212            Value::error(
213                ShellError::Generic(GenericError::new(
214                    "setting $nu.current-exe failed",
215                    "Could not get current executable path",
216                    span,
217                )),
218                span,
219            )
220        },
221    );
222
223    record.push("is-lsp", Value::bool(engine_state.is_lsp, span));
224    record.push("is-mcp", Value::bool(engine_state.is_mcp, span));
225
226    Value::record(record, span)
227}
228
229fn eval_const_call(
230    working_set: &StateWorkingSet,
231    call: &Call,
232    input: PipelineData,
233) -> Result<PipelineData, ShellError> {
234    let decl = working_set.get_decl(call.decl_id);
235
236    if !decl.is_const() {
237        return Err(ShellError::NotAConstCommand { span: call.head });
238    }
239
240    if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") {
241        // It would require re-implementing get_full_help() for const evaluation. Assuming that
242        // getting help messages at parse-time is rare enough, we can simply disallow it.
243        return Err(ShellError::NotAConstHelp { span: call.head });
244    }
245
246    decl.run_const(working_set, &call.into(), input)
247}
248
249pub fn eval_const_subexpression(
250    working_set: &StateWorkingSet,
251    block: &Block,
252    mut input: PipelineData,
253    span: Span,
254) -> Result<PipelineData, ShellError> {
255    for pipeline in block.pipelines.iter() {
256        for element in pipeline.elements.iter() {
257            if element.redirection.is_some() {
258                return Err(ShellError::NotAConstant { span });
259            }
260
261            input = eval_constant_with_input(working_set, &element.expr, input)?
262        }
263    }
264
265    Ok(input)
266}
267
268pub fn eval_constant_with_input(
269    working_set: &StateWorkingSet,
270    expr: &Expression,
271    input: PipelineData,
272) -> Result<PipelineData, ShellError> {
273    match &expr.expr {
274        Expr::Call(call) => eval_const_call(working_set, call, input),
275        Expr::Subexpression(block_id) => {
276            let block = working_set.get_block(*block_id);
277            eval_const_subexpression(working_set, block, input, expr.span(&working_set))
278        }
279        _ => eval_constant(working_set, expr).map(|v| PipelineData::value(v, None)),
280    }
281}
282
283/// Evaluate a constant value at parse time
284pub fn eval_constant(
285    working_set: &StateWorkingSet,
286    expr: &Expression,
287) -> Result<Value, ShellError> {
288    // TODO: Allow debugging const eval
289    <EvalConst as Eval>::eval::<WithoutDebug>(working_set, &mut (), expr)
290}
291
292struct EvalConst;
293
294impl Eval for EvalConst {
295    type State<'a> = &'a StateWorkingSet<'a>;
296
297    type MutState = ();
298
299    fn get_config(state: Self::State<'_>, _: &mut ()) -> Arc<Config> {
300        state.get_config().clone()
301    }
302
303    fn eval_var(
304        working_set: &StateWorkingSet,
305        _: &mut (),
306        var_id: VarId,
307        span: Span,
308    ) -> Result<Value, ShellError> {
309        match working_set.get_variable(var_id).const_val.as_ref() {
310            Some(val) => Ok(val.clone()),
311            None => Err(ShellError::NotAConstant { span }),
312        }
313    }
314
315    fn eval_call<D: DebugContext>(
316        working_set: &StateWorkingSet,
317        _: &mut (),
318        call: &Call,
319        span: Span,
320    ) -> Result<Value, ShellError> {
321        // TODO: Allow debugging const eval
322        // TODO: eval.rs uses call.head for the span rather than expr.span
323        eval_const_call(working_set, call, PipelineData::empty())?.into_value(span)
324    }
325
326    fn eval_external_call(
327        _: &StateWorkingSet,
328        _: &mut (),
329        _: &Expression,
330        _: &[ExternalArgument],
331        span: Span,
332    ) -> Result<Value, ShellError> {
333        // TODO: It may be more helpful to give not_a_const_command error
334        Err(ShellError::NotAConstant { span })
335    }
336
337    fn eval_collect<D: DebugContext>(
338        _: &StateWorkingSet,
339        _: &mut (),
340        _var_id: VarId,
341        expr: &Expression,
342    ) -> Result<Value, ShellError> {
343        Err(ShellError::NotAConstant { span: expr.span })
344    }
345
346    fn eval_subexpression<D: DebugContext>(
347        working_set: &StateWorkingSet,
348        _: &mut (),
349        block_id: BlockId,
350        span: Span,
351    ) -> Result<Value, ShellError> {
352        // If parsing errors exist in the subexpression, don't bother to evaluate it.
353        if working_set
354            .parse_errors
355            .iter()
356            .any(|error| span.contains_span(error.span()))
357        {
358            return Err(ShellError::ParseErrorInConstant { span });
359        }
360        // TODO: Allow debugging const eval
361        let block = working_set.get_block(block_id);
362        eval_const_subexpression(working_set, block, PipelineData::empty(), span)?.into_value(span)
363    }
364
365    fn regex_match(
366        _: &StateWorkingSet,
367        _op_span: Span,
368        _: &Value,
369        _: &Value,
370        _: bool,
371        expr_span: Span,
372    ) -> Result<Value, ShellError> {
373        Err(ShellError::NotAConstant { span: expr_span })
374    }
375
376    fn eval_assignment<D: DebugContext>(
377        _: &StateWorkingSet,
378        _: &mut (),
379        _: &Expression,
380        _: &Expression,
381        _: Assignment,
382        _op_span: Span,
383        expr_span: Span,
384    ) -> Result<Value, ShellError> {
385        // TODO: Allow debugging const eval
386        Err(ShellError::NotAConstant { span: expr_span })
387    }
388
389    fn eval_row_condition_or_closure(
390        _: &StateWorkingSet,
391        _: &mut (),
392        _: BlockId,
393        span: Span,
394    ) -> Result<Value, ShellError> {
395        Err(ShellError::NotAConstant { span })
396    }
397
398    fn eval_overlay(_: &StateWorkingSet, span: Span) -> Result<Value, ShellError> {
399        Err(ShellError::NotAConstant { span })
400    }
401
402    fn unreachable(working_set: &StateWorkingSet, expr: &Expression) -> Result<Value, ShellError> {
403        Err(ShellError::NotAConstant {
404            span: expr.span(&working_set),
405        })
406    }
407}