nu_engine/
env.rs

1use crate::ClosureEvalOnce;
2use nu_path::canonicalize_with;
3use nu_protocol::{
4    ShellError, Span, Type, Value, VarId,
5    ast::Expr,
6    engine::{Call, EngineState, Stack},
7};
8use std::{
9    collections::HashMap,
10    path::{Path, PathBuf},
11    sync::Arc,
12};
13
14pub const ENV_CONVERSIONS: &str = "ENV_CONVERSIONS";
15
16enum ConversionError {
17    ShellError(ShellError),
18    CellPathError,
19}
20
21impl From<ShellError> for ConversionError {
22    fn from(value: ShellError) -> Self {
23        Self::ShellError(value)
24    }
25}
26
27/// Translate environment variables from Strings to Values.
28pub fn convert_env_vars(
29    stack: &mut Stack,
30    engine_state: &EngineState,
31    conversions: &Value,
32) -> Result<(), ShellError> {
33    let conversions = conversions.as_record()?;
34    for (key, conversion) in conversions.into_iter() {
35        if let Some((case_preserve_env_name, val)) =
36            stack.get_env_var_insensitive(engine_state, key)
37        {
38            match val.get_type() {
39                Type::String => {}
40                _ => continue,
41            }
42
43            let conversion = conversion
44                .as_record()?
45                .get("from_string")
46                .ok_or(ShellError::MissingRequiredColumn {
47                    column: "from_string",
48                    span: conversion.span(),
49                })?
50                .as_closure()?;
51
52            let new_val = ClosureEvalOnce::new(engine_state, stack, conversion.clone())
53                .debug(false)
54                .run_with_value(val.clone())?
55                .into_value(val.span())?;
56
57            stack.add_env_var(case_preserve_env_name.to_string(), new_val);
58        }
59    }
60    Ok(())
61}
62
63/// Translate environment variables from Strings to Values. Requires config to be already set up in
64/// case the user defined custom env conversions in config.nu.
65///
66/// It returns Option instead of Result since we do want to translate all the values we can and
67/// skip errors. This function is called in the main() so we want to keep running, we cannot just
68/// exit.
69pub fn convert_env_values(
70    engine_state: &mut EngineState,
71    stack: &mut Stack,
72) -> Result<(), ShellError> {
73    let mut error = None;
74
75    let mut new_scope = HashMap::new();
76
77    let env_vars = engine_state.render_env_vars();
78
79    for (name, val) in env_vars {
80        if let Value::String { .. } = val {
81            // Only run from_string on string values
82            match get_converted_value(engine_state, stack, name, val, "from_string") {
83                Ok(v) => {
84                    let _ = new_scope.insert(name.to_string(), v);
85                }
86                Err(ConversionError::ShellError(e)) => error = error.or(Some(e)),
87                Err(ConversionError::CellPathError) => {
88                    let _ = new_scope.insert(name.to_string(), val.clone());
89                }
90            }
91        } else {
92            // Skip values that are already converted (not a string)
93            let _ = new_scope.insert(name.to_string(), val.clone());
94        }
95    }
96
97    error = error.or_else(|| ensure_path(engine_state, stack));
98
99    if let Ok(last_overlay_name) = &stack.last_overlay_name() {
100        if let Some(env_vars) = Arc::make_mut(&mut engine_state.env_vars).get_mut(last_overlay_name)
101        {
102            for (k, v) in new_scope {
103                env_vars.insert(k, v);
104            }
105        } else {
106            error = error.or_else(|| {
107                Some(ShellError::NushellFailedHelp { msg: "Last active overlay not found in permanent state.".into(), help: "This error happened during the conversion of environment variables from strings to Nushell values.".into() })
108            });
109        }
110    } else {
111        error = error.or_else(|| {
112            Some(ShellError::NushellFailedHelp { msg: "Last active overlay not found in stack.".into(), help: "This error happened during the conversion of environment variables from strings to Nushell values.".into() })
113        });
114    }
115
116    if let Some(err) = error {
117        Err(err)
118    } else {
119        Ok(())
120    }
121}
122
123/// Translate one environment variable from Value to String
124///
125/// Returns Ok(None) if the env var is not
126pub fn env_to_string(
127    env_name: &str,
128    value: &Value,
129    engine_state: &EngineState,
130    stack: &Stack,
131) -> Result<String, ShellError> {
132    match get_converted_value(engine_state, stack, env_name, value, "to_string") {
133        Ok(v) => Ok(v.coerce_into_string()?),
134        Err(ConversionError::ShellError(e)) => Err(e),
135        Err(ConversionError::CellPathError) => match value.coerce_string() {
136            Ok(s) => Ok(s),
137            Err(_) => {
138                if env_name.to_lowercase() == "path" {
139                    // Try to convert PATH/Path list to a string
140                    match value {
141                        Value::List { vals, .. } => {
142                            let paths: Vec<String> = vals
143                                .iter()
144                                .filter_map(|v| v.coerce_str().ok())
145                                .map(|s| nu_path::expand_tilde(&*s).to_string_lossy().into_owned())
146                                .collect();
147
148                            std::env::join_paths(paths.iter().map(AsRef::<str>::as_ref))
149                                .map(|p| p.to_string_lossy().to_string())
150                                .map_err(|_| ShellError::EnvVarNotAString {
151                                    envvar_name: env_name.to_string(),
152                                    span: value.span(),
153                                })
154                        }
155                        _ => Err(ShellError::EnvVarNotAString {
156                            envvar_name: env_name.to_string(),
157                            span: value.span(),
158                        }),
159                    }
160                } else {
161                    Err(ShellError::EnvVarNotAString {
162                        envvar_name: env_name.to_string(),
163                        span: value.span(),
164                    })
165                }
166            }
167        },
168    }
169}
170
171/// Translate all environment variables from Values to Strings
172pub fn env_to_strings(
173    engine_state: &EngineState,
174    stack: &Stack,
175) -> Result<HashMap<String, String>, ShellError> {
176    let env_vars = stack.get_env_vars(engine_state);
177    let mut env_vars_str = HashMap::new();
178    for (env_name, val) in env_vars {
179        match env_to_string(&env_name, &val, engine_state, stack) {
180            Ok(val_str) => {
181                env_vars_str.insert(env_name, val_str);
182            }
183            Err(ShellError::EnvVarNotAString { .. }) => {} // ignore non-string values
184            Err(e) => return Err(e),
185        }
186    }
187
188    Ok(env_vars_str)
189}
190
191/// Get the contents of path environment variable as a list of strings
192pub fn path_str(
193    engine_state: &EngineState,
194    stack: &Stack,
195    span: Span,
196) -> Result<String, ShellError> {
197    let (pathname, pathval) = match stack.get_env_var_insensitive(engine_state, "path") {
198        Some((_, v)) => Ok((if cfg!(windows) { "Path" } else { "PATH" }, v)),
199        None => Err(ShellError::EnvVarNotFoundAtRuntime {
200            envvar_name: if cfg!(windows) {
201                "Path".to_string()
202            } else {
203                "PATH".to_string()
204            },
205            span,
206        }),
207    }?;
208
209    env_to_string(pathname, pathval, engine_state, stack)
210}
211
212pub const DIR_VAR_PARSER_INFO: &str = "dirs_var";
213pub fn get_dirs_var_from_call(stack: &Stack, call: &Call) -> Option<VarId> {
214    call.get_parser_info(stack, DIR_VAR_PARSER_INFO)
215        .and_then(|x| {
216            if let Expr::Var(id) = x.expr {
217                Some(id)
218            } else {
219                None
220            }
221        })
222}
223
224/// This helper function is used to find files during eval
225///
226/// First, the actual current working directory is selected as
227///   a) the directory of a file currently being parsed
228///   b) current working directory (PWD)
229///
230/// Then, if the file is not found in the actual cwd, NU_LIB_DIRS is checked.
231/// If there is a relative path in NU_LIB_DIRS, it is assumed to be relative to the actual cwd
232/// determined in the first step.
233///
234/// Always returns an absolute path
235pub fn find_in_dirs_env(
236    filename: &str,
237    engine_state: &EngineState,
238    stack: &Stack,
239    dirs_var: Option<VarId>,
240) -> Result<Option<PathBuf>, ShellError> {
241    // Choose whether to use file-relative or PWD-relative path
242    let cwd = if let Some(pwd) = stack.get_env_var(engine_state, "FILE_PWD") {
243        match env_to_string("FILE_PWD", pwd, engine_state, stack) {
244            Ok(cwd) => {
245                if Path::new(&cwd).is_absolute() {
246                    cwd
247                } else {
248                    return Err(ShellError::GenericError {
249                        error: "Invalid current directory".into(),
250                        msg: format!(
251                            "The 'FILE_PWD' environment variable must be set to an absolute path. Found: '{cwd}'"
252                        ),
253                        span: Some(pwd.span()),
254                        help: None,
255                        inner: vec![],
256                    });
257                }
258            }
259            Err(e) => return Err(e),
260        }
261    } else {
262        engine_state.cwd_as_string(Some(stack))?
263    };
264
265    let check_dir = |lib_dirs: Option<&Value>| -> Option<PathBuf> {
266        if let Ok(p) = canonicalize_with(filename, &cwd) {
267            return Some(p);
268        }
269        let path = Path::new(filename);
270        if !path.is_relative() {
271            return None;
272        }
273
274        lib_dirs?
275            .as_list()
276            .ok()?
277            .iter()
278            .map(|lib_dir| -> Option<PathBuf> {
279                let dir = lib_dir.to_path().ok()?;
280                let dir_abs = canonicalize_with(dir, &cwd).ok()?;
281                canonicalize_with(filename, dir_abs).ok()
282            })
283            .find(Option::is_some)
284            .flatten()
285    };
286
287    let lib_dirs = dirs_var.and_then(|var_id| engine_state.get_var(var_id).const_val.as_ref());
288    // TODO: remove (see #8310)
289    let lib_dirs_fallback = stack.get_env_var(engine_state, "NU_LIB_DIRS");
290
291    Ok(check_dir(lib_dirs).or_else(|| check_dir(lib_dirs_fallback)))
292}
293
294fn get_converted_value(
295    engine_state: &EngineState,
296    stack: &Stack,
297    name: &str,
298    orig_val: &Value,
299    direction: &str,
300) -> Result<Value, ConversionError> {
301    let conversion = stack
302        .get_env_var(engine_state, ENV_CONVERSIONS)
303        .ok_or(ConversionError::CellPathError)?
304        .as_record()?
305        .get(name)
306        .ok_or(ConversionError::CellPathError)?
307        .as_record()?
308        .get(direction)
309        .ok_or(ConversionError::CellPathError)?
310        .as_closure()?;
311
312    Ok(
313        ClosureEvalOnce::new(engine_state, stack, conversion.clone())
314            .debug(false)
315            .run_with_value(orig_val.clone())?
316            .into_value(orig_val.span())?,
317    )
318}
319
320fn ensure_path(engine_state: &EngineState, stack: &mut Stack) -> Option<ShellError> {
321    let mut error = None;
322
323    // If PATH/Path is still a string, force-convert it to a list
324    if let Some((preserve_case_name, value)) = stack.get_env_var_insensitive(engine_state, "Path") {
325        let span = value.span();
326        match value {
327            Value::String { val, .. } => {
328                // Force-split path into a list
329                let paths = std::env::split_paths(val)
330                    .map(|p| Value::string(p.to_string_lossy().to_string(), span))
331                    .collect();
332
333                stack.add_env_var(preserve_case_name.to_string(), Value::list(paths, span));
334            }
335            Value::List { vals, .. } => {
336                // Must be a list of strings
337                if !vals.iter().all(|v| matches!(v, Value::String { .. })) {
338                    error = error.or_else(|| {
339                        Some(ShellError::GenericError {
340                            error: format!(
341                                "Incorrect {preserve_case_name} environment variable value"
342                            ),
343                            msg: format!("{preserve_case_name} must be a list of strings"),
344                            span: Some(span),
345                            help: None,
346                            inner: vec![],
347                        })
348                    });
349                }
350            }
351
352            val => {
353                // All other values are errors
354                let span = val.span();
355
356                error = error.or_else(|| {
357                    Some(ShellError::GenericError {
358                        error: format!("Incorrect {preserve_case_name} environment variable value"),
359                        msg: format!("{preserve_case_name} must be a list of strings"),
360                        span: Some(span),
361                        help: None,
362                        inner: vec![],
363                    })
364                });
365            }
366        }
367    }
368
369    error
370}