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";
17pub 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
32pub 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
67pub 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 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 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
127pub 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 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
175pub 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 { .. }) => {} Err(e) => return Err(e),
189 }
190 }
191
192 Ok(env_vars_str)
193}
194
195pub 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 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
230pub 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 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 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 let Some(value) = stack.get_env_var(engine_state, "Path") {
330 let span = value.span();
331 match value {
332 Value::String { val, .. } => {
333 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 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 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}