Skip to content

Commit 53ae721

Browse files
nekomoyifengmk2
andauthored
refactor(shell): enhance shell detection and add VP_SHELL override (#1658)
## What changed - Replaced `VP_SHELL_NU` and `VP_SHELL_PWSH` with one env var: `VP_SHELL` - Added shell parsing that accepts `bash`, `zsh`, `fish`, `nu`, `pwsh`, and `cmd` ## Why The old approach depended on a few shell-specific flags and inherited env vars. It could mis-detect nested shells and is harder to maintain. The new shell detection stages: 1. `VP_SHELL` if it is set 2. Fallback to posix or cmd --------- Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
1 parent 5f78738 commit 53ae721

8 files changed

Lines changed: 175 additions & 129 deletions

File tree

crates/vite_global_cli/src/commands/env/setup.rs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ unset __vp_bin
542542
vp() {
543543
if [ "$1" = "env" ] && [ "$2" = "use" ]; then
544544
case " $* " in *" -h "*|*" --help "*) command vp "$@"; return; esac
545-
__vp_out="$(VP_ENV_USE_EVAL_ENABLE=1 command vp "$@")" || return $?
545+
__vp_out="$(VP_ENV_USE_EVAL_ENABLE=1 VP_SHELL=sh command vp "$@")" || return $?
546546
eval "$__vp_out"
547547
else
548548
command vp "$@"
@@ -579,7 +579,8 @@ function vp
579579
command vp $argv; return
580580
end
581581
set -lx VP_ENV_USE_EVAL_ENABLE 1
582-
set -l __vp_out (env FISH_VERSION=$FISH_VERSION __VP_BIN__/vp $argv); or return $status
582+
set -lx VP_SHELL fish
583+
set -l __vp_out (command vp $argv); or return $status
583584
eval (string join ';' $__vp_out)
584585
else
585586
command vp $argv
@@ -611,7 +612,7 @@ def --env --wrapped vp [...args: string@"nu-complete vp"] {
611612
^vp ...$args
612613
return
613614
}
614-
let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", VP_SHELL_NU: "1" } {
615+
let out = (with-env { VP_ENV_USE_EVAL_ENABLE: "1", VP_SHELL: "nu" } {
615616
^vp ...$args
616617
})
617618
let lines = ($out | lines)
@@ -674,7 +675,7 @@ function vp {
674675
& (Join-Path $__vp_bin "vp") @args; return
675676
}
676677
$env:VP_ENV_USE_EVAL_ENABLE = "1"
677-
$env:VP_SHELL_PWSH = "1"
678+
$env:VP_SHELL = "pwsh"
678679
$output = & (Join-Path $__vp_bin "vp") @args 2>&1 | ForEach-Object {
679680
if ($_ -is [System.Management.Automation.ErrorRecord]) {
680681
Write-Host $_.Exception.Message
@@ -683,7 +684,7 @@ function vp {
683684
}
684685
}
685686
Remove-Item Env:VP_ENV_USE_EVAL_ENABLE -ErrorAction SilentlyContinue
686-
Remove-Item Env:VP_SHELL_PWSH -ErrorAction SilentlyContinue
687+
Remove-Item Env:VP_SHELL -ErrorAction SilentlyContinue
687688
if ($LASTEXITCODE -eq 0 -and $output) {
688689
Invoke-Expression ($output -join "`n")
689690
}
@@ -897,10 +898,6 @@ mod tests {
897898
nu_content.contains("VP_COMPLETE=fish"),
898899
"env.nu should use dynamic Fish completion delegation"
899900
);
900-
assert!(
901-
nu_content.contains("VP_SHELL_NU"),
902-
"env.nu should use VP_SHELL_NU explicit marker instead of inherited NU_VERSION"
903-
);
904901
assert!(nu_content.contains("load-env"), "env.nu should use load-env to apply exports");
905902
}
906903

@@ -1086,10 +1083,6 @@ mod tests {
10861083
fish_content.contains("\"$argv[2]\" = \"use\""),
10871084
"env.fish should check for 'use' subcommand"
10881085
);
1089-
assert!(
1090-
fish_content.contains("/vp $argv"),
1091-
"env.fish should use absolute path to vp for passthrough"
1092-
);
10931086
}
10941087

10951088
#[tokio::test]

crates/vite_global_cli/src/commands/env/use.rs

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -181,72 +181,43 @@ mod tests {
181181
use super::*;
182182

183183
#[test]
184-
fn test_detect_shell_pwsh() {
184+
fn test_detect_shell_vp_shell_powershell() {
185185
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
186-
vp_shell_pwsh: true,
186+
vp_shell: Some("powershell".into()),
187187
..vite_shared::EnvConfig::for_test()
188188
});
189189
let shell = detect_shell();
190-
assert!(matches!(shell, Shell::PowerShell));
190+
assert_eq!(shell, Shell::PowerShell);
191191
}
192192

193193
#[test]
194-
fn test_detect_shell_fish() {
194+
fn test_detect_shell_vp_shell_fish() {
195195
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
196-
fish_version: Some("3.7.0".into()),
196+
vp_shell: Some("fish".into()),
197197
..vite_shared::EnvConfig::for_test()
198198
});
199199
let shell = detect_shell();
200-
assert!(matches!(shell, Shell::Fish));
200+
assert_eq!(shell, Shell::Fish);
201201
}
202202

203203
#[test]
204-
fn test_detect_shell_fish_and_nushell() {
205-
// Fish takes priority over Nu shell signal
204+
fn test_detect_shell_vp_shell_nu() {
206205
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
207-
fish_version: Some("3.7.0".into()),
208-
vp_shell_nu: true,
206+
vp_shell: Some("nu".into()),
209207
..vite_shared::EnvConfig::for_test()
210208
});
211209
let shell = detect_shell();
212-
assert!(matches!(shell, Shell::Fish));
210+
assert_eq!(shell, Shell::NuShell);
213211
}
214212

215213
#[test]
216214
fn test_detect_shell_posix_default() {
217-
// All shell detection fields None → defaults
218215
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_test());
219216
let shell = detect_shell();
220217
#[cfg(not(windows))]
221-
assert!(matches!(shell, Shell::Posix));
218+
assert_eq!(shell, Shell::Posix);
222219
#[cfg(windows)]
223-
assert!(matches!(shell, Shell::Cmd));
224-
}
225-
226-
#[test]
227-
fn test_detect_shell_nushell() {
228-
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
229-
vp_shell_nu: true,
230-
..vite_shared::EnvConfig::for_test()
231-
});
232-
let shell = detect_shell();
233-
assert!(matches!(shell, Shell::NuShell));
234-
}
235-
236-
#[test]
237-
fn test_detect_shell_inherited_nu_version_is_posix() {
238-
// NU_VERSION alone (inherited from parent Nushell) must NOT trigger Nu detection.
239-
// Only the explicit VP_SHELL_NU marker set by env.nu wrapper counts.
240-
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
241-
nu_version: Some("0.111.0".into()),
242-
vp_shell_nu: false,
243-
..vite_shared::EnvConfig::for_test()
244-
});
245-
let shell = detect_shell();
246-
#[cfg(not(windows))]
247-
assert!(matches!(shell, Shell::Posix));
248-
#[cfg(windows)]
249-
let _ = shell;
220+
assert_eq!(shell, Shell::Cmd);
250221
}
251222

252223
#[test]
@@ -346,7 +317,7 @@ mod tests {
346317
let cwd = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
347318
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
348319
env_use_eval_enable: true,
349-
vp_shell_pwsh: true,
320+
vp_shell: Some("powershell".into()),
350321
..vite_shared::EnvConfig::for_test_with_home(temp_dir.path())
351322
});
352323

crates/vite_global_cli/src/commands/shell.rs

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Shared shell detection and profile helpers.
22
3+
use std::str::FromStr;
4+
35
use directories::BaseDirs;
46
use vite_path::{AbsolutePath, AbsolutePathBuf};
57
use vite_str::Str;
@@ -19,21 +21,37 @@ pub enum Shell {
1921
Cmd,
2022
}
2123

22-
/// Detect the current shell from environment variables.
24+
impl FromStr for Shell {
25+
type Err = ();
26+
27+
fn from_str(s: &str) -> Result<Self, Self::Err> {
28+
match s.to_lowercase().as_str() {
29+
"sh" | "bash" | "zsh" => Ok(Shell::Posix),
30+
"fish" => Ok(Shell::Fish),
31+
"nu" | "nushell" => Ok(Shell::NuShell),
32+
"pwsh" | "powershell" => Ok(Shell::PowerShell),
33+
"cmd" => Ok(Shell::Cmd),
34+
_ => Err(()),
35+
}
36+
}
37+
}
38+
39+
/// Detect the current shell:
40+
/// 1. `VP_SHELL` environment variable
41+
/// 2. Platform default
2342
#[must_use]
2443
pub fn detect_shell() -> Shell {
2544
let config = vite_shared::EnvConfig::get();
26-
if config.fish_version.is_some() {
27-
Shell::Fish
28-
} else if config.vp_shell_nu {
29-
Shell::NuShell
30-
} else if config.vp_shell_pwsh {
31-
Shell::PowerShell
32-
} else if cfg!(windows) {
33-
Shell::Cmd
34-
} else {
35-
Shell::Posix
45+
46+
// 1. Check VP_SHELL environment variable
47+
if let Some(vp_shell) = &config.vp_shell {
48+
if let Ok(shell) = Shell::from_str(vp_shell) {
49+
return shell;
50+
}
3651
}
52+
53+
// 2. Platform default
54+
if cfg!(windows) { Shell::Cmd } else { Shell::Posix }
3755
}
3856

3957
/// All shell profile files that interactive terminal sessions may source.
@@ -235,53 +253,98 @@ mod tests {
235253
use super::*;
236254

237255
#[test]
238-
fn test_detect_shell_pwsh() {
256+
fn test_shell_from_str() {
257+
// POSIX shells
258+
assert_eq!(Shell::from_str("sh"), Ok(Shell::Posix));
259+
assert_eq!(Shell::from_str("bash"), Ok(Shell::Posix));
260+
assert_eq!(Shell::from_str("zsh"), Ok(Shell::Posix));
261+
262+
// Other shells
263+
assert_eq!(Shell::from_str("fish"), Ok(Shell::Fish));
264+
assert_eq!(Shell::from_str("nu"), Ok(Shell::NuShell));
265+
assert_eq!(Shell::from_str("nushell"), Ok(Shell::NuShell));
266+
assert_eq!(Shell::from_str("powershell"), Ok(Shell::PowerShell));
267+
assert_eq!(Shell::from_str("pwsh"), Ok(Shell::PowerShell));
268+
assert_eq!(Shell::from_str("cmd"), Ok(Shell::Cmd));
269+
270+
// Case insensitive
271+
assert_eq!(Shell::from_str("SH"), Ok(Shell::Posix));
272+
assert_eq!(Shell::from_str("BASH"), Ok(Shell::Posix));
273+
assert_eq!(Shell::from_str("Fish"), Ok(Shell::Fish));
274+
assert_eq!(Shell::from_str("POWERSHELL"), Ok(Shell::PowerShell));
275+
assert_eq!(Shell::from_str("Nu"), Ok(Shell::NuShell));
276+
277+
// Invalid
278+
assert!(Shell::from_str("posix").is_err());
279+
assert!(Shell::from_str("invalid").is_err());
280+
assert!(Shell::from_str("").is_err());
281+
}
282+
283+
#[test]
284+
fn test_detect_shell_vp_shell_explicit() {
239285
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
240-
vp_shell_pwsh: true,
286+
vp_shell: Some("nu".into()),
241287
..vite_shared::EnvConfig::for_test()
242288
});
243289
let shell = detect_shell();
244-
assert!(matches!(shell, Shell::PowerShell));
290+
assert_eq!(shell, Shell::NuShell);
245291
}
246292

247293
#[test]
248-
fn test_detect_shell_fish() {
294+
fn test_detect_shell_vp_shell_case_insensitive() {
249295
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
250-
fish_version: Some("3.7.0".into()),
296+
vp_shell: Some("POWERSHELL".into()),
251297
..vite_shared::EnvConfig::for_test()
252298
});
253299
let shell = detect_shell();
254-
assert!(matches!(shell, Shell::Fish));
300+
assert_eq!(shell, Shell::PowerShell);
255301
}
256302

257303
#[test]
258-
fn test_detect_shell_nushell() {
304+
fn test_detect_shell_vp_shell_pwsh_alias() {
259305
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
260-
vp_shell_nu: true,
306+
vp_shell: Some("pwsh".into()),
261307
..vite_shared::EnvConfig::for_test()
262308
});
263309
let shell = detect_shell();
264-
assert!(matches!(shell, Shell::NuShell));
310+
assert_eq!(shell, Shell::PowerShell);
265311
}
266312

267313
#[test]
268-
fn test_detect_shell_nushell_wins_over_powershell() {
314+
fn test_detect_shell_vp_shell_fish() {
269315
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
270-
vp_shell_nu: true,
271-
ps_module_path: Some("/some/path".into()),
316+
vp_shell: Some("fish".into()),
272317
..vite_shared::EnvConfig::for_test()
273318
});
274319
let shell = detect_shell();
275-
assert!(matches!(shell, Shell::NuShell));
320+
assert_eq!(shell, Shell::Fish);
276321
}
277322

278323
#[test]
279-
fn test_detect_shell_posix_default() {
280-
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig::for_test());
324+
fn test_detect_shell_defaults_without_vp_shell() {
325+
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
326+
vp_shell: None,
327+
..vite_shared::EnvConfig::for_test()
328+
});
329+
let shell = detect_shell();
330+
if cfg!(windows) {
331+
assert_eq!(shell, Shell::Cmd);
332+
} else {
333+
assert_eq!(shell, Shell::Posix);
334+
}
335+
}
336+
337+
#[test]
338+
fn test_detect_shell_invalid_vp_shell_falls_back_to_default() {
339+
let _guard = vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
340+
vp_shell: Some("invalid".into()),
341+
..vite_shared::EnvConfig::for_test()
342+
});
281343
let shell = detect_shell();
282-
#[cfg(not(windows))]
283-
assert!(matches!(shell, Shell::Posix));
284-
#[cfg(windows)]
285-
assert!(matches!(shell, Shell::Cmd));
344+
if cfg!(windows) {
345+
assert_eq!(shell, Shell::Cmd);
346+
} else {
347+
assert_eq!(shell, Shell::Posix);
348+
}
286349
}
287350
}

crates/vite_shared/src/env_config.rs

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -113,33 +113,10 @@ pub struct EnvConfig {
113113
/// Env: `HOME` (Unix) / `USERPROFILE` (Windows)
114114
pub user_home: Option<PathBuf>,
115115

116-
/// Fish shell version (indicates running under fish).
116+
/// Explicitly specify the current shell.
117117
///
118-
/// Env: `FISH_VERSION`
119-
pub fish_version: Option<String>,
120-
121-
/// `PowerShell` module path (indicates running under `PowerShell` on Windows).
122-
///
123-
/// Env: `PSModulePath`
124-
pub ps_module_path: Option<String>,
125-
126-
/// Nu shell version (indicates running under Nu shell).
127-
///
128-
/// Env: `NU_VERSION`
129-
pub nu_version: Option<String>,
130-
131-
/// Explicit Nu shell eval signal set by the `env.nu` wrapper.
132-
///
133-
/// Unlike `NU_VERSION`, this is not inherited by child processes — it is only
134-
/// present when the Nushell wrapper explicitly passes it via `with-env`.
135-
///
136-
/// Env: `VP_SHELL_NU`
137-
pub vp_shell_nu: bool,
138-
139-
/// Explicit `PowerShell` eval signal set by the `env.ps1` wrapper.
140-
///
141-
/// Env: `VP_SHELL_PWSH`
142-
pub vp_shell_pwsh: bool,
118+
/// Env: `VP_SHELL`
119+
pub vp_shell: Option<String>,
143120
}
144121

145122
impl EnvConfig {
@@ -167,11 +144,7 @@ impl EnvConfig {
167144
.or_else(|_| std::env::var("USERPROFILE"))
168145
.ok()
169146
.map(PathBuf::from),
170-
fish_version: std::env::var("FISH_VERSION").ok(),
171-
ps_module_path: std::env::var("PSModulePath").ok(),
172-
nu_version: std::env::var("NU_VERSION").ok(),
173-
vp_shell_nu: std::env::var(env_vars::VP_SHELL_NU).is_ok(),
174-
vp_shell_pwsh: std::env::var(env_vars::VP_SHELL_PWSH).is_ok(),
147+
vp_shell: std::env::var(env_vars::VP_SHELL).ok(),
175148
}
176149
}
177150

@@ -254,11 +227,7 @@ impl EnvConfig {
254227
update_task_types: None,
255228
node_version: None,
256229
user_home: None,
257-
fish_version: None,
258-
ps_module_path: None,
259-
nu_version: None,
260-
vp_shell_nu: false,
261-
vp_shell_pwsh: false,
230+
vp_shell: None,
262231
}
263232
}
264233

0 commit comments

Comments
 (0)