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