diff --git a/e2e/cli/test_hook_env b/e2e/cli/test_hook_env index 8cfdc9229b3..f4b4b3f37d8 100644 --- a/e2e/cli/test_hook_env +++ b/e2e/cli/test_hook_env @@ -16,3 +16,13 @@ assert_contains "dummy" "1.0.0" export MISE_DUMMY_VERSION=1.1.0 pushd .. && popd assert_contains "dummy" "1.1.0" + +ln -s "$ROOT/test/data/plugins/dummy" "$MISE_DATA_DIR/plugins/hyphen-tool" +mise install hyphen-tool@1.0.0 +mise shell hyphen-tool@1.0.0 +assert "echo $MISE_HYPHEN_TOOL_VERSION" "1.0.0" +pushd .. && popd +assert_contains "mise current hyphen-tool" "1.0.0" + +mise shell --unset hyphen-tool +assert "echo ${MISE_HYPHEN_TOOL_VERSION+set}" "" diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index e1e5c0ef491..59d06fb3157 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -215,7 +215,7 @@ impl AsdfBackend { .with_env("MISE_DOWNLOAD_PATH", download) .with_env("MISE_INSTALL_PATH", install) .with_env("MISE_INSTALL_TYPE", install_type) - .with_env("MISE_INSTALL_VERSION", install_version); + .with_env(env::MISE_INSTALL_VERSION_ENV_VAR, install_version); Ok(sm) } } diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 4f52463ac1f..2120ec901ae 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -2382,7 +2382,7 @@ pub trait Backend: Debug + Send + Sync { .env(&*env::PATH_KEY, path_env.join()) .env("MISE_TOOL_INSTALL_PATH", tv.install_path()) .env("MISE_TOOL_NAME", tv.ba().short.clone()) - .env("MISE_TOOL_VERSION", tv.version.clone()) + .env(env::MISE_TOOL_VERSION_ENV_VAR, tv.version.clone()) .with_pr(ctx.pr.as_ref()) .cmd_body_args(shell_args, &rendered_script) .envs(env_vars); diff --git a/src/cli/shell.rs b/src/cli/shell.rs index f3321ee249d..5825a435e6e 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -1,13 +1,12 @@ use color_eyre::eyre::{Result, bail, eyre}; use console::style; -use heck::ToShoutySnakeCase; use indoc::formatdoc; use crate::cli::args::ToolArg; use crate::config::Config; use crate::env; use crate::shell::get_shell; -use crate::toolset::{InstallOptions, ToolSource, ToolsetBuilder}; +use crate::toolset::{InstallOptions, ToolSource, ToolsetBuilder, tool_env_var_name}; /// Sets a tool version for the current session. /// @@ -48,10 +47,7 @@ impl Shell { if self.unset { for ta in &self.tool { - let op = shell.unset_env(&format!( - "MISE_{}_VERSION", - ta.ba.short.to_shouty_snake_case() - )); + let op = shell.unset_env(&tool_env_var_name(&ta.ba.short)); print!("{op}"); } return Ok(()); @@ -82,7 +78,7 @@ impl Shell { for (p, tv) in ts.list_current_installed_versions(&config) { let source = &ts.versions.get(p.ba().as_ref()).unwrap().source; if matches!(source, ToolSource::Argument) { - let k = format!("MISE_{}_VERSION", p.id().to_shouty_snake_case()); + let k = tool_env_var_name(p.id()); let op = shell.set_env(&k, &tv.version); print!("{op}"); } diff --git a/src/env.rs b/src/env.rs index ab2db128683..cc18978cedc 100644 --- a/src/env.rs +++ b/src/env.rs @@ -22,6 +22,10 @@ use std::{path::PathBuf, sync::atomic::AtomicBool}; pub static ARGS: RwLock> = RwLock::new(vec![]); pub static TOOL_ARGS: RwLock> = RwLock::new(vec![]); +pub const MISE_INSTALL_VERSION_ENV_VAR: &str = "MISE_INSTALL_VERSION"; +pub const MISE_TOOL_VERSION_ENV_VAR: &str = "MISE_TOOL_VERSION"; +pub const NON_TOOL_VERSION_ENV_VARS: &[&str] = + &[MISE_INSTALL_VERSION_ENV_VAR, MISE_TOOL_VERSION_ENV_VAR]; #[cfg(unix)] pub static SHELL: Lazy = Lazy::new(|| var("SHELL").unwrap_or_else(|_| "sh".into())); #[cfg(windows)] diff --git a/src/toolset/builder.rs b/src/toolset/builder.rs index c1b1ee3790d..4e8c61e8c30 100644 --- a/src/toolset/builder.rs +++ b/src/toolset/builder.rs @@ -7,7 +7,7 @@ use crate::cli::args::{BackendArg, ToolArg}; use crate::config::{Config, ConfigMap}; use crate::env_diff::EnvMap; use crate::errors::Error; -use crate::toolset::{ResolveOptions, ToolRequest, ToolSource, Toolset}; +use crate::toolset::{ResolveOptions, ToolRequest, ToolSource, Toolset, tool_from_env_var_name}; use crate::{config, env}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -111,15 +111,7 @@ impl ToolsetBuilder { return Ok(()); } for (k, v) in env { - if k.starts_with("MISE_") && k.ends_with("_VERSION") && k != "MISE_VERSION" { - let tool_name = k - .trim_start_matches("MISE_") - .trim_end_matches("_VERSION") - .to_lowercase(); - if tool_name == "install" || tool_name == "tool" { - // ignore MISE_INSTALL_VERSION and MISE_TOOL_VERSION (set during hooks) - continue; - } + if let Some(tool_name) = tool_from_env_var_name(&k) { let ba: Arc = Arc::new(tool_name.as_str().into()); let source = ToolSource::Environment(k, v.clone()); let mut env_ts = Toolset::new(source.clone()); diff --git a/src/toolset/mod.rs b/src/toolset/mod.rs index ab1ade77f83..577b42da342 100644 --- a/src/toolset/mod.rs +++ b/src/toolset/mod.rs @@ -33,7 +33,9 @@ use tokio::sync::OnceCell; pub use install_options::InstallOptions; pub use tool_request::ToolRequest; -pub use tool_request_set::{ToolRequestSet, ToolRequestSetBuilder, tool_env_vars}; +pub use tool_request_set::{ + ToolRequestSet, ToolRequestSetBuilder, tool_env_var_name, tool_env_vars, tool_from_env_var_name, +}; pub use tool_source::ToolSource; pub use tool_version::{ResolveOptions, ToolVersion}; pub use tool_version_list::ToolVersionList; diff --git a/src/toolset/tool_request_set.rs b/src/toolset/tool_request_set.rs index f7fdf0f45ba..a2a4405fd9b 100644 --- a/src/toolset/tool_request_set.rs +++ b/src/toolset/tool_request_set.rs @@ -10,6 +10,7 @@ use crate::config::{Config, ConfigMap, Settings}; use crate::env; use crate::registry::{REGISTRY, tool_enabled}; use crate::toolset::{ToolRequest, ToolSource, Toolset}; +use heck::{ToKebabCase, ToShoutySnakeCase}; use indexmap::IndexMap; use itertools::Itertools; @@ -300,23 +301,33 @@ fn merge(mut a: ToolRequestSet, mut b: ToolRequestSet) -> ToolRequestSet { b } +/// Returns the environment variable used to select a tool version for a shell session. +pub fn tool_env_var_name(tool: &str) -> String { + format!("MISE_{}_VERSION", tool.to_shouty_snake_case()) +} + +/// Returns the tool selected by a shell-session environment variable. +/// +/// Shell environment variable names cannot preserve the distinction between `-` and `_`. +/// Since mise tool names conventionally use kebab-case, both spellings decode to `-`. +pub fn tool_from_env_var_name(name: &str) -> Option { + if env::NON_TOOL_VERSION_ENV_VARS.contains(&name) { + return None; + } + let raw = name.strip_prefix("MISE_")?.strip_suffix("_VERSION")?; + if raw.is_empty() { + return None; + } + Some(crate::backend::unalias_backend(&raw.to_kebab_case()).to_string()) +} + /// Yields `(short, key, value)` for each `MISE__VERSION` env var that /// maps to a tool. `short` is the unaliased backend short name (so /// `MISE_NODEJS_VERSION` yields `"node"`). Skips `MISE_VERSION` and the /// `MISE_INSTALL_VERSION` / `MISE_TOOL_VERSION` vars set during hooks. pub fn tool_env_vars() -> impl Iterator { env::vars_safe().filter_map(|(k, v)| { - if !k.starts_with("MISE_") || !k.ends_with("_VERSION") || k == "MISE_VERSION" { - return None; - } - let raw = k - .trim_start_matches("MISE_") - .trim_end_matches("_VERSION") - .to_lowercase(); - if raw == "install" || raw == "tool" { - return None; - } - let short = crate::backend::unalias_backend(&raw).to_string(); + let short = tool_from_env_var_name(&k)?; Some((short, k, v)) }) } @@ -326,6 +337,20 @@ mod tests { use super::*; use crate::toolset::{CoreToolOptions, ToolVersionOptions}; + #[test] + fn test_tool_env_var_name_round_trip() { + assert_eq!(tool_env_var_name("ls-lint"), "MISE_LS_LINT_VERSION"); + assert_eq!( + tool_from_env_var_name("MISE_LS_LINT_VERSION").as_deref(), + Some("ls-lint") + ); + assert_eq!(tool_env_var_name("ls-lint"), tool_env_var_name("ls_lint")); + assert_eq!( + tool_from_env_var_name("MISE_NODEJS_VERSION").as_deref(), + Some("node") + ); + } + #[test] fn test_load_runtime_env_with_valid_utf8() { // This test verifies that valid UTF-8 MISE_*_VERSION variables work correctly @@ -384,19 +409,19 @@ mod tests { fn test_tool_env_vars_skips_non_tool_vars() { unsafe { std::env::set_var("MISE_VERSION", "2026.4.28"); - std::env::set_var("MISE_INSTALL_VERSION", "1.0.0"); - std::env::set_var("MISE_TOOL_VERSION", "1.0.0"); + std::env::set_var(env::MISE_INSTALL_VERSION_ENV_VAR, "1.0.0"); + std::env::set_var(env::MISE_TOOL_VERSION_ENV_VAR, "1.0.0"); } let keys: HashSet = tool_env_vars().map(|(_, k, _)| k).collect(); assert!(!keys.contains("MISE_VERSION")); - assert!(!keys.contains("MISE_INSTALL_VERSION")); - assert!(!keys.contains("MISE_TOOL_VERSION")); + assert!(!keys.contains(env::MISE_INSTALL_VERSION_ENV_VAR)); + assert!(!keys.contains(env::MISE_TOOL_VERSION_ENV_VAR)); unsafe { std::env::remove_var("MISE_VERSION"); - std::env::remove_var("MISE_INSTALL_VERSION"); - std::env::remove_var("MISE_TOOL_VERSION"); + std::env::remove_var(env::MISE_INSTALL_VERSION_ENV_VAR); + std::env::remove_var(env::MISE_TOOL_VERSION_ENV_VAR); } }