When sourcing the project environment for the terminal tool, we will now do so by spawning the shell specified by the users `terminal.shell` setting (or as usual fall back to the login shell). Closes #37687 Release Notes: - N/A
341 lines
11 KiB
Rust
341 lines
11 KiB
Rust
use std::{fmt, path::Path, sync::LazyLock};
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum ShellKind {
|
|
#[default]
|
|
Posix,
|
|
Csh,
|
|
Tcsh,
|
|
Rc,
|
|
Fish,
|
|
PowerShell,
|
|
Nushell,
|
|
Cmd,
|
|
}
|
|
|
|
pub fn get_system_shell() -> String {
|
|
if cfg!(windows) {
|
|
get_windows_system_shell()
|
|
} else {
|
|
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
|
|
}
|
|
}
|
|
|
|
pub fn get_default_system_shell() -> String {
|
|
if cfg!(windows) {
|
|
get_windows_system_shell()
|
|
} else {
|
|
"/bin/sh".to_string()
|
|
}
|
|
}
|
|
|
|
pub fn get_windows_system_shell() -> String {
|
|
use std::path::PathBuf;
|
|
|
|
fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
|
|
#[cfg(target_pointer_width = "64")]
|
|
let env_var = if find_alternate {
|
|
"ProgramFiles(x86)"
|
|
} else {
|
|
"ProgramFiles"
|
|
};
|
|
|
|
#[cfg(target_pointer_width = "32")]
|
|
let env_var = if find_alternate {
|
|
"ProgramW6432"
|
|
} else {
|
|
"ProgramFiles"
|
|
};
|
|
|
|
let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
|
|
install_base_dir
|
|
.read_dir()
|
|
.ok()?
|
|
.filter_map(Result::ok)
|
|
.filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
|
|
.filter_map(|entry| {
|
|
let dir_name = entry.file_name();
|
|
let dir_name = dir_name.to_string_lossy();
|
|
|
|
let version = if find_preview {
|
|
let dash_index = dir_name.find('-')?;
|
|
if &dir_name[dash_index + 1..] != "preview" {
|
|
return None;
|
|
};
|
|
dir_name[..dash_index].parse::<u32>().ok()?
|
|
} else {
|
|
dir_name.parse::<u32>().ok()?
|
|
};
|
|
|
|
let exe_path = entry.path().join("pwsh.exe");
|
|
if exe_path.exists() {
|
|
Some((version, exe_path))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.max_by_key(|(version, _)| *version)
|
|
.map(|(_, path)| path)
|
|
}
|
|
|
|
fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
|
|
let msix_app_dir =
|
|
PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
|
|
if !msix_app_dir.exists() {
|
|
return None;
|
|
}
|
|
|
|
let prefix = if find_preview {
|
|
"Microsoft.PowerShellPreview_"
|
|
} else {
|
|
"Microsoft.PowerShell_"
|
|
};
|
|
msix_app_dir
|
|
.read_dir()
|
|
.ok()?
|
|
.filter_map(|entry| {
|
|
let entry = entry.ok()?;
|
|
if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
|
|
return None;
|
|
}
|
|
|
|
if !entry.file_name().to_string_lossy().starts_with(prefix) {
|
|
return None;
|
|
}
|
|
|
|
let exe_path = entry.path().join("pwsh.exe");
|
|
exe_path.exists().then_some(exe_path)
|
|
})
|
|
.next()
|
|
}
|
|
|
|
fn find_pwsh_in_scoop() -> Option<PathBuf> {
|
|
let pwsh_exe =
|
|
PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
|
|
pwsh_exe.exists().then_some(pwsh_exe)
|
|
}
|
|
|
|
static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
|
|
find_pwsh_in_programfiles(false, false)
|
|
.or_else(|| find_pwsh_in_programfiles(true, false))
|
|
.or_else(|| find_pwsh_in_msix(false))
|
|
.or_else(|| find_pwsh_in_programfiles(false, true))
|
|
.or_else(|| find_pwsh_in_msix(true))
|
|
.or_else(|| find_pwsh_in_programfiles(true, true))
|
|
.or_else(find_pwsh_in_scoop)
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
.unwrap_or("powershell.exe".to_string())
|
|
});
|
|
|
|
(*SYSTEM_SHELL).clone()
|
|
}
|
|
|
|
impl fmt::Display for ShellKind {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
ShellKind::Posix => write!(f, "sh"),
|
|
ShellKind::Csh => write!(f, "csh"),
|
|
ShellKind::Tcsh => write!(f, "tcsh"),
|
|
ShellKind::Fish => write!(f, "fish"),
|
|
ShellKind::PowerShell => write!(f, "powershell"),
|
|
ShellKind::Nushell => write!(f, "nu"),
|
|
ShellKind::Cmd => write!(f, "cmd"),
|
|
ShellKind::Rc => write!(f, "rc"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ShellKind {
|
|
pub fn system() -> Self {
|
|
Self::new(&get_system_shell())
|
|
}
|
|
|
|
pub fn new(program: impl AsRef<Path>) -> Self {
|
|
let program = program.as_ref();
|
|
let Some(program) = program.file_name().and_then(|s| s.to_str()) else {
|
|
return if cfg!(windows) {
|
|
ShellKind::PowerShell
|
|
} else {
|
|
ShellKind::Posix
|
|
};
|
|
};
|
|
if program == "powershell"
|
|
|| program.ends_with("powershell.exe")
|
|
|| program == "pwsh"
|
|
|| program.ends_with("pwsh.exe")
|
|
{
|
|
ShellKind::PowerShell
|
|
} else if program == "cmd" || program.ends_with("cmd.exe") {
|
|
ShellKind::Cmd
|
|
} else if program == "nu" {
|
|
ShellKind::Nushell
|
|
} else if program == "fish" {
|
|
ShellKind::Fish
|
|
} else if program == "csh" {
|
|
ShellKind::Csh
|
|
} else if program == "tcsh" {
|
|
ShellKind::Tcsh
|
|
} else if program == "rc" {
|
|
ShellKind::Rc
|
|
} else {
|
|
if cfg!(windows) {
|
|
ShellKind::PowerShell
|
|
} else {
|
|
// Some other shell detected, the user might install and use a
|
|
// unix-like shell.
|
|
ShellKind::Posix
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn to_shell_variable(self, input: &str) -> String {
|
|
match self {
|
|
Self::PowerShell => Self::to_powershell_variable(input),
|
|
Self::Cmd => Self::to_cmd_variable(input),
|
|
Self::Posix => input.to_owned(),
|
|
Self::Fish => input.to_owned(),
|
|
Self::Csh => input.to_owned(),
|
|
Self::Tcsh => input.to_owned(),
|
|
Self::Rc => input.to_owned(),
|
|
Self::Nushell => Self::to_nushell_variable(input),
|
|
}
|
|
}
|
|
|
|
fn to_cmd_variable(input: &str) -> String {
|
|
if let Some(var_str) = input.strip_prefix("${") {
|
|
if var_str.find(':').is_none() {
|
|
// If the input starts with "${", remove the trailing "}"
|
|
format!("%{}%", &var_str[..var_str.len() - 1])
|
|
} else {
|
|
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
|
// which will result in the task failing to run in such cases.
|
|
input.into()
|
|
}
|
|
} else if let Some(var_str) = input.strip_prefix('$') {
|
|
// If the input starts with "$", directly append to "$env:"
|
|
format!("%{}%", var_str)
|
|
} else {
|
|
// If no prefix is found, return the input as is
|
|
input.into()
|
|
}
|
|
}
|
|
fn to_powershell_variable(input: &str) -> String {
|
|
if let Some(var_str) = input.strip_prefix("${") {
|
|
if var_str.find(':').is_none() {
|
|
// If the input starts with "${", remove the trailing "}"
|
|
format!("$env:{}", &var_str[..var_str.len() - 1])
|
|
} else {
|
|
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
|
// which will result in the task failing to run in such cases.
|
|
input.into()
|
|
}
|
|
} else if let Some(var_str) = input.strip_prefix('$') {
|
|
// If the input starts with "$", directly append to "$env:"
|
|
format!("$env:{}", var_str)
|
|
} else {
|
|
// If no prefix is found, return the input as is
|
|
input.into()
|
|
}
|
|
}
|
|
|
|
fn to_nushell_variable(input: &str) -> String {
|
|
let mut result = String::new();
|
|
let mut source = input;
|
|
let mut is_start = true;
|
|
|
|
loop {
|
|
match source.chars().next() {
|
|
None => return result,
|
|
Some('$') => {
|
|
source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
|
|
is_start = false;
|
|
}
|
|
Some(_) => {
|
|
is_start = false;
|
|
let chunk_end = source.find('$').unwrap_or(source.len());
|
|
let (chunk, rest) = source.split_at(chunk_end);
|
|
result.push_str(chunk);
|
|
source = rest;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
|
|
if source.starts_with("env.") {
|
|
text.push('$');
|
|
return source;
|
|
}
|
|
|
|
match source.chars().next() {
|
|
Some('{') => {
|
|
let source = &source[1..];
|
|
if let Some(end) = source.find('}') {
|
|
let var_name = &source[..end];
|
|
if !var_name.is_empty() {
|
|
if !is_start {
|
|
text.push_str("(");
|
|
}
|
|
text.push_str("$env.");
|
|
text.push_str(var_name);
|
|
if !is_start {
|
|
text.push_str(")");
|
|
}
|
|
&source[end + 1..]
|
|
} else {
|
|
text.push_str("${}");
|
|
&source[end + 1..]
|
|
}
|
|
} else {
|
|
text.push_str("${");
|
|
source
|
|
}
|
|
}
|
|
Some(c) if c.is_alphabetic() || c == '_' => {
|
|
let end = source
|
|
.find(|c: char| !c.is_alphanumeric() && c != '_')
|
|
.unwrap_or(source.len());
|
|
let var_name = &source[..end];
|
|
if !is_start {
|
|
text.push_str("(");
|
|
}
|
|
text.push_str("$env.");
|
|
text.push_str(var_name);
|
|
if !is_start {
|
|
text.push_str(")");
|
|
}
|
|
&source[end..]
|
|
}
|
|
_ => {
|
|
text.push('$');
|
|
source
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
|
|
match self {
|
|
ShellKind::PowerShell => vec!["-C".to_owned(), combined_command],
|
|
ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
|
|
ShellKind::Posix
|
|
| ShellKind::Nushell
|
|
| ShellKind::Fish
|
|
| ShellKind::Csh
|
|
| ShellKind::Tcsh
|
|
| ShellKind::Rc => interactive
|
|
.then(|| "-i".to_owned())
|
|
.into_iter()
|
|
.chain(["-c".to_owned(), combined_command])
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
pub fn command_prefix(&self) -> Option<char> {
|
|
match self {
|
|
ShellKind::PowerShell => Some('&'),
|
|
ShellKind::Nushell => Some('^'),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|