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 Ok(())
64}
65
66pub 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 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 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
126pub 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 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
174pub 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 { .. }) => {} Err(e) => return Err(e),
188 }
189 }
190
191 Ok(env_vars_str)
192}
193
194pub 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 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
229pub 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 let cwd = if let Some(pwd) = stack.get_env_var(engine_state, "FILE_PWD") {
248 let cwd = env_to_string("FILE_PWD", pwd, engine_state, stack)?;
249 if Path::new(&cwd).is_absolute() {
250 cwd
251 } else {
252 return Err(ShellError::Generic(GenericError::new(
253 "Invalid current directory",
254 format!(
255 "The 'FILE_PWD' environment variable must be set to an absolute path. Found: '{cwd}'"
256 ),
257 pwd.span(),
258 )));
259 }
260 } else {
261 engine_state.cwd_as_string(Some(stack))?
262 };
263
264 let check_dir = |lib_dirs: Option<&Value>| -> Option<PathBuf> {
265 fn exists_with<P: AsRef<Path>, Q: AsRef<Path>>(path: P, relative_to: Q) -> Option<PathBuf> {
266 let path = absolute_with(path, relative_to).ok()?;
267 path.exists().then_some(path)
268 }
269 if let Some(p) = exists_with(filename, &cwd) {
270 return Some(p);
271 }
272 let path = Path::new(filename);
273 if !path.is_relative() {
274 return None;
275 }
276
277 lib_dirs?
278 .as_list()
279 .ok()?
280 .iter()
281 .map(|lib_dir| -> Option<PathBuf> {
282 let dir = lib_dir.to_path().ok()?;
283 let dir_abs = exists_with(dir, &cwd)?;
284 exists_with(filename, dir_abs)
285 })
286 .find(Option::is_some)
287 .flatten()
288 };
289
290 let lib_dirs = dirs_var.and_then(|var_id| engine_state.get_var(var_id).const_val.as_ref());
291 let lib_dirs_fallback = stack.get_env_var(engine_state, "NU_LIB_DIRS");
293
294 Ok(check_dir(lib_dirs).or_else(|| check_dir(lib_dirs_fallback)))
295}
296
297fn get_converted_value(
298 engine_state: &EngineState,
299 stack: &Stack,
300 name: &str,
301 orig_val: &Value,
302 direction: &str,
303) -> Result<Value, ConversionError> {
304 let conversion = stack
305 .get_env_var(engine_state, ENV_CONVERSIONS)
306 .ok_or(ConversionError::CellPathError)?
307 .as_record()?
308 .get(name)
309 .ok_or(ConversionError::CellPathError)?
310 .as_record()?
311 .get(direction)
312 .ok_or(ConversionError::CellPathError)?
313 .as_closure()?;
314
315 Ok(
316 ClosureEvalOnce::new(engine_state, stack, conversion.clone())
317 .debug(false)
318 .run_with_value(orig_val.clone())?
319 .into_value(orig_val.span())?,
320 )
321}
322
323fn ensure_path(engine_state: &EngineState, stack: &mut Stack) -> Option<ShellError> {
324 let mut error = None;
325 let preserve_case_name = if cfg!(windows) { "Path" } else { "PATH" };
326
327 if let Some(value) = stack.get_env_var(engine_state, "Path") {
329 let span = value.span();
330 match value {
331 Value::String { val, .. } => {
332 let paths = std::env::split_paths(val)
334 .map(|p| Value::string(p.to_string_lossy().to_string(), span))
335 .collect();
336
337 stack.add_env_var(preserve_case_name.to_string(), Value::list(paths, span));
338 }
339 Value::List { vals, .. } => {
340 if !vals.iter().all(|v| matches!(v, Value::String { .. })) {
342 error = error.or_else(|| {
343 Some(ShellError::Generic(GenericError::new(
344 format!("Incorrect {preserve_case_name} environment variable value"),
345 format!("{preserve_case_name} must be a list of strings"),
346 span,
347 )))
348 });
349 }
350 }
351
352 val => {
353 let span = val.span();
355
356 error = error.or_else(|| {
357 Some(ShellError::Generic(GenericError::new(
358 format!("Incorrect {preserve_case_name} environment variable value"),
359 format!("{preserve_case_name} must be a list of strings"),
360 span,
361 )))
362 });
363 }
364 }
365 }
366
367 error
368}