mirror of
https://github.com/nushell/nushell.git
synced 2025-03-28 16:46:18 +01:00
Continuation of #8229 and #8326 # Description The `ShellError` enum at the moment is kind of messy. Many variants are basic tuple structs where you always have to reference the implementation with its macro invocation to know which field serves which purpose. Furthermore we have both variants that are kind of redundant or either overly broad to be useful for the user to match on or overly specific with few uses. So I set out to start fixing the lacking documentation and naming to make it feasible to critically review the individual usages and fix those. Furthermore we can decide to join or split up variants that don't seem to be fit for purpose. # Call to action **Everyone:** Feel free to add review comments if you spot inconsistent use of `ShellError` variants. # User-Facing Changes (None now, end goal more explicit and consistent error messages) # Tests + Formatting (No additional tests needed so far) # Commits (so far) - Remove `ShellError::FeatureNotEnabled` - Name fields on `SE::ExternalNotSupported` - Name field on `SE::InvalidProbability` - Name fields on `SE::NushellFailed` variants - Remove unused `SE::NushellFailedSpannedHelp` - Name field on `SE::VariableNotFoundAtRuntime` - Name fields on `SE::EnvVarNotFoundAtRuntime` - Name fields on `SE::ModuleNotFoundAtRuntime` - Remove usused `ModuleOrOverlayNotFoundAtRuntime` - Name fields on `SE::OverlayNotFoundAtRuntime` - Name field on `SE::NotFound`
426 lines
15 KiB
Rust
426 lines
15 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use nu_protocol::ast::PathMember;
|
|
use nu_protocol::engine::{EngineState, Stack};
|
|
use nu_protocol::{PipelineData, ShellError, Span, Value};
|
|
|
|
use nu_path::canonicalize_with;
|
|
|
|
use crate::eval_block;
|
|
|
|
#[cfg(windows)]
|
|
const ENV_PATH_NAME: &str = "Path";
|
|
#[cfg(windows)]
|
|
const ENV_PATH_NAME_SECONDARY: &str = "PATH";
|
|
#[cfg(not(windows))]
|
|
const ENV_PATH_NAME: &str = "PATH";
|
|
|
|
const ENV_CONVERSIONS: &str = "ENV_CONVERSIONS";
|
|
|
|
static LIB_DIRS_ENV: &str = "NU_LIB_DIRS";
|
|
|
|
enum ConversionResult {
|
|
Ok(Value),
|
|
ConversionError(ShellError), // Failure during the conversion itself
|
|
GeneralError(ShellError), // Other error not directly connected to running the conversion
|
|
CellPathError, // Error looking up the ENV_VAR.to_/from_string fields in $env.ENV_CONVERSIONS
|
|
}
|
|
|
|
/// Translate environment variables from Strings to Values. Requires config to be already set up in
|
|
/// case the user defined custom env conversions in config.nu.
|
|
///
|
|
/// 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) -> Option<ShellError> {
|
|
let mut error = None;
|
|
|
|
let mut new_scope = HashMap::new();
|
|
|
|
let env_vars = engine_state.render_env_vars();
|
|
|
|
for (name, val) in env_vars {
|
|
match get_converted_value(engine_state, stack, name, val, "from_string") {
|
|
ConversionResult::Ok(v) => {
|
|
let _ = new_scope.insert(name.to_string(), v);
|
|
}
|
|
ConversionResult::ConversionError(e) => error = error.or(Some(e)),
|
|
ConversionResult::GeneralError(_) => continue,
|
|
ConversionResult::CellPathError => {
|
|
let _ = new_scope.insert(name.to_string(), val.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
error = error.or_else(|| ensure_path(&mut new_scope, ENV_PATH_NAME));
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
let first_result = ensure_path(&mut new_scope, ENV_PATH_NAME);
|
|
if first_result.is_some() {
|
|
let second_result = ensure_path(&mut new_scope, ENV_PATH_NAME_SECONDARY);
|
|
|
|
if second_result.is_some() {
|
|
error = error.or(first_result);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Ok(last_overlay_name) = &stack.last_overlay_name() {
|
|
if let Some(env_vars) = engine_state.env_vars.get_mut(last_overlay_name) {
|
|
for (k, v) in new_scope {
|
|
env_vars.insert(k, v);
|
|
}
|
|
} else {
|
|
error = error.or_else(|| {
|
|
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() })
|
|
});
|
|
}
|
|
} else {
|
|
error = error.or_else(|| {
|
|
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() })
|
|
});
|
|
}
|
|
|
|
error
|
|
}
|
|
|
|
/// Translate one environment variable from Value to String
|
|
///
|
|
/// Returns Ok(None) if the env var is not
|
|
pub fn env_to_string(
|
|
env_name: &str,
|
|
value: &Value,
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
) -> Result<String, ShellError> {
|
|
match get_converted_value(engine_state, stack, env_name, value, "to_string") {
|
|
ConversionResult::Ok(v) => Ok(v.as_string()?),
|
|
ConversionResult::ConversionError(e) => Err(e),
|
|
ConversionResult::GeneralError(e) => Err(e),
|
|
ConversionResult::CellPathError => match value.as_string() {
|
|
Ok(s) => Ok(s),
|
|
Err(_) => {
|
|
if env_name == ENV_PATH_NAME {
|
|
// Try to convert PATH/Path list to a string
|
|
match value {
|
|
Value::List { vals, .. } => {
|
|
let paths = vals
|
|
.iter()
|
|
.map(|v| v.as_string())
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
match std::env::join_paths(paths) {
|
|
Ok(p) => Ok(p.to_string_lossy().to_string()),
|
|
Err(_) => Err(ShellError::EnvVarNotAString {
|
|
envvar_name: env_name.to_string(),
|
|
span: value.span()?,
|
|
}),
|
|
}
|
|
}
|
|
_ => Err(ShellError::EnvVarNotAString {
|
|
envvar_name: env_name.to_string(),
|
|
span: value.span()?,
|
|
}),
|
|
}
|
|
} else {
|
|
Err(ShellError::EnvVarNotAString {
|
|
envvar_name: env_name.to_string(),
|
|
span: value.span()?,
|
|
})
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Translate all environment variables from Values to Strings
|
|
pub fn env_to_strings(
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
) -> Result<HashMap<String, String>, ShellError> {
|
|
let env_vars = stack.get_env_vars(engine_state);
|
|
let mut env_vars_str = HashMap::new();
|
|
for (env_name, val) in env_vars {
|
|
match env_to_string(&env_name, &val, engine_state, stack) {
|
|
Ok(val_str) => {
|
|
env_vars_str.insert(env_name, val_str);
|
|
}
|
|
Err(ShellError::EnvVarNotAString { .. }) => {} // ignore non-string values
|
|
Err(e) => return Err(e),
|
|
}
|
|
}
|
|
|
|
Ok(env_vars_str)
|
|
}
|
|
|
|
/// Shorthand for env_to_string() for PWD with custom error
|
|
pub fn current_dir_str(engine_state: &EngineState, stack: &Stack) -> Result<String, ShellError> {
|
|
if let Some(pwd) = stack.get_env_var(engine_state, "PWD") {
|
|
match env_to_string("PWD", &pwd, engine_state, stack) {
|
|
Ok(cwd) => {
|
|
if Path::new(&cwd).is_absolute() {
|
|
Ok(cwd)
|
|
} else {
|
|
Err(ShellError::GenericError(
|
|
"Invalid current directory".to_string(),
|
|
format!("The 'PWD' environment variable must be set to an absolute path. Found: '{cwd}'"),
|
|
Some(pwd.span()?),
|
|
None,
|
|
Vec::new()
|
|
))
|
|
}
|
|
}
|
|
Err(e) => Err(e),
|
|
}
|
|
} else {
|
|
Err(ShellError::GenericError(
|
|
"Current directory not found".to_string(),
|
|
"".to_string(),
|
|
None,
|
|
Some("The environment variable 'PWD' was not found. It is required to define the current directory.".to_string()),
|
|
Vec::new(),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Calls current_dir_str() and returns the current directory as a PathBuf
|
|
pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result<PathBuf, ShellError> {
|
|
current_dir_str(engine_state, stack).map(PathBuf::from)
|
|
}
|
|
|
|
/// Get the contents of path environment variable as a list of strings
|
|
///
|
|
/// On non-Windows: It will fetch PATH
|
|
/// On Windows: It will try to fetch Path first but if not present, try PATH
|
|
pub fn path_str(
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
span: Span,
|
|
) -> Result<String, ShellError> {
|
|
let (pathname, pathval) = match stack.get_env_var(engine_state, ENV_PATH_NAME) {
|
|
Some(v) => Ok((ENV_PATH_NAME, v)),
|
|
None => {
|
|
#[cfg(windows)]
|
|
match stack.get_env_var(engine_state, ENV_PATH_NAME_SECONDARY) {
|
|
Some(v) => Ok((ENV_PATH_NAME_SECONDARY, v)),
|
|
None => Err(ShellError::EnvVarNotFoundAtRuntime {
|
|
envvar_name: ENV_PATH_NAME_SECONDARY.to_string(),
|
|
span,
|
|
}),
|
|
}
|
|
#[cfg(not(windows))]
|
|
Err(ShellError::EnvVarNotFoundAtRuntime {
|
|
envvar_name: ENV_PATH_NAME.to_string(),
|
|
span,
|
|
})
|
|
}
|
|
}?;
|
|
|
|
env_to_string(pathname, &pathval, engine_state, stack)
|
|
}
|
|
|
|
/// This helper function is used to find files during eval
|
|
///
|
|
/// First, the actual current working directory is selected as
|
|
/// a) the directory of a file currently being parsed
|
|
/// b) current working directory (PWD)
|
|
///
|
|
/// Then, if the file is not found in the actual cwd, NU_LIB_DIRS is checked.
|
|
/// If there is a relative path in NU_LIB_DIRS, it is assumed to be relative to the actual cwd
|
|
/// determined in the first step.
|
|
///
|
|
/// Always returns an absolute path
|
|
pub fn find_in_dirs_env(
|
|
filename: &str,
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
) -> Result<Option<PathBuf>, ShellError> {
|
|
// Choose whether to use file-relative or PWD-relative path
|
|
let cwd = if let Some(pwd) = stack.get_env_var(engine_state, "FILE_PWD") {
|
|
match env_to_string("FILE_PWD", &pwd, engine_state, stack) {
|
|
Ok(cwd) => {
|
|
if Path::new(&cwd).is_absolute() {
|
|
cwd
|
|
} else {
|
|
return Err(ShellError::GenericError(
|
|
"Invalid current directory".to_string(),
|
|
format!("The 'FILE_PWD' environment variable must be set to an absolute path. Found: '{cwd}'"),
|
|
Some(pwd.span()?),
|
|
None,
|
|
Vec::new()
|
|
));
|
|
}
|
|
}
|
|
Err(e) => return Err(e),
|
|
}
|
|
} else {
|
|
current_dir_str(engine_state, stack)?
|
|
};
|
|
|
|
if let Ok(p) = canonicalize_with(filename, &cwd) {
|
|
Ok(Some(p))
|
|
} else {
|
|
let path = Path::new(filename);
|
|
|
|
if path.is_relative() {
|
|
if let Some(lib_dirs) = stack.get_env_var(engine_state, LIB_DIRS_ENV) {
|
|
if let Ok(dirs) = lib_dirs.as_list() {
|
|
for lib_dir in dirs {
|
|
if let Ok(dir) = lib_dir.as_path() {
|
|
// make sure the dir is absolute path
|
|
if let Ok(dir_abs) = canonicalize_with(dir, &cwd) {
|
|
if let Ok(path) = canonicalize_with(filename, dir_abs) {
|
|
return Ok(Some(path));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_converted_value(
|
|
engine_state: &EngineState,
|
|
stack: &Stack,
|
|
name: &str,
|
|
orig_val: &Value,
|
|
direction: &str,
|
|
) -> ConversionResult {
|
|
if let Some(env_conversions) = stack.get_env_var(engine_state, ENV_CONVERSIONS) {
|
|
let env_span = match env_conversions.span() {
|
|
Ok(span) => span,
|
|
Err(e) => {
|
|
return ConversionResult::GeneralError(e);
|
|
}
|
|
};
|
|
let val_span = match orig_val.span() {
|
|
Ok(span) => span,
|
|
Err(e) => {
|
|
return ConversionResult::GeneralError(e);
|
|
}
|
|
};
|
|
|
|
let path_members = &[
|
|
PathMember::String {
|
|
val: name.to_string(),
|
|
span: env_span,
|
|
},
|
|
PathMember::String {
|
|
val: direction.to_string(),
|
|
span: env_span,
|
|
},
|
|
];
|
|
|
|
if let Ok(Value::Closure {
|
|
val: block_id,
|
|
span: from_span,
|
|
..
|
|
}) = env_conversions.follow_cell_path_not_from_user_input(path_members, false)
|
|
{
|
|
let block = engine_state.get_block(block_id);
|
|
|
|
if let Some(var) = block.signature.get_positional(0) {
|
|
let mut stack = stack.gather_captures(&block.captures);
|
|
if let Some(var_id) = &var.var_id {
|
|
stack.add_var(*var_id, orig_val.clone());
|
|
}
|
|
|
|
let result = eval_block(
|
|
engine_state,
|
|
&mut stack,
|
|
block,
|
|
PipelineData::new_with_metadata(None, val_span),
|
|
true,
|
|
true,
|
|
);
|
|
|
|
match result {
|
|
Ok(data) => ConversionResult::Ok(data.into_value(val_span)),
|
|
Err(e) => ConversionResult::ConversionError(e),
|
|
}
|
|
} else {
|
|
ConversionResult::ConversionError(ShellError::MissingParameter {
|
|
param_name: "block input".into(),
|
|
span: from_span,
|
|
})
|
|
}
|
|
} else {
|
|
ConversionResult::CellPathError
|
|
}
|
|
} else {
|
|
ConversionResult::CellPathError
|
|
}
|
|
}
|
|
|
|
fn ensure_path(scope: &mut HashMap<String, Value>, env_path_name: &str) -> Option<ShellError> {
|
|
let mut error = None;
|
|
|
|
// If PATH/Path is still a string, force-convert it to a list
|
|
match scope.get(env_path_name) {
|
|
Some(Value::String { val, span }) => {
|
|
// Force-split path into a list
|
|
let span = *span;
|
|
let paths = std::env::split_paths(val)
|
|
.map(|p| Value::String {
|
|
val: p.to_string_lossy().to_string(),
|
|
span,
|
|
})
|
|
.collect();
|
|
|
|
scope.insert(env_path_name.to_string(), Value::List { vals: paths, span });
|
|
}
|
|
Some(Value::List { vals, span }) => {
|
|
// Must be a list of strings
|
|
if !vals.iter().all(|v| matches!(v, Value::String { .. })) {
|
|
error = error.or_else(|| {
|
|
Some(ShellError::GenericError(
|
|
format!("Wrong {env_path_name} environment variable value"),
|
|
format!("{env_path_name} must be a list of strings"),
|
|
Some(*span),
|
|
None,
|
|
Vec::new(),
|
|
))
|
|
});
|
|
}
|
|
}
|
|
Some(val) => {
|
|
// All other values are errors
|
|
let span = match val.span() {
|
|
Ok(sp) => sp,
|
|
Err(e) => {
|
|
error = error.or(Some(e));
|
|
Span::unknown() // FIXME: any better span to use here?
|
|
}
|
|
};
|
|
|
|
error = error.or_else(|| {
|
|
Some(ShellError::GenericError(
|
|
format!("Wrong {env_path_name} environment variable value"),
|
|
format!("{env_path_name} must be a list of strings"),
|
|
Some(span),
|
|
None,
|
|
Vec::new(),
|
|
))
|
|
});
|
|
}
|
|
None => { /* not preset, do nothing */ }
|
|
}
|
|
|
|
error
|
|
}
|