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