Convert Path to list in main and preserve case (#14764)

# Description

Fixes multiple issues related to `ENV_CONVERSION` and
path-conversion-to-list.

* #14681 removed some calls to `convert_env_values()`, but we found that
this caused `nu -n` to no longer convert the path properly.
* `ENV_CONVERSIONS` have apparently never preserved case, meaning a
conversion with a key of `foo` would not update `$env.FOO` but rather
create a new environment variable with a different case.
* There was a partial code-path that attempted to solve this for `PATH`,
but it only worked for `PATH` and `Path`.
* `convert_env_values()`, which handled `ENV_CONVERSIONS` was called in
multiple places in the startup depending on flags.

This PR:

* Refactors the startup to handle the conversion in `main()` rather than
in each potential startup path
* Updates `get_env_var_insensitive()` functions added in #14390 to
return the name of the environment variable with its original case. This
allows code that updates environment variables to preserve the case.
* Makes use of the updated function in `ENV_CONVERSIONS` to preserve the
case of any updated environment variables. The `ENV_CONVERSION` key
itself is still case **insensitive**.
* Makes use of the updated function to preserve the case of the `PATH`
environment variable (normally handled separately, regardless of whether
or not there was an `ENV_CONVERSION` for it).

## Before

`env_convert_values` was run:

* Before the user `env.nu` ran, which included `nu -c <commandstring>`
and `nu <script.nu>`
* Before the REPL loaded, which included `nu -n`

## After

`env_convert_values` always runs once in `main()` before any config file
is processed or the REPL is started

# User-Facing Changes

Bug fixes

# Tests + Formatting

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

Added additional tests to prevent future regression.

# After Submitting

There is additional cleanup that should probably be done in
`convert_env_values()`. This function previously handled
`ENV_CONVERSIONS`, but there is no longer any need for this since
`convert_env_vars()` runs whenever `$env.ENV_CONVERSIONS` changes now.

This means that the only relevant task in the old `convert_env_values()`
is to convert the `PATH` to a list, and ensure that it is a list of
strings. It's still calling the `from_string` conversion on every
variable (just once) even though there are no `ENV_CONVERSIONS` at this
point.

Leaving that to another PR though, while we get the core issue fixed
with this one.
This commit is contained in:
Douglas
2025-01-10 11:18:44 -05:00
committed by GitHub
parent 3a1601de8e
commit 72d50cf8b7
12 changed files with 99 additions and 54 deletions

View File

@ -43,7 +43,7 @@ impl CommandCompletion {
let paths = working_set.permanent_state.get_env_var_insensitive("path");
if let Some(paths) = paths {
if let Some((_, paths)) = paths {
if let Ok(paths) = paths.as_list() {
for path in paths {
let path = path.coerce_str().unwrap_or_default();

View File

@ -1,5 +1,5 @@
use log::info;
use nu_engine::{convert_env_values, eval_block};
use nu_engine::eval_block;
use nu_parser::parse;
use nu_protocol::{
cli_error::report_compile_error,
@ -50,9 +50,6 @@ pub fn evaluate_commands(
}
}
// Translate environment variables from Strings to Values
convert_env_values(engine_state, stack)?;
// Parse the source code
let (block, delta) = {
if let Some(ref t_mode) = table_mode {

View File

@ -1,6 +1,6 @@
use crate::util::{eval_source, print_pipeline};
use log::{info, trace};
use nu_engine::{convert_env_values, eval_block};
use nu_engine::eval_block;
use nu_parser::parse;
use nu_path::canonicalize_with;
use nu_protocol::{
@ -22,9 +22,6 @@ pub fn evaluate_file(
stack: &mut Stack,
input: PipelineData,
) -> Result<(), ShellError> {
// Convert environment variables from Strings to Values and store them in the engine state.
convert_env_values(engine_state, stack)?;
let cwd = engine_state.cwd_as_string(Some(stack))?;
let file_path =

View File

@ -32,7 +32,9 @@ pub fn convert_env_vars(
) -> Result<(), ShellError> {
let conversions = conversions.as_record()?;
for (key, conversion) in conversions.into_iter() {
if let Some(val) = stack.get_env_var_insensitive(engine_state, key) {
if let Some((case_preserve_env_name, val)) =
stack.get_env_var_insensitive(engine_state, key)
{
match val.get_type() {
Type::String => {}
_ => continue,
@ -52,7 +54,7 @@ pub fn convert_env_vars(
.run_with_value(val.clone())?
.into_value(val.span())?;
stack.add_env_var(key.clone(), new_val);
stack.add_env_var(case_preserve_env_name.to_string(), new_val);
}
}
Ok(())
@ -64,7 +66,10 @@ pub fn convert_env_vars(
/// It returns Option instead of Result since we do want to translate all the values we can and
/// skip errors. This function is called in the main() so we want to keep running, we cannot just
/// exit.
pub fn convert_env_values(engine_state: &mut EngineState, stack: &Stack) -> Result<(), ShellError> {
pub fn convert_env_values(
engine_state: &mut EngineState,
stack: &mut Stack,
) -> Result<(), ShellError> {
let mut error = None;
let mut new_scope = HashMap::new();
@ -89,7 +94,7 @@ pub fn convert_env_values(engine_state: &mut EngineState, stack: &Stack) -> Resu
}
}
error = error.or_else(|| ensure_path(&mut new_scope, "PATH"));
error = error.or_else(|| ensure_path(engine_state, stack));
if let Ok(last_overlay_name) = &stack.last_overlay_name() {
if let Some(env_vars) = Arc::make_mut(&mut engine_state.env_vars).get_mut(last_overlay_name)
@ -242,7 +247,7 @@ pub fn path_str(
span: Span,
) -> Result<String, ShellError> {
let (pathname, pathval) = match stack.get_env_var_insensitive(engine_state, "path") {
Some(v) => Ok((if cfg!(windows) { "Path" } else { "PATH" }, v)),
Some((_, v)) => Ok((if cfg!(windows) { "Path" } else { "PATH" }, v)),
None => Err(ShellError::EnvVarNotFoundAtRuntime {
envvar_name: if cfg!(windows) {
"Path".to_string()
@ -362,11 +367,11 @@ fn get_converted_value(
)
}
fn ensure_path(scope: &mut HashMap<String, Value>, env_path_name: &str) -> Option<ShellError> {
fn ensure_path(engine_state: &EngineState, stack: &mut Stack) -> Option<ShellError> {
let mut error = None;
// If PATH/Path is still a string, force-convert it to a list
if let Some(value) = scope.get(env_path_name) {
if let Some((preserve_case_name, value)) = stack.get_env_var_insensitive(engine_state, "Path") {
let span = value.span();
match value {
Value::String { val, .. } => {
@ -375,15 +380,17 @@ fn ensure_path(scope: &mut HashMap<String, Value>, env_path_name: &str) -> Optio
.map(|p| Value::string(p.to_string_lossy().to_string(), span))
.collect();
scope.insert(env_path_name.to_string(), Value::list(paths, span));
stack.add_env_var(preserve_case_name.to_string(), Value::list(paths, span));
}
Value::List { vals, .. } => {
// Must be a list of strings
if !vals.iter().all(|v| matches!(v, Value::String { .. })) {
error = error.or_else(|| {
Some(ShellError::GenericError {
error: format!("Wrong {env_path_name} environment variable value"),
msg: format!("{env_path_name} must be a list of strings"),
error: format!(
"Incorrect {preserve_case_name} environment variable value"
),
msg: format!("{preserve_case_name} must be a list of strings"),
span: Some(span),
help: None,
inner: vec![],
@ -398,8 +405,8 @@ fn ensure_path(scope: &mut HashMap<String, Value>, env_path_name: &str) -> Optio
error = error.or_else(|| {
Some(ShellError::GenericError {
error: format!("Wrong {env_path_name} environment variable value"),
msg: format!("{env_path_name} must be a list of strings"),
error: format!("Incorrect {preserve_case_name} environment variable value"),
msg: format!("{preserve_case_name} must be a list of strings"),
span: Some(span),
help: None,
inner: vec![],

View File

@ -126,7 +126,10 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
}
fn get_env_var(&self, name: &str) -> Result<Option<&Value>, ShellError> {
Ok(self.stack.get_env_var_insensitive(&self.engine_state, name))
Ok(self
.stack
.get_env_var_insensitive(&self.engine_state, name)
.map(|(_, value)| value))
}
fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError> {

View File

@ -48,6 +48,7 @@ impl UseAnsiColoring {
let env_value = |env_name| {
engine_state
.get_env_var_insensitive(env_name)
.map(|(_, v)| v)
.and_then(|v| v.coerce_bool().ok())
.unwrap_or(false)
};
@ -60,7 +61,7 @@ impl UseAnsiColoring {
return false;
}
if let Some(cli_color) = engine_state.get_env_var_insensitive("clicolor") {
if let Some((_, cli_color)) = engine_state.get_env_var_insensitive("clicolor") {
if let Ok(cli_color) = cli_color.coerce_bool() {
return cli_color;
}

View File

@ -466,12 +466,16 @@ impl EngineState {
None
}
pub fn get_env_var_insensitive(&self, name: &str) -> Option<&Value> {
// Returns Some((name, value)) if found, None otherwise.
// When updating environment variables, make sure to use
// the same case (the returned "name") as the original
// environment variable name.
pub fn get_env_var_insensitive(&self, name: &str) -> Option<(&String, &Value)> {
for overlay_id in self.scope.active_overlays.iter().rev() {
let overlay_name = String::from_utf8_lossy(self.get_overlay_name(*overlay_id));
if let Some(env_vars) = self.env_vars.get(overlay_name.as_ref()) {
if let Some(v) = env_vars.iter().find(|(k, _)| k.eq_ignore_case(name)) {
return Some(v.1);
return Some((v.0, v.1));
}
}
}

View File

@ -481,16 +481,20 @@ impl Stack {
}
// Case-Insensitive version of get_env_var
// Returns Some((name, value)) if found, None otherwise.
// When updating environment variables, make sure to use
// the same case (from the returned "name") as the original
// environment variable name.
pub fn get_env_var_insensitive<'a>(
&'a self,
engine_state: &'a EngineState,
name: &str,
) -> Option<&'a Value> {
) -> Option<(&'a String, &'a Value)> {
for scope in self.env_vars.iter().rev() {
for active_overlay in self.active_overlays.iter().rev() {
if let Some(env_vars) = scope.get(active_overlay) {
if let Some(v) = env_vars.iter().find(|(k, _)| k.eq_ignore_case(name)) {
return Some(v.1);
return Some((v.0, v.1));
}
}
}
@ -506,7 +510,7 @@ impl Stack {
if !is_hidden {
if let Some(env_vars) = engine_state.env_vars.get(active_overlay) {
if let Some(v) = env_vars.iter().find(|(k, _)| k.eq_ignore_case(name)) {
return Some(v.1);
return Some((v.0, v.1));
}
}
}