Douglas 72d50cf8b7
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.
2025-01-10 10:18:44 -06:00

378 lines
13 KiB
Rust

use crate::util::MutableCow;
use nu_engine::{get_eval_block_with_early_return, get_full_help, ClosureEvalOnce};
use nu_plugin_protocol::EvaluatedCall;
use nu_protocol::{
engine::{Call, Closure, EngineState, Redirection, Stack},
ir, Config, DeclId, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals,
Span, Spanned, Value,
};
use std::{
borrow::Cow,
collections::HashMap,
sync::{atomic::AtomicU32, Arc},
};
/// Object safe trait for abstracting operations required of the plugin context.
pub trait PluginExecutionContext: Send + Sync {
/// A span pointing to the command being executed
fn span(&self) -> Span;
/// The [`Signals`] struct, if present
fn signals(&self) -> &Signals;
/// The pipeline externals state, for tracking the foreground process group, if present
fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>>;
/// Get engine configuration
fn get_config(&self) -> Result<Arc<Config>, ShellError>;
/// Get plugin configuration
fn get_plugin_config(&self) -> Result<Option<Value>, ShellError>;
/// Get an environment variable from `$env`
fn get_env_var(&self, name: &str) -> Result<Option<&Value>, ShellError>;
/// Get all environment variables
fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError>;
/// Get current working directory
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError>;
/// Set an environment variable
fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError>;
/// Get help for the current command
fn get_help(&self) -> Result<Spanned<String>, ShellError>;
/// Get the contents of a [`Span`]
fn get_span_contents(&self, span: Span) -> Result<Spanned<Vec<u8>>, ShellError>;
/// Evaluate a closure passed to the plugin
fn eval_closure(
&self,
closure: Spanned<Closure>,
positional: Vec<Value>,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError>;
/// Find a declaration by name
fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError>;
/// Call a declaration with arguments and input
fn call_decl(
&mut self,
decl_id: DeclId,
call: EvaluatedCall,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError>;
/// Create an owned version of the context with `'static` lifetime
fn boxed(&self) -> Box<dyn PluginExecutionContext>;
}
/// The execution context of a plugin command. Can be borrowed.
pub struct PluginExecutionCommandContext<'a> {
identity: Arc<PluginIdentity>,
engine_state: Cow<'a, EngineState>,
stack: MutableCow<'a, Stack>,
call: Call<'a>,
}
impl<'a> PluginExecutionCommandContext<'a> {
pub fn new(
identity: Arc<PluginIdentity>,
engine_state: &'a EngineState,
stack: &'a mut Stack,
call: &'a Call<'a>,
) -> PluginExecutionCommandContext<'a> {
PluginExecutionCommandContext {
identity,
engine_state: Cow::Borrowed(engine_state),
stack: MutableCow::Borrowed(stack),
call: call.clone(),
}
}
}
impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
fn span(&self) -> Span {
self.call.head
}
fn signals(&self) -> &Signals {
self.engine_state.signals()
}
fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> {
Some(&self.engine_state.pipeline_externals_state)
}
fn get_config(&self) -> Result<Arc<Config>, ShellError> {
Ok(self.stack.get_config(&self.engine_state))
}
fn get_plugin_config(&self) -> Result<Option<Value>, ShellError> {
// Fetch the configuration for a plugin
//
// The `plugin` must match the registered name of a plugin. For `plugin add
// nu_plugin_example` the plugin config lookup uses `"example"`
Ok(self
.get_config()?
.plugins
.get(self.identity.name())
.cloned()
.map(|value| {
let span = value.span();
match value {
Value::Closure { val, .. } => {
ClosureEvalOnce::new(&self.engine_state, &self.stack, *val)
.run_with_input(PipelineData::Empty)
.and_then(|data| data.into_value(span))
.unwrap_or_else(|err| Value::error(err, self.call.head))
}
_ => value.clone(),
}
}))
}
fn get_env_var(&self, name: &str) -> Result<Option<&Value>, ShellError> {
Ok(self
.stack
.get_env_var_insensitive(&self.engine_state, name)
.map(|(_, value)| value))
}
fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError> {
Ok(self.stack.get_env_vars(&self.engine_state))
}
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> {
#[allow(deprecated)]
let cwd = nu_engine::env::current_dir_str(&self.engine_state, &self.stack)?;
// The span is not really used, so just give it call.head
Ok(cwd.into_spanned(self.call.head))
}
fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError> {
self.stack.add_env_var(name, value);
Ok(())
}
fn get_help(&self) -> Result<Spanned<String>, ShellError> {
let decl = self.engine_state.get_decl(self.call.decl_id);
Ok(
get_full_help(decl, &self.engine_state, &mut self.stack.clone())
.into_spanned(self.call.head),
)
}
fn get_span_contents(&self, span: Span) -> Result<Spanned<Vec<u8>>, ShellError> {
Ok(self
.engine_state
.get_span_contents(span)
.to_vec()
.into_spanned(self.call.head))
}
fn eval_closure(
&self,
closure: Spanned<Closure>,
positional: Vec<Value>,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
let block = self
.engine_state
.try_get_block(closure.item.block_id)
.ok_or_else(|| ShellError::GenericError {
error: "Plugin misbehaving".into(),
msg: format!(
"Tried to evaluate unknown block id: {}",
closure.item.block_id.get()
),
span: Some(closure.span),
help: None,
inner: vec![],
})?;
let mut stack = self
.stack
.captures_to_stack(closure.item.captures)
.reset_pipes();
let stack = &mut stack.push_redirection(
redirect_stdout.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
redirect_stderr.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
);
// Set up the positional arguments
for (idx, value) in positional.into_iter().enumerate() {
if let Some(arg) = block.signature.get_positional(idx) {
if let Some(var_id) = arg.var_id {
stack.add_var(var_id, value);
} else {
return Err(ShellError::NushellFailedSpanned {
msg: "Error while evaluating closure from plugin".into(),
label: "closure argument missing var_id".into(),
span: closure.span,
});
}
}
}
let eval_block_with_early_return = get_eval_block_with_early_return(&self.engine_state);
eval_block_with_early_return(&self.engine_state, stack, block, input)
}
fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError> {
Ok(self.engine_state.find_decl(name.as_bytes(), &[]))
}
fn call_decl(
&mut self,
decl_id: DeclId,
call: EvaluatedCall,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
if decl_id.get() >= self.engine_state.num_decls() {
return Err(ShellError::GenericError {
error: "Plugin misbehaving".into(),
msg: format!("Tried to call unknown decl id: {}", decl_id.get()),
span: Some(call.head),
help: None,
inner: vec![],
});
}
let decl = self.engine_state.get_decl(decl_id);
let stack = &mut self.stack.push_redirection(
redirect_stdout.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
redirect_stderr.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
);
let mut call_builder = ir::Call::build(decl_id, call.head);
for positional in call.positional {
call_builder.add_positional(stack, positional.span(), positional);
}
for (name, value) in call.named {
if let Some(value) = value {
call_builder.add_named(stack, &name.item, "", name.span, value);
} else {
call_builder.add_flag(stack, &name.item, "", name.span);
}
}
call_builder.with(stack, |stack, call| {
decl.run(&self.engine_state, stack, call, input)
})
}
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
Box::new(PluginExecutionCommandContext {
identity: self.identity.clone(),
engine_state: Cow::Owned(self.engine_state.clone().into_owned()),
stack: self.stack.owned(),
call: self.call.to_owned(),
})
}
}
/// A bogus execution context for testing that doesn't really implement anything properly
#[cfg(test)]
pub(crate) struct PluginExecutionBogusContext;
#[cfg(test)]
impl PluginExecutionContext for PluginExecutionBogusContext {
fn span(&self) -> Span {
Span::test_data()
}
fn signals(&self) -> &Signals {
&Signals::EMPTY
}
fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> {
None
}
fn get_config(&self) -> Result<Arc<Config>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_config not implemented on bogus".into(),
})
}
fn get_plugin_config(&self) -> Result<Option<Value>, ShellError> {
Ok(None)
}
fn get_env_var(&self, _name: &str) -> Result<Option<&Value>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_env_var not implemented on bogus".into(),
})
}
fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_env_vars not implemented on bogus".into(),
})
}
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_current_dir not implemented on bogus".into(),
})
}
fn add_env_var(&mut self, _name: String, _value: Value) -> Result<(), ShellError> {
Err(ShellError::NushellFailed {
msg: "add_env_var not implemented on bogus".into(),
})
}
fn get_help(&self) -> Result<Spanned<String>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_help not implemented on bogus".into(),
})
}
fn get_span_contents(&self, _span: Span) -> Result<Spanned<Vec<u8>>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_span_contents not implemented on bogus".into(),
})
}
fn eval_closure(
&self,
_closure: Spanned<Closure>,
_positional: Vec<Value>,
_input: PipelineData,
_redirect_stdout: bool,
_redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
Err(ShellError::NushellFailed {
msg: "eval_closure not implemented on bogus".into(),
})
}
fn find_decl(&self, _name: &str) -> Result<Option<DeclId>, ShellError> {
Err(ShellError::NushellFailed {
msg: "find_decl not implemented on bogus".into(),
})
}
fn call_decl(
&mut self,
_decl_id: DeclId,
_call: EvaluatedCall,
_input: PipelineData,
_redirect_stdout: bool,
_redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
Err(ShellError::NushellFailed {
msg: "call_decl not implemented on bogus".into(),
})
}
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
Box::new(PluginExecutionBogusContext)
}
}