nu_engine/
env.rs

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