diff --git a/src/alias.rs b/src/alias.rs index 14b1f64..3c444c6 100644 --- a/src/alias.rs +++ b/src/alias.rs @@ -1,5 +1,6 @@ use crate::config::FnmConfig; -use crate::fs::{remove_symlink_dir, symlink_dir}; +use crate::fs::{remove_symlink_dir, shallow_read_symlink, symlink_dir}; +use crate::system_version; use crate::version::Version; use std::convert::TryInto; use std::path::PathBuf; @@ -12,15 +13,10 @@ pub fn create_alias( let aliases_dir = config.aliases_dir(); std::fs::create_dir_all(&aliases_dir)?; - let version_dir = version - .installation_path(config) - .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?; + let version_dir = version.installation_path(config); let alias_dir = aliases_dir.join(common_name); - if alias_dir.exists() { - remove_symlink_dir(&alias_dir)?; - } - + remove_symlink_dir(&alias_dir).ok(); symlink_dir(&version_dir, &alias_dir)?; Ok(()) @@ -44,7 +40,12 @@ impl std::convert::TryInto for &std::path::Path { type Error = std::io::Error; fn try_into(self) -> Result { - let destination_path = std::fs::canonicalize(&self)?; + let shallow_self = shallow_read_symlink(self)?; + let destination_path = if shallow_self == system_version::path() { + shallow_self + } else { + std::fs::canonicalize(&shallow_self)? + }; Ok(StoredAlias { alias_path: PathBuf::from(self), destination_path, @@ -54,13 +55,17 @@ impl std::convert::TryInto for &std::path::Path { impl StoredAlias { pub fn s_ver(&self) -> &str { - self.destination_path - .parent() - .unwrap() - .file_name() - .expect("must have basename") - .to_str() - .unwrap() + if self.destination_path == system_version::path() { + system_version::display_name() + } else { + self.destination_path + .parent() + .unwrap() + .file_name() + .expect("must have basename") + .to_str() + .unwrap() + } } pub fn name(&self) -> &str { diff --git a/src/choose_version_for_user_input.rs b/src/choose_version_for_user_input.rs index f213fdc..18d9d3d 100644 --- a/src/choose_version_for_user_input.rs +++ b/src/choose_version_for_user_input.rs @@ -1,4 +1,5 @@ use crate::config::FnmConfig; +use crate::fs; use crate::installed_versions; use crate::system_version; use crate::user_version::UserVersion; @@ -8,6 +9,7 @@ use log::info; use snafu::{ensure, ResultExt, Snafu}; use std::path::{Path, PathBuf}; +#[derive(Debug)] pub struct ApplicableVersion { path: PathBuf, version: Version, @@ -31,24 +33,40 @@ pub fn choose_version_for_user_input<'a>( installed_versions::list(config.installations_dir()).context(VersionListing)?; let result = if let UserVersion::Full(Version::Bypassed) = requested_version { - info!("Bypassing fnm: using {} node", "system".cyan()); + info!( + "Bypassing fnm: using {} node", + system_version::display_name().cyan() + ); Some(ApplicableVersion { path: system_version::path(), version: Version::Bypassed, }) } else if let Some(alias_name) = requested_version.alias_name() { let alias_path = config.aliases_dir().join(&alias_name); - ensure!( - alias_path.exists(), - CantFindVersion { - requested_version: requested_version.clone() - } - ); - info!("Using Node for alias {}", alias_name.cyan()); - Some(ApplicableVersion { - path: alias_path, - version: Version::Alias(alias_name), - }) + let system_path = system_version::path(); + if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path) + { + info!( + "Bypassing fnm: using {} node", + system_version::display_name().cyan() + ); + Some(ApplicableVersion { + path: alias_path, + version: Version::Bypassed, + }) + } else { + ensure!( + alias_path.exists(), + CantFindVersion { + requested_version: requested_version.clone() + } + ); + info!("Using Node for alias {}", alias_name.cyan()); + Some(ApplicableVersion { + path: alias_path, + version: Version::Alias(alias_name), + }) + } } else { let current_version = requested_version.to_version(&all_versions, config); current_version.map(|version| { diff --git a/src/commands/env.rs b/src/commands/env.rs index a14b15d..4304559 100644 --- a/src/commands/env.rs +++ b/src/commands/env.rs @@ -84,6 +84,9 @@ impl Command for Env { if self.use_on_cd { println!("{}", shell.use_on_cd(config)); } + if let Some(v) = shell.rehash() { + println!("{}", v); + } Ok(()) } } diff --git a/src/commands/unalias.rs b/src/commands/unalias.rs index d7dedd0..4bedabc 100644 --- a/src/commands/unalias.rs +++ b/src/commands/unalias.rs @@ -1,7 +1,9 @@ use super::command::Command; -use crate::config::FnmConfig; use crate::fs::remove_symlink_dir; -use snafu::{ensure, ResultExt, Snafu}; +use crate::user_version::UserVersion; +use crate::version::Version; +use crate::{choose_version_for_user_input, config::FnmConfig}; +use snafu::{OptionExt, ResultExt, Snafu}; use structopt::StructOpt; #[derive(StructOpt, Debug)] @@ -13,15 +15,17 @@ impl Command for Unalias { type Error = Error; fn apply(self, config: &FnmConfig) -> Result<(), Self::Error> { - let alias_path = config.aliases_dir().join(&self.requested_alias); - ensure!( - alias_path.exists(), - AliasNotFound { - requested_alias: self.requested_alias - } - ); + let requested_version = choose_version_for_user_input::choose_version_for_user_input( + &UserVersion::Full(Version::Alias(self.requested_alias.clone())), + config, + ) + .ok() + .flatten() + .with_context(|| AliasNotFound { + requested_alias: self.requested_alias, + })?; - remove_symlink_dir(&alias_path).context(CantDeleteSymlink)?; + remove_symlink_dir(&requested_version.path()).context(CantDeleteSymlink)?; Ok(()) } diff --git a/src/commands/use.rs b/src/commands/use.rs index f8f2049..e69bc76 100644 --- a/src/commands/use.rs +++ b/src/commands/use.rs @@ -38,11 +38,16 @@ impl Command for Use { .context(CantInferVersion)?; let version_path = if let UserVersion::Full(Version::Bypassed) = requested_version { - outln!(config#Info, "Bypassing fnm: using {} node", "system".cyan()); + outln!(config#Info, "Bypassing fnm: using {} node", system_version::display_name().cyan()); system_version::path() } else if let Some(alias_name) = requested_version.alias_name() { let alias_path = config.aliases_dir().join(&alias_name); - if alias_path.exists() { + let system_path = system_version::path(); + if matches!(fs::shallow_read_symlink(&alias_path), Ok(shallow_path) if shallow_path == system_path) + { + outln!(config#Info, "Bypassing fnm: using {} node", system_version::display_name().cyan()); + system_path + } else if alias_path.exists() { outln!(config#Info, "Using Node for alias {}", alias_name.cyan()); alias_path } else { @@ -164,6 +169,8 @@ fn warn_if_multishell_path_not_in_path_env_var( pub enum Error { #[snafu(display("Can't create the symlink: {}", source))] SymlinkingCreationIssue { source: std::io::Error }, + #[snafu(display("Can't read the symlink: {}", source))] + SymlinkReadFailed { source: std::io::Error }, #[snafu(display("{}", source))] InstallError { source: ::Error }, #[snafu(display("Can't get locally installed versions: {}", source))] diff --git a/src/fs.rs b/src/fs.rs index 5108926..b7a4231 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -23,3 +23,7 @@ pub fn remove_symlink_dir>(path: P) -> std::io::Result<()> { std::fs::remove_file(path)?; Ok(()) } + +pub fn shallow_read_symlink>(path: P) -> std::io::Result { + std::fs::read_link(path) +} diff --git a/src/shell/shell.rs b/src/shell/shell.rs index f1071ae..8d48824 100644 --- a/src/shell/shell.rs +++ b/src/shell/shell.rs @@ -5,6 +5,9 @@ pub trait Shell: Debug { fn path(&self, path: &Path) -> String; fn set_env_var(&self, name: &str, value: &str) -> String; fn use_on_cd(&self, config: &crate::config::FnmConfig) -> String; + fn rehash(&self) -> Option { + None + } fn to_structopt_shell(&self) -> structopt::clap::Shell; } diff --git a/src/shell/zsh.rs b/src/shell/zsh.rs index 679be1f..9848048 100644 --- a/src/shell/zsh.rs +++ b/src/shell/zsh.rs @@ -18,6 +18,10 @@ impl Shell for Zsh { format!("export {}={:?}", name, value) } + fn rehash(&self) -> Option { + Some("rehash".to_string()) + } + fn use_on_cd(&self, _config: &crate::config::FnmConfig) -> String { indoc!( r#" diff --git a/src/system_version.rs b/src/system_version.rs index bb74bc0..b3b4578 100644 --- a/src/system_version.rs +++ b/src/system_version.rs @@ -9,3 +9,7 @@ pub fn path() -> PathBuf { PathBuf::from(path_as_string) } + +pub fn display_name() -> &'static str { + "system" +} diff --git a/src/version.rs b/src/version.rs index da86bb0..5883aa3 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,6 +1,7 @@ use crate::alias; use crate::config; use crate::lts::LtsType; +use crate::system_version; use std::str::FromStr; #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] @@ -18,7 +19,7 @@ fn first_letter_is_number(s: &str) -> bool { impl Version { pub fn parse>(version_str: S) -> Result { let lowercased = version_str.as_ref().to_lowercase(); - if lowercased == "system" { + if lowercased == system_version::display_name() { Ok(Self::Bypassed) } else if lowercased.starts_with("lts-") || lowercased.starts_with("lts/") { let lts_type = LtsType::from(&lowercased[4..]); @@ -54,30 +55,24 @@ impl Version { format!("{}", self) } - pub fn installation_path(&self, config: &config::FnmConfig) -> Option { + pub fn installation_path(&self, config: &config::FnmConfig) -> std::path::PathBuf { match self { - Self::Bypassed => None, + Self::Bypassed => system_version::path(), v @ (Self::Lts(_) | Self::Alias(_)) => { - Some(config.aliases_dir().join(v.alias_name().unwrap())) + config.aliases_dir().join(v.alias_name().unwrap()) } - v @ Self::Semver(_) => Some( - config - .installations_dir() - .join(v.v_str()) - .join("installation"), - ), + v @ Self::Semver(_) => config + .installations_dir() + .join(v.v_str()) + .join("installation"), } } pub fn root_path(&self, config: &config::FnmConfig) -> Option { - match self.installation_path(config) { - None => None, - Some(path) => { - let mut canon_path = path.canonicalize().ok()?; - canon_path.pop(); - Some(canon_path) - } - } + let path = self.installation_path(config); + let mut canon_path = path.canonicalize().ok()?; + canon_path.pop(); + Some(canon_path) } } @@ -94,7 +89,7 @@ impl<'de> serde::Deserialize<'de> for Version { impl std::fmt::Display for Version { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Bypassed => write!(f, "system"), + Self::Bypassed => write!(f, "{}", system_version::display_name()), Self::Lts(lts) => write!(f, "lts-{}", lts), Self::Semver(semver) => write!(f, "v{}", semver), Self::Alias(alias) => write!(f, "{}", alias), diff --git a/tests/feature_tests/mod.rs b/tests/feature_tests/mod.rs index daa43ec..f9a2796 100644 --- a/tests/feature_tests/mod.rs +++ b/tests/feature_tests/mod.rs @@ -238,3 +238,17 @@ mod unalias_error { .then(OutputContains::new(IgnoreErrors::new(GetStderr::new(Call::new("fnm", vec!["unalias", "lts"]))), "Requested alias lts not found")) }); } + +mod alias_system { + test_shell!(Bash, Zsh, Fish, PowerShell; { + EvalFnmEnv::default() + .then(Call::new("fnm", vec!["alias", "system", "my_system"])) + .then(OutputContains::new(Call::new("fnm", vec!["ls"]), "my_system")) + .then(Call::new("fnm", vec!["alias", "system", "default"])) + .then(Call::new("fnm", vec!["alias", "my_system", "my_system2"])) + .then(OutputContains::new(Call::new("fnm", vec!["ls"]), "my_system2")) + .then(OutputContains::new(Call::new("fnm", vec!["use", "my_system"]), "Bypassing fnm")) + .then(Call::new("fnm", vec!["unalias", "my_system"])) + .then(OutputContains::new(IgnoreErrors::new(GetStderr::new(Call::new("fnm", vec!["use", "my_system"]))), "Requested version my_system is not currently installed")) + }); +} diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Bash.snap b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Bash.snap new file mode 100644 index 0000000..ccb9d3b --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Bash.snap @@ -0,0 +1,17 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +set -e +shopt -s expand_aliases + +eval "$(fnm env)" +fnm alias system my_system +fnm ls | grep my_system +fnm alias system default +fnm alias my_system my_system2 +fnm ls | grep my_system2 +fnm use my_system | grep 'Bypassing fnm' +fnm unalias my_system +fnm use my_system 2>&1 | grep 'Requested version my_system is not currently installed' diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Fish.snap b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Fish.snap new file mode 100644 index 0000000..fe299c8 --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Fish.snap @@ -0,0 +1,14 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +fnm env | source +fnm alias system my_system +fnm ls | grep my_system +fnm alias system default +fnm alias my_system my_system2 +fnm ls | grep my_system2 +fnm use my_system | grep 'Bypassing fnm' +fnm unalias my_system +fnm use my_system 2>&1 | grep 'Requested version my_system is not currently installed' diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__PowerShell.snap b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__PowerShell.snap new file mode 100644 index 0000000..758c7cb --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__PowerShell.snap @@ -0,0 +1,15 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +$ErrorActionPreference = "Stop" +fnm env | Out-String | Invoke-Expression +fnm alias system my_system +$($__out__ = $(fnm ls | Select-String 'my_system'); echo $__out__; if ($__out__ -eq $null){ exit 1 } else { $__out__ }) +fnm alias system default +fnm alias my_system my_system2 +$($__out__ = $(fnm ls | Select-String 'my_system2'); echo $__out__; if ($__out__ -eq $null){ exit 1 } else { $__out__ }) +$($__out__ = $(fnm use my_system | Select-String 'Bypassing fnm'); echo $__out__; if ($__out__ -eq $null){ exit 1 } else { $__out__ }) +fnm unalias my_system +$($__out__ = $($($_tmp_err_action = $ErrorActionPreference;$ErrorActionPreference = "Continue";fnm use my_system 2>&1;$ErrorActionPreference = $_tmp_err_action) | Select-String 'Requested version my_system is not currently installed'); echo $__out__; if ($__out__ -eq $null){ exit 1 } else { $__out__ }) diff --git a/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Zsh.snap b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Zsh.snap new file mode 100644 index 0000000..2eeccfb --- /dev/null +++ b/tests/feature_tests/snapshots/e2e__feature_tests__alias_system__Zsh.snap @@ -0,0 +1,15 @@ +--- +source: tests/feature_tests/mod.rs +expression: "&source.trim()" + +--- +set -e +eval "$(fnm env)" +fnm alias system my_system +fnm ls | grep my_system +fnm alias system default +fnm alias my_system my_system2 +fnm ls | grep my_system2 +fnm use my_system | grep 'Bypassing fnm' +fnm unalias my_system +fnm use my_system 2>&1 | grep 'Requested version my_system is not currently installed'