Skip to main content

nu_engine/
env.rs

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