Detailed changes
@@ -3,6 +3,7 @@ mod diff;
mod mention;
mod terminal;
+use ::terminal::terminal_settings::TerminalSettings;
use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
@@ -1961,11 +1962,11 @@ impl AcpThread {
) -> Task<Result<Entity<Terminal>>> {
let env = match &cwd {
Some(dir) => self.project.update(cx, |project, cx| {
- project.directory_environment(dir.as_path().into(), cx)
+ let shell = TerminalSettings::get_global(cx).shell.clone();
+ project.directory_environment(&shell, dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
-
let env = cx.spawn(async move |_, _| {
let mut env = env.await.unwrap_or_default();
// Disables paging for `git` and hopefully other commands
@@ -27,6 +27,7 @@ use std::{
time::{Duration, Instant},
};
use task::{Shell, ShellBuilder};
+use terminal::terminal_settings::TerminalSettings;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
@@ -119,9 +120,10 @@ impl Tool for TerminalTool {
};
let cwd = working_dir.clone();
- let env = match &working_dir {
+ let env = match &cwd {
Some(dir) => project.update(cx, |project, cx| {
- project.directory_environment(dir.as_path().into(), cx)
+ let shell = TerminalSettings::get_global(cx).shell.clone();
+ project.directory_environment(&shell, dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
@@ -1187,11 +1187,13 @@ impl ToolchainLister for PythonToolchainProvider {
ShellKind::PowerShell => ".",
ShellKind::Fish => "source",
ShellKind::Csh => "source",
- ShellKind::Posix => "source",
+ ShellKind::Tcsh => "source",
+ ShellKind::Posix | ShellKind::Rc => "source",
};
let activate_script_name = match shell {
- ShellKind::Posix => "activate",
+ ShellKind::Posix | ShellKind::Rc => "activate",
ShellKind::Csh => "activate.csh",
+ ShellKind::Tcsh => "activate.csh",
ShellKind::Fish => "activate.fish",
ShellKind::Nushell => "activate.nu",
ShellKind::PowerShell => "activate.ps1",
@@ -1220,7 +1222,9 @@ impl ToolchainLister for PythonToolchainProvider {
ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")),
ShellKind::PowerShell => None,
ShellKind::Csh => None,
+ ShellKind::Tcsh => None,
ShellKind::Cmd => None,
+ ShellKind::Rc => None,
})
}
_ => {}
@@ -1,7 +1,6 @@
use crate::environment::EnvironmentErrorMessage;
use std::process::ExitStatus;
-#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
use {collections::HashMap, std::path::Path, util::ResultExt};
#[derive(Clone)]
@@ -28,7 +27,6 @@ impl From<DirenvError> for Option<EnvironmentErrorMessage> {
}
}
-#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
pub async fn load_direnv_environment(
env: &HashMap<String, String>,
dir: &Path,
@@ -1,6 +1,7 @@
use futures::{FutureExt, future::Shared};
use language::Buffer;
use std::{path::Path, sync::Arc};
+use task::Shell;
use util::ResultExt;
use worktree::Worktree;
@@ -16,6 +17,8 @@ use crate::{
pub struct ProjectEnvironment {
cli_environment: Option<HashMap<String, String>>,
environments: HashMap<Arc<Path>, Shared<Task<Option<HashMap<String, String>>>>>,
+ shell_based_environments:
+ HashMap<(Shell, Arc<Path>), Shared<Task<Option<HashMap<String, String>>>>>,
environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
}
@@ -30,6 +33,7 @@ impl ProjectEnvironment {
Self {
cli_environment,
environments: Default::default(),
+ shell_based_environments: Default::default(),
environment_error_messages: Default::default(),
}
}
@@ -134,7 +138,22 @@ impl ProjectEnvironment {
self.environments
.entry(abs_path.clone())
- .or_insert_with(|| get_directory_env_impl(abs_path.clone(), cx).shared())
+ .or_insert_with(|| {
+ get_directory_env_impl(&Shell::System, abs_path.clone(), cx).shared()
+ })
+ .clone()
+ }
+
+ /// Returns the project environment, if possible, with the given shell.
+ pub fn get_directory_environment_for_shell(
+ &mut self,
+ shell: &Shell,
+ abs_path: Arc<Path>,
+ cx: &mut Context<Self>,
+ ) -> Shared<Task<Option<HashMap<String, String>>>> {
+ self.shell_based_environments
+ .entry((shell.clone(), abs_path.clone()))
+ .or_insert_with(|| get_directory_env_impl(shell, abs_path.clone(), cx).shared())
.clone()
}
}
@@ -176,6 +195,7 @@ impl EnvironmentErrorMessage {
}
async fn load_directory_shell_environment(
+ shell: &Shell,
abs_path: &Path,
load_direnv: &DirenvSettings,
) -> (
@@ -198,7 +218,7 @@ async fn load_directory_shell_environment(
);
};
- load_shell_environment(dir, load_direnv).await
+ load_shell_environment(shell, dir, load_direnv).await
}
Err(err) => (
None,
@@ -211,51 +231,8 @@ async fn load_directory_shell_environment(
}
}
-#[cfg(any(test, feature = "test-support"))]
-async fn load_shell_environment(
- _dir: &Path,
- _load_direnv: &DirenvSettings,
-) -> (
- Option<HashMap<String, String>>,
- Option<EnvironmentErrorMessage>,
-) {
- let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
- .into_iter()
- .collect();
- (Some(fake_env), None)
-}
-
-#[cfg(all(target_os = "windows", not(any(test, feature = "test-support"))))]
-async fn load_shell_environment(
- dir: &Path,
- _load_direnv: &DirenvSettings,
-) -> (
- Option<HashMap<String, String>>,
- Option<EnvironmentErrorMessage>,
-) {
- use util::shell_env;
-
- let envs = match shell_env::capture(dir).await {
- Ok(envs) => envs,
- Err(err) => {
- util::log_err(&err);
- return (
- None,
- Some(EnvironmentErrorMessage(format!(
- "Failed to load environment variables: {}",
- err
- ))),
- );
- }
- };
-
- // Note: direnv is not available on Windows, so we skip direnv processing
- // and just return the shell environment
- (Some(envs), None)
-}
-
-#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
async fn load_shell_environment(
+ shell: &Shell,
dir: &Path,
load_direnv: &DirenvSettings,
) -> (
@@ -265,55 +242,86 @@ async fn load_shell_environment(
use crate::direnv::load_direnv_environment;
use util::shell_env;
- let dir_ = dir.to_owned();
- let mut envs = match shell_env::capture(&dir_).await {
- Ok(envs) => envs,
- Err(err) => {
- util::log_err(&err);
- return (
- None,
- Some(EnvironmentErrorMessage::from_str(
- "Failed to load environment variables. See log for details",
- )),
- );
- }
- };
-
- // If the user selects `Direct` for direnv, it would set an environment
- // variable that later uses to know that it should not run the hook.
- // We would include in `.envs` call so it is okay to run the hook
- // even if direnv direct mode is enabled.
- let (direnv_environment, direnv_error) = match load_direnv {
- DirenvSettings::ShellHook => (None, None),
- DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
- Ok(env) => (Some(env), None),
- Err(err) => (None, err.into()),
- },
- };
- if let Some(direnv_environment) = direnv_environment {
- for (key, value) in direnv_environment {
- if let Some(value) = value {
- envs.insert(key, value);
- } else {
- envs.remove(&key);
+ if cfg!(any(test, feature = "test-support")) {
+ let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
+ .into_iter()
+ .collect();
+ (Some(fake_env), None)
+ } else if cfg!(target_os = "windows",) {
+ let (shell, args) = shell.program_and_args();
+ let envs = match shell_env::capture(shell, args, dir).await {
+ Ok(envs) => envs,
+ Err(err) => {
+ util::log_err(&err);
+ return (
+ None,
+ Some(EnvironmentErrorMessage(format!(
+ "Failed to load environment variables: {}",
+ err
+ ))),
+ );
+ }
+ };
+
+ // Note: direnv is not available on Windows, so we skip direnv processing
+ // and just return the shell environment
+ (Some(envs), None)
+ } else {
+ let dir_ = dir.to_owned();
+ let (shell, args) = shell.program_and_args();
+ let mut envs = match shell_env::capture(shell, args, &dir_).await {
+ Ok(envs) => envs,
+ Err(err) => {
+ util::log_err(&err);
+ return (
+ None,
+ Some(EnvironmentErrorMessage::from_str(
+ "Failed to load environment variables. See log for details",
+ )),
+ );
+ }
+ };
+
+ // If the user selects `Direct` for direnv, it would set an environment
+ // variable that later uses to know that it should not run the hook.
+ // We would include in `.envs` call so it is okay to run the hook
+ // even if direnv direct mode is enabled.
+ let (direnv_environment, direnv_error) = match load_direnv {
+ DirenvSettings::ShellHook => (None, None),
+ DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await {
+ Ok(env) => (Some(env), None),
+ Err(err) => (None, err.into()),
+ },
+ };
+ if let Some(direnv_environment) = direnv_environment {
+ for (key, value) in direnv_environment {
+ if let Some(value) = value {
+ envs.insert(key, value);
+ } else {
+ envs.remove(&key);
+ }
}
}
- }
- (Some(envs), direnv_error)
+ (Some(envs), direnv_error)
+ }
}
fn get_directory_env_impl(
+ shell: &Shell,
abs_path: Arc<Path>,
cx: &Context<ProjectEnvironment>,
) -> Task<Option<HashMap<String, String>>> {
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
+ let shell = shell.clone();
cx.spawn(async move |this, cx| {
let (mut shell_env, error_message) = cx
.background_spawn({
let abs_path = abs_path.clone();
- async move { load_directory_shell_environment(&abs_path, &load_direnv).await }
+ async move {
+ load_directory_shell_environment(&shell, &abs_path, &load_direnv).await
+ }
})
.await;
@@ -33,6 +33,7 @@ pub mod search_history;
mod yarn;
use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
+use task::Shell;
use crate::{
agent_server_store::{AgentServerStore, AllAgentServersSettings},
@@ -1894,11 +1895,12 @@ impl Project {
pub fn directory_environment(
&self,
+ shell: &Shell,
abs_path: Arc<Path>,
cx: &mut App,
) -> Shared<Task<Option<HashMap<String, String>>>> {
self.environment.update(cx, |environment, cx| {
- environment.get_directory_environment(abs_path, cx)
+ environment.get_directory_environment_for_shell(shell, abs_path, cx)
})
}
@@ -16,7 +16,7 @@ use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
};
-use util::{get_default_system_shell, get_system_shell, maybe, rel_path::RelPath};
+use util::{get_default_system_shell, maybe, rel_path::RelPath};
use crate::{Project, ProjectPath};
@@ -98,15 +98,7 @@ impl Project {
.read(cx)
.shell()
.unwrap_or_else(get_default_system_shell),
- None => match &settings.shell {
- Shell::Program(program) => program.clone(),
- Shell::WithArguments {
- program,
- args: _,
- title_override: _,
- } => program.clone(),
- Shell::System => get_system_shell(),
- },
+ None => settings.shell.program(),
};
let project_path_contexts = self
@@ -332,15 +324,7 @@ impl Project {
.read(cx)
.shell()
.unwrap_or_else(get_default_system_shell),
- None => match &settings.shell {
- Shell::Program(program) => program.clone(),
- Shell::WithArguments {
- program,
- args: _,
- title_override: _,
- } => program.clone(),
- Shell::System => get_system_shell(),
- },
+ None => settings.shell.program(),
});
let lang_registry = self.languages.clone();
@@ -1,207 +1,8 @@
-use std::fmt;
-
-use util::get_system_shell;
+use util::shell::get_system_shell;
use crate::Shell;
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum ShellKind {
- #[default]
- Posix,
- Csh,
- Fish,
- PowerShell,
- Nushell,
- Cmd,
-}
-
-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::Fish => write!(f, "fish"),
- ShellKind::PowerShell => write!(f, "powershell"),
- ShellKind::Nushell => write!(f, "nu"),
- ShellKind::Cmd => write!(f, "cmd"),
- }
- }
-}
-
-impl ShellKind {
- pub fn system() -> Self {
- Self::new(&get_system_shell())
- }
-
- pub fn new(program: &str) -> Self {
- #[cfg(windows)]
- let (_, program) = program.rsplit_once('\\').unwrap_or(("", program));
- #[cfg(not(windows))]
- let (_, program) = program.rsplit_once('/').unwrap_or(("", program));
- 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 {
- // Some other shell detected, the user might install and use a
- // unix-like shell.
- ShellKind::Posix
- }
- }
-
- 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::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
- }
- }
- }
-
- 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 => 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,
- }
- }
-}
+pub use util::shell::ShellKind;
/// ShellBuilder is used to turn a user-requested task into a
/// program that can be executed by the shell.
@@ -253,7 +54,12 @@ impl ShellBuilder {
ShellKind::Cmd => {
format!("{} /C '{}'", self.program, command_to_use_in_label)
}
- ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => {
+ ShellKind::Posix
+ | ShellKind::Nushell
+ | ShellKind::Fish
+ | ShellKind::Csh
+ | ShellKind::Tcsh
+ | ShellKind::Rc => {
let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
format!(
"{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
@@ -283,7 +89,12 @@ impl ShellBuilder {
});
if self.redirect_stdin {
match self.kind {
- ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => {
+ ShellKind::Posix
+ | ShellKind::Nushell
+ | ShellKind::Fish
+ | ShellKind::Csh
+ | ShellKind::Tcsh
+ | ShellKind::Rc => {
combined_command.insert(0, '(');
combined_command.push_str(") </dev/null");
}
@@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::path::PathBuf;
use std::str::FromStr;
+use util::get_system_shell;
pub use adapter_schema::{AdapterSchema, AdapterSchemas};
pub use debug_format::{
@@ -317,7 +318,7 @@ pub struct TaskContext {
pub struct RunnableTag(pub SharedString);
/// Shell configuration to open the terminal with.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Shell {
/// Use the system's default terminal configuration in /etc/passwd
@@ -336,6 +337,23 @@ pub enum Shell {
},
}
+impl Shell {
+ pub fn program(&self) -> String {
+ match self {
+ Shell::Program(program) => program.clone(),
+ Shell::WithArguments { program, .. } => program.clone(),
+ Shell::System => get_system_shell(),
+ }
+ }
+ pub fn program_and_args(&self) -> (String, &[String]) {
+ match self {
+ Shell::Program(program) => (program.clone(), &[]),
+ Shell::WithArguments { program, args, .. } => (program.clone(), args),
+ Shell::System => (get_system_shell(), &[]),
+ }
+ }
+}
+
type VsCodeEnvVariable = String;
type ZedEnvVariable = String;
@@ -398,7 +398,7 @@ impl TerminalBuilder {
#[cfg(target_os = "windows")]
{
Some(ShellParams::new(
- util::get_windows_system_shell(),
+ util::shell::get_windows_system_shell(),
None,
None,
))
@@ -0,0 +1,340 @@
+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,
+ }
+ }
+}
@@ -1,60 +1,81 @@
-#![cfg_attr(not(unix), allow(unused))]
+use std::path::Path;
use anyhow::{Context as _, Result};
use collections::HashMap;
-/// Capture all environment variables from the login shell.
+use crate::shell::ShellKind;
+
+pub fn print_env() {
+ let env_vars: HashMap<String, String> = std::env::vars().collect();
+ let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
+ eprintln!("Error serializing environment variables: {}", err);
+ std::process::exit(1);
+ });
+ println!("{}", json);
+}
+
+/// Capture all environment variables from the login shell in the given directory.
+pub async fn capture(
+ shell_path: impl AsRef<Path>,
+ args: &[String],
+ directory: impl AsRef<Path>,
+) -> Result<collections::HashMap<String, String>> {
+ #[cfg(windows)]
+ return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
+ #[cfg(unix)]
+ return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
+}
+
#[cfg(unix)]
-pub async fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
+async fn capture_unix(
+ shell_path: &Path,
+ args: &[String],
+ directory: &Path,
+) -> Result<collections::HashMap<String, String>> {
use std::os::unix::process::CommandExt;
use std::process::Stdio;
let zed_path = super::get_shell_safe_zed_path()?;
- let shell_path = std::env::var("SHELL").map(std::path::PathBuf::from)?;
- let shell_name = shell_path.file_name().and_then(std::ffi::OsStr::to_str);
+ let shell_kind = ShellKind::new(shell_path);
let mut command_string = String::new();
- let mut command = std::process::Command::new(&shell_path);
+ let mut command = std::process::Command::new(shell_path);
+ command.args(args);
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
// so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
// See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
const FD_STDIN: std::os::fd::RawFd = 0;
const FD_STDOUT: std::os::fd::RawFd = 1;
- let (fd_num, redir) = match shell_name {
- Some("rc") => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
- Some("nu") | Some("tcsh") => (FD_STDOUT, "".to_string()),
+ let (fd_num, redir) = match shell_kind {
+ ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
+ ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
};
command.stdin(Stdio::null());
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
- let mut command_prefix = String::new();
- match shell_name {
- Some("tcsh" | "csh") => {
+ match shell_kind {
+ ShellKind::Csh | ShellKind::Tcsh => {
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
command.arg0("-");
}
- Some("fish") => {
+ ShellKind::Fish => {
// in fish, asdf, direnv attach to the `fish_prompt` event
command_string.push_str("emit fish_prompt;");
command.arg("-l");
}
- Some("nu") => {
- // nu needs special handling for -- options.
- command_prefix = String::from("^");
- }
_ => {
command.arg("-l");
}
}
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
command_string.push_str(&format!("cd '{}';", directory.display()));
- command_string.push_str(&format!(
- "{}{} --printenv {}",
- command_prefix, zed_path, redir
- ));
+ if let Some(prefix) = shell_kind.command_prefix() {
+ command_string.push(prefix);
+ }
+ command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
command.args(["-i", "-c", &command_string]);
super::set_pre_exec_to_start_new_session(&mut command);
@@ -99,54 +120,104 @@ async fn spawn_and_read_fd(
Ok((buffer, process.output().await?))
}
-/// Capture all environment variables from the shell on Windows.
#[cfg(windows)]
-pub async fn capture(directory: &std::path::Path) -> Result<collections::HashMap<String, String>> {
+async fn capture_windows(
+ shell_path: &Path,
+ _args: &[String],
+ directory: &Path,
+) -> Result<collections::HashMap<String, String>> {
use std::process::Stdio;
let zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
- // Use PowerShell to get environment variables in the directory context
- let output = crate::command::new_smol_command(crate::get_windows_system_shell())
- .args([
- "-NonInteractive",
- "-NoProfile",
- "-Command",
- &format!(
- "Set-Location '{}'; & '{}' --printenv",
- directory.display(),
- zed_path.display()
- ),
- ])
- .stdin(Stdio::null())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .output()
- .await?;
-
- anyhow::ensure!(
- output.status.success(),
- "PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
- output.status,
- String::from_utf8_lossy(&output.stdout),
- String::from_utf8_lossy(&output.stderr),
- );
+ let shell_kind = ShellKind::new(shell_path);
+ let env_output = match shell_kind {
+ ShellKind::Posix | ShellKind::Csh | ShellKind::Tcsh | ShellKind::Rc | ShellKind::Fish => {
+ return Err(anyhow::anyhow!("unsupported shell kind"));
+ }
+ ShellKind::PowerShell => {
+ let output = crate::command::new_smol_command(shell_path)
+ .args([
+ "-NonInteractive",
+ "-NoProfile",
+ "-Command",
+ &format!(
+ "Set-Location '{}'; & '{}' --printenv",
+ directory.display(),
+ zed_path.display()
+ ),
+ ])
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
+ output.status,
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr),
+ );
+ output
+ }
+ ShellKind::Nushell => {
+ let output = crate::command::new_smol_command(shell_path)
+ .args([
+ "-c",
+ &format!(
+ "cd '{}'; {} --printenv",
+ directory.display(),
+ zed_path.display()
+ ),
+ ])
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
+ output.status,
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr),
+ );
+ output
+ }
+ ShellKind::Cmd => {
+ let output = crate::command::new_smol_command(shell_path)
+ .args([
+ "/c",
+ &format!(
+ "cd '{}'; {} --printenv",
+ directory.display(),
+ zed_path.display()
+ ),
+ ])
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .output()
+ .await?;
+
+ anyhow::ensure!(
+ output.status.success(),
+ "Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
+ output.status,
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr),
+ );
+ output
+ }
+ };
- let env_output = String::from_utf8_lossy(&output.stdout);
+ let env_output = String::from_utf8_lossy(&env_output.stdout);
// Parse the JSON output from zed --printenv
- let env_map: collections::HashMap<String, String> = serde_json::from_str(&env_output)
- .with_context(|| "Failed to deserialize environment variables from json")?;
- Ok(env_map)
-}
-
-pub fn print_env() {
- let env_vars: HashMap<String, String> = std::env::vars().collect();
- let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
- eprintln!("Error serializing environment variables: {}", err);
- std::process::exit(1);
- });
- println!("{}", json);
- std::process::exit(0);
+ serde_json::from_str(&env_output)
+ .with_context(|| "Failed to deserialize environment variables from json")
}
@@ -8,6 +8,7 @@ pub mod redact;
pub mod rel_path;
pub mod schemars;
pub mod serde;
+pub mod shell;
pub mod shell_env;
pub mod size;
#[cfg(any(test, feature = "test-support"))]
@@ -367,7 +368,7 @@ pub async fn load_login_shell_environment() -> Result<()> {
// into shell's `cd` command (and hooks) to manipulate env.
// We do this so that we get the env a user would have when spawning a shell
// in home directory.
- for (name, value) in shell_env::capture(paths::home_dir()).await? {
+ for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()).await? {
unsafe { env::set_var(&name, &value) };
}
@@ -555,108 +556,6 @@ pub fn wrapped_usize_outward_from(
})
}
-#[cfg(target_os = "windows")]
-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()
-}
-
pub trait ResultExt<E> {
type Ok;
@@ -1100,29 +999,7 @@ pub fn default<D: Default>() -> D {
Default::default()
}
-pub fn get_system_shell() -> String {
- #[cfg(target_os = "windows")]
- {
- get_windows_system_shell()
- }
-
- #[cfg(not(target_os = "windows"))]
- {
- std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
- }
-}
-
-pub fn get_default_system_shell() -> String {
- #[cfg(target_os = "windows")]
- {
- get_windows_system_shell()
- }
-
- #[cfg(not(target_os = "windows"))]
- {
- "/bin/sh".to_string()
- }
-}
+pub use self::shell::{get_default_system_shell, get_system_shell};
#[derive(Debug)]
pub enum ConnectionResult<O> {