Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 66 additions & 22 deletions crates/pixi_cli/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use rattler_shell::{
shell::{Bash, CmdExe, PowerShell, Shell, ShellEnum, ShellScript},
};
use which::which;
use tempfile::TempPath;

use pixi_config::{ConfigCli, ConfigCliActivation, ConfigCliPrompt};
use pixi_core::{
Expand Down Expand Up @@ -138,61 +139,86 @@ fn start_cmdexe(
}

// allowing dead code so that we test this on unix compilation as well
#[cfg_attr(unix, expect(unused))]
fn start_winbash(
bash: Bash,
#[allow(dead_code)]
fn start_shell_using_winbash<T: Shell + Copy + 'static, F: FnOnce(&TempPath) -> String>(
shell: T,
exec_format: F,
env: &HashMap<String, String>,
prompt: String,
prefix: &Prefix,
source_shell_completions: bool,
) -> miette::Result<Option<i32>> {
// create a tempfile for activation
let mut temp_file = tempfile::Builder::new()
.prefix("pixi_env_")
.suffix(&format!(".{}", bash.extension()))
let mut init_file = tempfile::Builder::new()
.prefix("pixi_init_")
.suffix(".sh")
.rand_bytes(3)
.tempfile()
.into_diagnostic()?;

let mut shell_script = ShellScript::new(bash, Platform::current());
// the init file is always consumed by bash
let mut init_script = ShellScript::new(Bash, Platform::current());
for (key, value) in env {
if key == "PATH" || key == "Path" {
// For Git Bash on Windows, the PATH must be formatted as POSIX paths according
// to the cygpath command, and separated by ":" instead of ";". Use the
// shell_script.set_path call to handle these details.
// init_script.set_path call to handle these details.
let paths = value
.split(";")
.split(';')
.map(PathBuf::from)
.collect::<Vec<PathBuf>>();
shell_script
init_script
.set_path(&paths, PathModificationBehavior::Replace)
.into_diagnostic()?;
} else {
shell_script.set_env_var(key, value).into_diagnostic()?;
init_script.set_env_var(key, value).into_diagnostic()?;
}
}
init_file
.write_all(init_script.contents().into_diagnostic()?.as_bytes())
.into_diagnostic()?;

// the env file is consumed by the actual shell
let mut env_file = tempfile::Builder::new()
.prefix("pixi_env_")
.suffix(&format!(".{}", shell.extension()))
.rand_bytes(3)
.tempfile()
.into_diagnostic()?;

// Write custom prompt to the env file
env_file.write_all(prompt.as_bytes()).into_diagnostic()?;

// Write code to bootstrap the actual shell we want to start
if source_shell_completions {
if let Some(completions_dir) = bash.completion_script_location() {
shell_script
let mut env_script = ShellScript::new(shell, Platform::current());
if let Some(completions_dir) = shell.completion_script_location() {
env_script
.source_completions(&prefix.root().join(completions_dir))
.into_diagnostic()?;
}
env_file
.write_all(env_script.contents().into_diagnostic()?.as_bytes())
.into_diagnostic()?;
}
temp_file
.write_all(shell_script.contents().into_diagnostic()?.as_bytes())

env_file.flush().into_diagnostic()?;
let env_file_path = env_file.into_temp_path();
let exec_str = exec_format(&env_file_path);

init_file
.write_all(exec_str.as_bytes())
.into_diagnostic()?;

// Write custom prompt to the env file
temp_file.write_all(prompt.as_bytes()).into_diagnostic()?;
temp_file.flush().into_diagnostic()?;
init_file.flush().into_diagnostic()?;

// close the file handle, but keep the path (needed for Windows)
let temp_path = temp_file.into_temp_path();
let init_file_path = init_file.into_temp_path();

let bash_path = which(bash.executable()).into_diagnostic()?;
let bash_path = which(Bash.executable()).into_diagnostic()?;
let mut command = std::process::Command::new(bash_path);
command.arg("--init-file");
command.arg(&temp_path);
command.arg(&init_file_path);
command.arg("-i");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if you could leave the bash --init-file logic the same as before, and switch this -i into -c fish -i when the shell is fish?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that initially and, unfortunately, it didn't work. While fish inherits the environment variables from the parent bash, it can't inherit the functions (i.e. prompt and completions) because those are shell-specific. So either that would start fish without the prompt or shell completion (if we always use those for bash regardless of shell) or would fail completely (because fish function syntax is incompatible with bash).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it, we could probably just skip completions and prompt for zsh for now (or for fish as well, if we want to make this code simpler) and possibly tell the user how to add these manually by editing their .zshrc / config.fish.


ignore_ctrl_c();
Expand Down Expand Up @@ -392,7 +418,24 @@ pub async fn execute(args: Args) -> miette::Result<()> {
ShellEnum::PowerShell(pwsh) => start_powershell(pwsh, env, prompt_hook),
ShellEnum::CmdExe(cmdexe) => start_cmdexe(cmdexe, env, prompt_hook),
ShellEnum::Bash(bash) => {
start_winbash(bash, env, prompt_hook, &prefix, source_shell_completions)
start_shell_using_winbash(
bash,
|env_file| format!(". \"{}\"", env_file.as_os_str().to_string_lossy()),
env,
prompt_hook,
&prefix,
source_shell_completions,
)
}
ShellEnum::Fish(fish) => {
start_shell_using_winbash(
fish,
|env_file| format!("exec fish -i -C \". \\\"{}\\\"\"", env_file.as_os_str().to_string_lossy()),
env,
prompt_hook,
&prefix,
source_shell_completions,
)
}
_ => {
miette::bail!("Unsupported shell: {:?}", interactive_shell);
Expand Down Expand Up @@ -463,3 +506,4 @@ pub async fn execute(args: Args) -> miette::Result<()> {
}
}
}

Loading