use std::path::{Path, PathBuf};
use crate::config::Settings;
use crate::env::PATH_KEY;
use crate::file::{canonicalize_cached, canonicalize_or_self, touch_dir};
use crate::path_env::PathEnv;
use crate::shell::{
ActivateOptions, ActivatePrelude, EXAMPLE_SHELL, Shell, ShellType, require_shell,
};
use crate::toolset::env_cache::CachedEnv;
use crate::{dirs, env};
use eyre::Result;
use itertools::Itertools;
#[derive(Debug, usage_rs::Args)]
#[usage(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub(crate) struct Activate {
#[usage(value_enum)]
shell_type: Option<ShellType>,
#[usage(long, short)]
quiet: bool,
#[usage(long, short, hide = true, value_enum)]
shell: Option<ShellType>,
#[usage(long)]
no_hook_env: bool,
#[usage(long, verbatim_doc_comment)]
shims: bool,
#[usage(long, hide = true)]
status: bool,
}
impl Activate {
pub(crate) fn run(self) -> Result<()> {
let shell = require_shell(
self.shell_type.or(self.shell),
&format!("Name the shell: `mise activate {EXAMPLE_SHELL}`."),
)?;
let _ = touch_dir(&dirs::DATA);
let mise_bin = if cfg!(target_os = "linux") {
let argv0 = PathBuf::from(&*env::ARGV0);
let path = if argv0.is_absolute() {
argv0
} else {
which::which(&*env::ARGV0).unwrap_or_else(|_| env::MISE_BIN.clone())
};
if path.is_absolute() {
path
} else {
std::env::current_dir()
.map(|cwd| cwd.join(path))
.unwrap_or_else(|_| env::MISE_BIN.clone())
}
} else {
env::MISE_BIN.clone()
};
match self.shims {
true => self.activate_shims(shell.as_ref(), &mise_bin)?,
false => self.activate(shell.as_ref(), &mise_bin)?,
}
Ok(())
}
fn activate_shims(&self, shell: &dyn Shell, mise_bin: &Path) -> std::io::Result<()> {
let exe_dir = mise_bin.parent().unwrap();
let mut prelude = vec![];
let prepended_exe_dir = if let Some(p) = self.prepend_path(exe_dir) {
prelude.push(p);
true
} else {
false
};
let has_command_wrappers = dirs::COMMAND_WRAPPERS.is_dir();
let dispatch_dirs_already_first = has_command_wrappers
&& are_dirs_first_in_paths(&env::PATH, &[&dirs::COMMAND_WRAPPERS, &dirs::SHIMS]);
if shell.supports_move_path() || prepended_exe_dir || !dispatch_dirs_already_first {
if let Some(p) = self.shims_prepend_path(shell, &dirs::SHIMS, prepended_exe_dir) {
prelude.push(p);
}
if has_command_wrappers
&& let Some(p) = self.shims_prepend_path(shell, &dirs::COMMAND_WRAPPERS, true)
{
prelude.push(p);
}
}
miseprint!("{}", shell.format_activate_prelude(&prelude))?;
Ok(())
}
fn activate(&self, shell: &dyn Shell, mise_bin: &Path) -> std::io::Result<()> {
let mut prelude = vec![];
if let Some(set_path) = remove_shims()? {
prelude.push(set_path);
}
let exe_dir = mise_bin.parent().unwrap();
let mut flags = vec![];
if self.quiet {
flags.push(" --quiet".to_string());
}
if self.status {
flags.push(" --status".to_string());
}
flags.extend(forwarded_logging_flags(&env::ARGS.read().unwrap()));
if let Some(prepend_path) = self.prepend_path(exe_dir) {
prelude.push(prepend_path);
}
if Settings::get().env_cache {
let key = CachedEnv::ensure_encryption_key();
prelude.push(ActivatePrelude::Set(
"__MISE_ENV_CACHE_KEY".to_string(),
key,
));
}
miseprint!(
"{}",
shell.activate(ActivateOptions {
exe: mise_bin.to_path_buf(),
flags: flags.join(""),
no_hook_env: self.no_hook_env,
prelude,
})
)?;
Ok(())
}
fn prepend_path(&self, p: &Path) -> Option<ActivatePrelude> {
if is_dir_not_in_nix(p) && !is_dir_in_path(p) && !p.is_relative() {
Some(ActivatePrelude::Prepend(
PATH_KEY.to_string(),
p.to_string_lossy().to_string(),
))
} else {
None
}
}
fn shims_prepend_path(
&self,
shell: &dyn Shell,
p: &Path,
path_changed_before: bool,
) -> Option<ActivatePrelude> {
if !is_dir_not_in_nix(p) || p.is_relative() {
return None;
}
if shell.supports_move_path() {
Some(ActivatePrelude::MovePrepend(
PATH_KEY.to_string(),
p.to_string_lossy().to_string(),
))
} else if should_prepend_shims(&env::PATH, p, path_changed_before) {
Some(ActivatePrelude::Prepend(
PATH_KEY.to_string(),
p.to_string_lossy().to_string(),
))
} else {
None
}
}
}
fn forwarded_logging_flags(args: &[String]) -> Vec<String> {
let mut flags = vec![];
let mut remaining = args.iter();
while let Some(arg) = remaining.next() {
if arg == "--silent" {
flags.push(" --silent".to_string());
} else if let Some(level) = arg.strip_prefix("--log-level=") {
flags.push(format!(" --log-level={level}"));
} else if arg == "--log-level"
&& let Some(level) = remaining.next()
{
flags.push(format!(" --log-level={level}"));
}
}
flags
}
fn remove_shims() -> std::io::Result<Option<ActivatePrelude>> {
if Settings::get().not_found_auto_install {
return Ok(None);
}
let shims = canonicalize_or_self(&dirs::SHIMS);
if env::PATH
.iter()
.filter_map(|p| canonicalize_cached(p))
.contains(&shims)
{
let path_env = PathEnv::from_iter(env::PATH.clone());
let path = path_env.join_verbatim().to_string_lossy().to_string();
Ok(Some(ActivatePrelude::Set(PATH_KEY.to_string(), path)))
} else {
Ok(None)
}
}
fn is_dir_in_path(dir: &Path) -> bool {
let dir = canonicalize_or_self(dir);
env::PATH
.clone()
.into_iter()
.any(|p| canonicalize_or_self(&p) == dir)
}
fn should_prepend_shims(paths: &[PathBuf], dir: &Path, path_changed_before: bool) -> bool {
path_changed_before || !is_dir_first_in_paths(paths, dir)
}
fn is_dir_first_in_paths(paths: &[PathBuf], dir: &Path) -> bool {
let dir = canonicalize_or_self(dir);
paths
.first()
.is_some_and(|p| canonicalize_or_self(p) == dir)
}
fn are_dirs_first_in_paths(paths: &[PathBuf], dirs: &[&Path]) -> bool {
paths.len() >= dirs.len()
&& paths
.iter()
.zip(dirs)
.all(|(path, dir)| canonicalize_or_self(path) == canonicalize_or_self(dir))
}
fn is_dir_not_in_nix(dir: &Path) -> bool {
!canonicalize_or_self(dir).starts_with("/nix/")
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
$ <bold>eval "$(mise activate bash)"</bold>
$ <bold>eval "$(mise activate zsh)"</bold>
$ <bold>mise activate fish | source</bold>
$ <bold>execx($(mise activate xonsh))</bold>
$ <bold>(&mise activate pwsh) | Out-String | Invoke-Expression</bold>
"#
);
#[cfg(test)]
mod tests {
use super::{
are_dirs_first_in_paths, forwarded_logging_flags, is_dir_first_in_paths,
should_prepend_shims,
};
use std::path::PathBuf;
fn args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
#[test]
fn forwards_silent_so_it_reaches_hook_env() {
assert_eq!(
forwarded_logging_flags(&args(&["mise", "activate", "bash", "--silent"])),
vec![" --silent".to_string()]
);
}
#[test]
fn forwards_a_flag_given_before_the_subcommand_too() {
assert_eq!(
forwarded_logging_flags(&args(&["mise", "--silent", "activate", "bash"])),
vec![" --silent".to_string()]
);
}
#[test]
fn normalizes_both_log_level_spellings_to_one_word() {
let separate =
forwarded_logging_flags(&args(&["mise", "activate", "bash", "--log-level", "error"]));
let joined =
forwarded_logging_flags(&args(&["mise", "activate", "bash", "--log-level=error"]));
assert_eq!(separate, vec![" --log-level=error".to_string()]);
assert_eq!(separate, joined);
}
#[test]
fn keeps_the_order_so_hook_env_resolves_overrides_the_same_way() {
assert_eq!(
forwarded_logging_flags(&args(&[
"mise",
"activate",
"bash",
"--silent",
"--log-level=error"
])),
vec![" --silent".to_string(), " --log-level=error".to_string()]
);
}
#[test]
fn leaves_quiet_to_the_activate_flag_and_skips_verbosity() {
for arg in ["-q", "--quiet", "-v", "--debug", "--trace"] {
assert!(
forwarded_logging_flags(&args(&["mise", "activate", "bash", arg])).is_empty(),
"{arg} should not be forwarded"
);
}
}
#[test]
fn a_trailing_log_level_without_a_value_is_dropped() {
assert!(
forwarded_logging_flags(&args(&["mise", "activate", "bash", "--log-level"])).is_empty()
);
}
#[test]
fn nothing_is_forwarded_without_a_logging_flag() {
assert!(forwarded_logging_flags(&args(&["mise", "activate", "bash"])).is_empty());
}
#[test]
fn detects_only_a_matching_first_path_entry() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().to_path_buf();
let equivalent = target.join(".");
let other = PathBuf::from("/other");
assert!(!is_dir_first_in_paths(&[], &target));
assert!(is_dir_first_in_paths(&[equivalent, other.clone()], &target));
assert!(!is_dir_first_in_paths(&[other, target.clone()], &target));
assert!(!should_prepend_shims(
std::slice::from_ref(&target),
&target,
false
));
assert!(should_prepend_shims(
std::slice::from_ref(&target),
&target,
true
));
}
#[test]
fn detects_an_existing_dispatch_prefix() {
let root = tempfile::tempdir().unwrap();
let wrappers = root.path().join("wrappers");
let shims = root.path().join("shims");
let other = root.path().join("other");
assert!(are_dirs_first_in_paths(
&[wrappers.clone(), shims.clone(), other.clone()],
&[&wrappers, &shims]
));
assert!(!are_dirs_first_in_paths(
&[shims.clone(), wrappers.clone(), other],
&[&wrappers, &shims]
));
assert!(!are_dirs_first_in_paths(
std::slice::from_ref(&wrappers),
&[&wrappers, &shims]
));
}
}