mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 00:13:21 +01:00
Change environment variables to be case-preserving (#12701)
This PR changes `$env` to be **case-preserving** instead of case-sensitive. That is, it preserves the case of the environment variable when it is first assigned, but subsequent retrieval and update ignores the case. Notably, both `$env.PATH` and `$env.Path` can now be used to read or set the environment variable, but child processes will always see the correct case based on the platform. Fixes #11268. --- This feature was surprising simple to implement, because most of the infrastructure to support case-insensitive cell path access already exists. The `get` command extracts data using a cell path in a case-insensitive way (!), but accepts a `--sensitive` flag. (I think this should be flipped around?)
This commit is contained in:
parent
21ebdfe8d7
commit
52d99cc60c
@ -11,6 +11,7 @@ use nu_protocol::{
|
|||||||
Config, FromValue, IntoPipelineData, OutDest, PipelineData, ShellError, Span, Spanned, Type,
|
Config, FromValue, IntoPipelineData, OutDest, PipelineData, ShellError, Span, Spanned, Type,
|
||||||
Value, VarId, ENV_VARIABLE_ID,
|
Value, VarId, ENV_VARIABLE_ID,
|
||||||
};
|
};
|
||||||
|
use nu_utils::IgnoreCaseExt;
|
||||||
use std::{borrow::Cow, fs::OpenOptions, path::PathBuf};
|
use std::{borrow::Cow, fs::OpenOptions, path::PathBuf};
|
||||||
|
|
||||||
pub fn eval_call<D: DebugContext>(
|
pub fn eval_call<D: DebugContext>(
|
||||||
@ -769,40 +770,48 @@ impl Eval for EvalRuntime {
|
|||||||
if is_env || engine_state.get_var(*var_id).mutable {
|
if is_env || engine_state.get_var(*var_id).mutable {
|
||||||
let mut lhs =
|
let mut lhs =
|
||||||
eval_expression::<D>(engine_state, stack, &cell_path.head)?;
|
eval_expression::<D>(engine_state, stack, &cell_path.head)?;
|
||||||
|
|
||||||
lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?;
|
|
||||||
if is_env {
|
if is_env {
|
||||||
|
// Reject attempts to assign to the entire $env
|
||||||
if cell_path.tail.is_empty() {
|
if cell_path.tail.is_empty() {
|
||||||
return Err(ShellError::CannotReplaceEnv {
|
return Err(ShellError::CannotReplaceEnv {
|
||||||
span: cell_path.head.span,
|
span: cell_path.head.span,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// The special $env treatment: for something like $env.config.history.max_size = 2000,
|
// Updating environment variables should be case-preserving,
|
||||||
// get $env.config (or whichever one it is) AFTER the above mutation, and set it
|
// so we need to figure out the original key before we do anything.
|
||||||
// as the "config" environment variable.
|
let (key, span) = match &cell_path.tail[0] {
|
||||||
let vardata =
|
PathMember::String { val, span, .. } => (val.to_string(), span),
|
||||||
lhs.follow_cell_path(&[cell_path.tail[0].clone()], false)?;
|
PathMember::Int { val, span, .. } => (val.to_string(), span),
|
||||||
match &cell_path.tail[0] {
|
};
|
||||||
PathMember::String { val, span, .. } => {
|
let original_key = if let Value::Record { val: record, .. } = &lhs {
|
||||||
if val == "FILE_PWD"
|
record
|
||||||
|| val == "CURRENT_FILE"
|
.iter()
|
||||||
|| val == "PWD"
|
.rev()
|
||||||
{
|
.map(|(k, _)| k)
|
||||||
return Err(ShellError::AutomaticEnvVarSetManually {
|
.find(|x| x.eq_ignore_case(&key))
|
||||||
envvar_name: val.to_string(),
|
.cloned()
|
||||||
span: *span,
|
.unwrap_or(key)
|
||||||
});
|
} else {
|
||||||
} else {
|
key
|
||||||
stack.add_env_var(val.to_string(), vardata);
|
};
|
||||||
}
|
|
||||||
}
|
// Retrieve the updated environment value.
|
||||||
// In case someone really wants an integer env-var
|
lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?;
|
||||||
PathMember::Int { val, .. } => {
|
let value =
|
||||||
stack.add_env_var(val.to_string(), vardata);
|
lhs.follow_cell_path(&[cell_path.tail[0].clone()], true)?;
|
||||||
}
|
|
||||||
|
// Reject attempts to set automatic environment variables.
|
||||||
|
if is_automatic_env_var(&original_key) {
|
||||||
|
return Err(ShellError::AutomaticEnvVarSetManually {
|
||||||
|
envvar_name: original_key,
|
||||||
|
span: *span,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.add_env_var(original_key, value);
|
||||||
} else {
|
} else {
|
||||||
|
lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?;
|
||||||
stack.add_var(*var_id, lhs);
|
stack.add_var(*var_id, lhs);
|
||||||
}
|
}
|
||||||
Ok(Value::nothing(cell_path.head.span))
|
Ok(Value::nothing(cell_path.head.span))
|
||||||
@ -854,3 +863,19 @@ impl Eval for EvalRuntime {
|
|||||||
Ok(Value::nothing(expr.span))
|
Ok(Value::nothing(expr.span))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether a string, when used as the name of an environment variable,
|
||||||
|
/// is considered an automatic environment variable.
|
||||||
|
///
|
||||||
|
/// An automatic environment variable cannot be assigned to by user code.
|
||||||
|
/// Current there are three of them: $env.PWD, $env.FILE_PWD, $env.CURRENT_FILE
|
||||||
|
fn is_automatic_env_var(var: &str) -> bool {
|
||||||
|
let names = ["PWD", "FILE_PWD", "CURRENT_FILE"];
|
||||||
|
names.iter().any(|&name| {
|
||||||
|
if cfg!(windows) {
|
||||||
|
name.eq_ignore_case(var)
|
||||||
|
} else {
|
||||||
|
name.eq(var)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
debugger::DebugContext,
|
debugger::DebugContext,
|
||||||
Config, IntoInterruptiblePipelineData, Range, Record, ShellError, Span, Value, VarId,
|
Config, IntoInterruptiblePipelineData, Range, Record, ShellError, Span, Value, VarId,
|
||||||
|
ENV_VARIABLE_ID,
|
||||||
};
|
};
|
||||||
use std::{borrow::Cow, collections::HashMap};
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
@ -37,7 +38,13 @@ pub trait Eval {
|
|||||||
Expr::FullCellPath(cell_path) => {
|
Expr::FullCellPath(cell_path) => {
|
||||||
let value = Self::eval::<D>(state, mut_state, &cell_path.head)?;
|
let value = Self::eval::<D>(state, mut_state, &cell_path.head)?;
|
||||||
|
|
||||||
value.follow_cell_path(&cell_path.tail, false)
|
// Cell paths are usually case-sensitive, but we give $env
|
||||||
|
// special treatment.
|
||||||
|
if cell_path.head.expr == Expr::Var(ENV_VARIABLE_ID) {
|
||||||
|
value.follow_cell_path(&cell_path.tail, true)
|
||||||
|
} else {
|
||||||
|
value.follow_cell_path(&cell_path.tail, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Expr::DateTime(dt) => Ok(Value::date(*dt, expr.span)),
|
Expr::DateTime(dt) => Ok(Value::date(*dt, expr.span)),
|
||||||
Expr::List(list) => {
|
Expr::List(list) => {
|
||||||
|
@ -194,3 +194,15 @@ fn env_var_not_var() {
|
|||||||
");
|
");
|
||||||
assert!(actual.err.contains("use $env.PWD instead of $PWD"));
|
assert!(actual.err.contains("use $env.PWD instead of $PWD"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn env_var_case_insensitive() {
|
||||||
|
let actual = nu!("
|
||||||
|
$env.foo = 111
|
||||||
|
print $env.Foo
|
||||||
|
$env.FOO = 222
|
||||||
|
print $env.foo
|
||||||
|
");
|
||||||
|
assert!(actual.out.contains("111"));
|
||||||
|
assert!(actual.out.contains("222"));
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user