use std::{
borrow::Cow,
collections::HashMap,
ffi::OsStr,
fmt::Write,
path::{Path, PathBuf},
process::Command,
str::FromStr,
};
use enum_dispatch::enum_dispatch;
use indexmap::IndexMap;
use itertools::Itertools;
use rattler_conda_types::Platform;
use thiserror::Error;
use crate::activation::PathModificationBehavior;
#[enum_dispatch(ShellEnum)]
pub trait Shell {
fn force_utf8(&self, _f: &mut impl Write) -> ShellResult {
Ok(())
}
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult;
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult;
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult;
fn source_completions(&self, _f: &mut impl Write, _completions_dir: &Path) -> ShellResult {
Ok(())
}
fn can_run_script(&self, path: &Path) -> bool {
path.is_file()
&& path
.extension()
.and_then(OsStr::to_str)
.is_some_and(|ext| ext == self.extension())
}
fn run_command<'a>(
&self,
f: &mut impl Write,
command: impl IntoIterator<Item = &'a str> + 'a,
) -> std::fmt::Result {
writeln!(f, "{}", command.into_iter().join(" "))
}
fn set_path(
&self,
f: &mut impl Write,
paths: &[PathBuf],
modification_behavior: PathModificationBehavior,
platform: &Platform,
) -> ShellResult {
let mut paths_vec = paths
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect_vec();
let path_var = self.path_var(platform);
match modification_behavior {
PathModificationBehavior::Replace => (),
PathModificationBehavior::Append => paths_vec.insert(0, self.format_env_var(path_var)),
PathModificationBehavior::Prepend => paths_vec.push(self.format_env_var(path_var)),
}
let paths_string = paths_vec.join(self.path_separator(platform));
self.set_env_var(f, self.path_var(platform), paths_string.as_str())
}
fn extension(&self) -> &str;
fn executable(&self) -> &str;
fn create_run_script_command(&self, path: &Path) -> Command;
fn path_separator(&self, platform: &Platform) -> &str {
if platform.is_unix() {
":"
} else {
";"
}
}
fn path_var(&self, platform: &Platform) -> &str {
if platform.is_windows() {
"Path"
} else {
"PATH"
}
}
fn format_env_var(&self, var_name: &str) -> String {
format!("${{{var_name}}}")
}
fn echo(&self, f: &mut impl Write, text: &str) -> std::fmt::Result {
writeln!(f, "echo {}", shlex::try_quote(text).unwrap_or_default())
}
fn print_env(&self, f: &mut impl Write) -> std::fmt::Result {
writeln!(f, "/usr/bin/env")
}
fn write_script(&self, f: &mut impl std::io::Write, script: &str) -> std::io::Result<()> {
f.write_all(script.as_bytes())
}
fn parse_env<'i>(&self, env: &'i str) -> HashMap<&'i str, &'i str> {
env.lines()
.filter_map(|line| {
line.split_once('=')
.map(|(key, value)| (key, value.trim_matches('"')))
})
.collect()
}
fn line_ending(&self) -> &str {
"\n"
}
fn completion_script_location(&self) -> Option<&'static Path> {
None
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
self.unset_env_var(f, backup_key)?;
self.unset_env_var(f, key)
}
}
pub(crate) fn native_path_to_unix(path: &str) -> Result<String, std::io::Error> {
let output = Command::new("cygpath")
.arg("--unix")
.arg("--path")
.arg(path)
.output();
match output {
Ok(output) if output.status.success() => Ok(String::from_utf8(output.stdout)
.map_err(|_err| std::io::Error::other("failed to convert path to Unix style"))?
.trim()
.to_string()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(e),
Err(e) => Err(std::io::Error::other(format!(
"failed to convert path to Unix style: {e}"
))),
_ => Err(std::io::Error::other(
"failed to convert path to Unix style: cygpath failed",
)),
}
}
#[derive(Debug, Error)]
pub enum ShellError {
#[error("Invalid environment variable name '{0}': {1}")]
InvalidName(String, &'static str),
#[error("Invalid environment variable value for '{0}': {1}")]
InvalidValue(String, &'static str),
#[error("Could not format with std::fmt::Error")]
FmtError(#[from] std::fmt::Error),
}
fn validate_env_var_name(name: &str) -> Result<(), ShellError> {
if name.is_empty() {
return Err(ShellError::InvalidName(
name.to_string(),
"name cannot be empty",
));
}
for ch in name.chars() {
if ch.is_control() || ch == '=' {
return Err(ShellError::InvalidName(
name.to_string(),
"name cannot contain control characters or '='",
));
}
}
Ok(())
}
type ShellResult = Result<(), ShellError>;
#[derive(Debug, Clone, Copy, Default)]
pub struct Bash;
impl Shell for Bash {
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult {
validate_env_var_name(env_var)?;
if value.contains('$') {
let escaped_value = value.replace('"', "\\\"");
Ok(writeln!(f, "export {env_var}=\"{escaped_value}\"")?)
} else {
let quoted_value = shlex::try_quote(value).unwrap_or_else(|_| value.into());
Ok(writeln!(f, "export {env_var}={quoted_value}")?)
}
}
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult {
validate_env_var_name(env_var)?;
writeln!(f, "unset {env_var}")?;
Ok(())
}
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult {
let lossy_path = path.to_string_lossy();
let quoted_path = shlex::try_quote(&lossy_path).unwrap_or_default();
Ok(writeln!(f, ". {quoted_path}")?)
}
fn set_path(
&self,
f: &mut impl Write,
paths: &[PathBuf],
modification_behavior: PathModificationBehavior,
platform: &Platform,
) -> ShellResult {
let paths_vec = paths
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect_vec();
let paths_string = if cfg!(windows) {
match native_path_to_unix(&paths_vec.join(";")) {
Ok(path) => path,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
paths_vec.join(":")
}
Err(e) => panic!("{e}"),
}
} else {
paths_vec.join(":")
};
let path_var = self.path_var(platform);
let combined_paths_string: String = match modification_behavior {
PathModificationBehavior::Replace => paths_string,
PathModificationBehavior::Prepend => {
format!("{paths_string}:{}", &self.format_env_var(path_var))
}
PathModificationBehavior::Append => {
format!("{}:{paths_string}", self.format_env_var(path_var))
}
};
Ok(writeln!(
f,
"export {path_var}=\"{combined_paths_string}\""
)?)
}
fn path_var(&self, _platform: &Platform) -> &str {
"PATH"
}
fn extension(&self) -> &str {
"sh"
}
fn completion_script_location(&self) -> Option<&'static Path> {
Some(Path::new("share/bash-completion/completions"))
}
fn source_completions(&self, f: &mut impl Write, completions_dir: &Path) -> ShellResult {
if completions_dir.exists() {
let completions_dir_str = if cfg!(windows) {
match native_path_to_unix(completions_dir.to_string_lossy().as_ref()) {
Ok(path) => path,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
completions_dir.to_string_lossy().to_string()
}
Err(e) => panic!("{e}"),
}
} else {
completions_dir.to_string_lossy().to_string()
};
writeln!(f, "source {completions_dir_str}/*")?;
}
Ok(())
}
fn executable(&self) -> &str {
"bash"
}
fn create_run_script_command(&self, path: &Path) -> Command {
let mut cmd = Command::new(self.executable());
if cfg!(windows) {
cmd.arg(native_path_to_unix(path.to_str().unwrap()).unwrap());
} else {
cmd.arg(path);
}
cmd
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
validate_env_var_name(key)?;
validate_env_var_name(backup_key)?;
Ok(writeln!(
f,
r#"if [ -n "${{{backup_key}:-}}" ]; then
{key}="${{{backup_key}}}"
unset {backup_key}
else
unset {key}
fi"#
)?)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Zsh;
impl Shell for Zsh {
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "export {env_var}=\"{value}\"")?)
}
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "unset {env_var}")?)
}
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult {
Ok(writeln!(f, ". \"{}\"", path.to_string_lossy())?)
}
fn extension(&self) -> &str {
"sh"
}
fn executable(&self) -> &str {
"zsh"
}
fn create_run_script_command(&self, path: &Path) -> Command {
let mut cmd = Command::new(self.executable());
cmd.arg(path);
cmd
}
fn completion_script_location(&self) -> Option<&'static Path> {
Some(Path::new("share/zsh/site-functions"))
}
fn source_completions(&self, f: &mut impl Write, completions_dir: &Path) -> ShellResult {
if completions_dir.exists() {
writeln!(f, "fpath+=({})", completions_dir.to_string_lossy())?;
writeln!(f, "autoload -Uz compinit")?;
writeln!(f, "compinit")?;
}
Ok(())
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
validate_env_var_name(key)?;
validate_env_var_name(backup_key)?;
Ok(writeln!(
f,
r#"if [ -n "${{{backup_key}:-}}" ]; then
{key}="${{{backup_key}}}"
unset {backup_key}
else
unset {key}
fi"#
)?)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Xonsh;
impl Shell for Xonsh {
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "${env_var} = \"{value}\"")?)
}
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "del ${env_var}")?)
}
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult {
let ext = path.extension().and_then(OsStr::to_str);
let cmd = match ext {
Some("sh") => "source-bash",
_ => "source",
};
Ok(writeln!(f, "{} \"{}\"", cmd, path.to_string_lossy())?)
}
fn can_run_script(&self, path: &Path) -> bool {
path.is_file()
&& path
.extension()
.and_then(OsStr::to_str)
.is_some_and(|ext| ext == "xsh" || ext == "sh")
}
fn extension(&self) -> &str {
"xsh"
}
fn executable(&self) -> &str {
"xonsh"
}
fn create_run_script_command(&self, path: &Path) -> Command {
let mut cmd = Command::new(self.executable());
cmd.arg(path);
cmd
}
fn completion_script_location(&self) -> Option<&'static Path> {
None
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
validate_env_var_name(key)?;
validate_env_var_name(backup_key)?;
Ok(writeln!(
f,
r#"if {backup_key} in $env:
$env[{key}] = $env[{backup_key}]
del $env[{backup_key}]
else:
del $env[{key}]"#
)?)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CmdExe;
impl Shell for CmdExe {
fn force_utf8(&self, f: &mut impl Write) -> ShellResult {
Ok(writeln!(f, "@chcp 65001 > nul")?)
}
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "@SET \"{env_var}={value}\"")?)
}
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "@SET {env_var}=")?)
}
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult {
Ok(writeln!(f, "@CALL \"{}\"", path.to_string_lossy())?)
}
fn run_command<'a>(
&self,
f: &mut impl Write,
command: impl IntoIterator<Item = &'a str> + 'a,
) -> std::fmt::Result {
writeln!(f, "@{}", command.into_iter().join(" "))
}
fn extension(&self) -> &str {
"bat"
}
fn executable(&self) -> &str {
"cmd.exe"
}
fn create_run_script_command(&self, path: &Path) -> Command {
let mut cmd = Command::new(self.executable());
cmd.arg("/D").arg("/C").arg(path);
cmd
}
fn format_env_var(&self, var_name: &str) -> String {
format!("%{var_name}%")
}
fn echo(&self, f: &mut impl Write, text: &str) -> std::fmt::Result {
write!(f, "@ECHO ",)?;
let mut text = text;
while let Some(idx) = text.find(['^', '&', '|', '\\', '<', '>']) {
write!(f, "{}^{}", &text[..idx], &text[idx..idx + 1])?;
text = &text[idx + 1..];
}
writeln!(f, "{text}")
}
fn write_script(&self, f: &mut impl std::io::Write, script: &str) -> std::io::Result<()> {
let script = script.replace('\n', "\r\n");
f.write_all(script.as_bytes())
}
fn print_env(&self, f: &mut impl Write) -> std::fmt::Result {
writeln!(f, "@SET")
}
fn line_ending(&self) -> &str {
"\r\n"
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
validate_env_var_name(key)?;
validate_env_var_name(backup_key)?;
Ok(writeln!(
f,
r#"if defined {backup_key} (
set "{key}=%{backup_key}%"
set "{backup_key}="
) else (
set "{key}="
)"#
)?)
}
}
#[derive(Debug, Clone)]
pub struct PowerShell {
executable_path: String,
}
impl Default for PowerShell {
fn default() -> Self {
let test_powershell = Command::new("pwsh").arg("-v").output().is_ok();
let exe = if test_powershell {
"pwsh"
} else {
"powershell"
};
PowerShell {
executable_path: exe.to_string(),
}
}
}
impl Shell for PowerShell {
fn force_utf8(&self, f: &mut impl Write) -> ShellResult {
Ok(writeln!(
f,
"$OutputEncoding = [System.Console]::OutputEncoding = [System.Console]::InputEncoding = [System.Text.Encoding]::UTF8"
)?)
}
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "${{Env:{env_var}}} = \"{value}\"")?)
}
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "${{Env:{env_var}}}=\"\"")?)
}
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult {
Ok(writeln!(f, ". \"{}\"", path.to_string_lossy())?)
}
fn extension(&self) -> &str {
"ps1"
}
fn executable(&self) -> &str {
&self.executable_path
}
fn create_run_script_command(&self, path: &Path) -> Command {
let mut cmd = Command::new(self.executable());
cmd.arg(path);
cmd
}
fn format_env_var(&self, var_name: &str) -> String {
format!("$Env:{var_name}")
}
fn print_env(&self, f: &mut impl Write) -> std::fmt::Result {
writeln!(f, r##"dir env: | %{{"{{0}}={{1}}" -f $_.Name,$_.Value}}"##)
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
validate_env_var_name(key)?;
validate_env_var_name(backup_key)?;
Ok(writeln!(
f,
r#"if (Test-Path env:{backup_key}) {{
$env:{key} = $env:{backup_key}
Remove-Item env:{backup_key}
}} else {{
Remove-Item env:{key} -ErrorAction SilentlyContinue
}}"#
)?)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Fish;
impl Shell for Fish {
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "set -gx {env_var} \"{value}\"")?)
}
fn format_env_var(&self, var_name: &str) -> String {
format!("${var_name}")
}
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "set -e {env_var}")?)
}
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult {
Ok(writeln!(f, "source \"{}\"", path.to_string_lossy())?)
}
fn extension(&self) -> &str {
"fish"
}
fn executable(&self) -> &str {
"fish"
}
fn create_run_script_command(&self, path: &Path) -> Command {
let mut cmd = Command::new(self.executable());
cmd.arg(path);
cmd
}
fn completion_script_location(&self) -> Option<&'static Path> {
Some(Path::new("share/fish/vendor_completions.d"))
}
fn source_completions(&self, f: &mut impl Write, completions_dir: &Path) -> ShellResult {
if completions_dir.exists() {
let completions_glob = completions_dir.join("*");
writeln!(f, "for file in {}", completions_glob.to_string_lossy())?;
writeln!(f, " source $file")?;
writeln!(f, "end")?;
}
Ok(())
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
validate_env_var_name(key)?;
validate_env_var_name(backup_key)?;
Ok(writeln!(
f,
r#"if set -q {backup_key}
set -gx {key} ${backup_key}
set -e {backup_key}
else
set -e {key}
end"#
)?)
}
}
fn escape_backslashes(s: &str) -> String {
s.replace('\\', "\\\\")
}
fn quote_if_required(s: &str) -> Cow<'_, str> {
if s.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-') {
Cow::Owned(format!("\"{s}\""))
} else {
Cow::Borrowed(s)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NuShell;
impl Shell for NuShell {
fn set_env_var(&self, f: &mut impl Write, env_var: &str, value: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(
f,
"$env.{} = \"{}\"",
quote_if_required(env_var),
escape_backslashes(value)
)?)
}
fn unset_env_var(&self, f: &mut impl Write, env_var: &str) -> ShellResult {
validate_env_var_name(env_var)?;
Ok(writeln!(f, "hide-env {}", quote_if_required(env_var))?)
}
fn run_script(&self, f: &mut impl Write, path: &Path) -> ShellResult {
Ok(writeln!(f, "source-env \"{}\"", path.to_string_lossy())?)
}
fn set_path(
&self,
f: &mut impl Write,
paths: &[PathBuf],
modification_behavior: PathModificationBehavior,
platform: &Platform,
) -> ShellResult {
let path = paths
.iter()
.map(|path| escape_backslashes(&format!("\"{}\"", path.to_string_lossy().into_owned())))
.join(", ");
let path_var = self.path_var(platform);
match modification_behavior {
PathModificationBehavior::Replace => Ok(writeln!(f, "$env.{path_var} = [{path}]",)?),
PathModificationBehavior::Prepend => Ok(writeln!(
f,
"$env.{path_var} = ($env.{path_var} | prepend [{path}])"
)?),
PathModificationBehavior::Append => Ok(writeln!(
f,
"$env.{path_var} = ($env.{path_var} | append [{path}])"
)?),
}
}
fn extension(&self) -> &str {
"nu"
}
fn executable(&self) -> &str {
"nu"
}
fn create_run_script_command(&self, path: &Path) -> Command {
let mut cmd = Command::new(self.executable());
cmd.arg(path);
cmd
}
fn completion_script_location(&self) -> Option<&'static Path> {
None
}
fn restore_env_var(&self, f: &mut impl Write, key: &str, backup_key: &str) -> ShellResult {
validate_env_var_name(key)?;
validate_env_var_name(backup_key)?;
Ok(writeln!(
f,
r#"if ($env | get {backup_key}?) {{
$env.{key} = $env.{backup_key}
$env = $env | reject {backup_key}
}} else {{
$env = $env | reject {key}
}}"#
)?)
}
}
#[enum_dispatch]
#[allow(missing_docs)]
#[derive(Clone, Debug)]
pub enum ShellEnum {
Bash,
Zsh,
Xonsh,
CmdExe,
PowerShell,
Fish,
NuShell,
}
impl Default for ShellEnum {
fn default() -> Self {
if cfg!(windows) {
CmdExe.into()
} else {
Bash.into()
}
}
}
impl ShellEnum {
pub fn from_shell_path<P: AsRef<Path>>(path: P) -> Option<Self> {
parse_shell_from_path(path.as_ref())
}
pub fn from_env() -> Option<Self> {
if let Some(env_shell) = std::env::var_os("SHELL") {
Self::from_shell_path(env_shell)
} else if cfg!(windows) {
Some(PowerShell::default().into())
} else {
None
}
}
#[cfg(feature = "sysinfo")]
pub fn from_parent_process() -> Option<Self> {
use sysinfo::get_current_pid;
let mut system_info = sysinfo::System::new();
let mut current_pid = get_current_pid().ok()?;
system_info.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[current_pid]), true);
while let Some(parent_process_id) = system_info
.process(current_pid)
.and_then(sysinfo::Process::parent)
{
system_info
.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[parent_process_id]), true);
let parent_process = system_info.process(parent_process_id)?;
let parent_process_name = parent_process.name().to_string_lossy().to_lowercase();
let shell: Option<ShellEnum> = if parent_process_name.contains("bash") {
Some(Bash.into())
} else if parent_process_name.contains("zsh") {
Some(Zsh.into())
} else if parent_process_name.contains("xonsh")
|| (parent_process_name.contains("python")
&& parent_process
.cmd().iter()
.any(|arg| arg.to_string_lossy().contains("xonsh")))
{
Some(Xonsh.into())
} else if parent_process_name.contains("fish") {
Some(Fish.into())
} else if parent_process_name.contains("nu") {
Some(NuShell.into())
} else if parent_process_name.contains("powershell")
|| parent_process_name.contains("pwsh")
{
Some(
PowerShell {
executable_path: parent_process_name.clone(),
}
.into(),
)
} else if parent_process_name.contains("cmd.exe") {
Some(CmdExe.into())
} else {
None
};
if let Some(shell) = shell {
tracing::debug!(
"Guessing the current shell is {}. Parent process name: {} and args: {:?}",
&shell.executable(),
&parent_process_name,
&parent_process.cmd()
);
return Some(shell);
}
current_pid = parent_process_id;
}
None
}
}
#[derive(Debug, Error)]
#[error("{0}")]
pub struct ParseShellEnumError(String);
impl FromStr for ShellEnum {
type Err = ParseShellEnumError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"bash" => Ok(Bash.into()),
"zsh" => Ok(Zsh.into()),
"xonsh" => Ok(Xonsh.into()),
"fish" => Ok(Fish.into()),
"cmd" => Ok(CmdExe.into()),
"nu" | "nushell" => Ok(NuShell.into()),
"powershell" | "powershell_ise" => Ok(PowerShell::default().into()),
_ => Err(ParseShellEnumError(format!(
"'{s}' is an unknown shell variant"
))),
}
}
}
fn parse_shell_from_path(path: &Path) -> Option<ShellEnum> {
let name = path.file_stem()?.to_str()?;
ShellEnum::from_str(name).ok()
}
pub struct ShellScript<T: Shell> {
shell: T,
contents: String,
platform: Platform,
}
impl<T: Shell + 'static> ShellScript<T> {
pub fn new(shell: T, platform: Platform) -> Self {
Self {
shell,
contents: String::new(),
platform,
}
}
pub fn apply_env_vars_with_backup(
&mut self,
current_env: &HashMap<String, String>,
new_shlvl: i32,
envs: &IndexMap<String, String>,
) -> Result<&mut Self, ShellError> {
for (key, value) in envs {
if let Some(existing_value) = current_env.get(key) {
self.set_env_var(
&format!("CONDA_ENV_SHLVL_{new_shlvl}_{key}"),
existing_value,
)?;
}
self.set_env_var(key, value)?;
}
Ok(self)
}
pub fn set_env_var(&mut self, env_var: &str, value: &str) -> Result<&mut Self, ShellError> {
self.shell.set_env_var(&mut self.contents, env_var, value)?;
Ok(self)
}
pub fn unset_env_var(&mut self, env_var: &str) -> Result<&mut Self, ShellError> {
self.shell.unset_env_var(&mut self.contents, env_var)?;
Ok(self)
}
pub fn set_path(
&mut self,
paths: &[PathBuf],
path_modification_behavior: PathModificationBehavior,
) -> Result<&mut Self, ShellError> {
self.shell.set_path(
&mut self.contents,
paths,
path_modification_behavior,
&self.platform,
)?;
Ok(self)
}
pub fn run_script(&mut self, path: &Path) -> Result<&mut Self, ShellError> {
self.shell.run_script(&mut self.contents, path)?;
Ok(self)
}
pub fn source_completions(&mut self, completions_dir: &Path) -> Result<&mut Self, ShellError> {
self.shell
.source_completions(&mut self.contents, completions_dir)?;
Ok(self)
}
pub fn append_script(&mut self, script: &Self) -> &mut Self {
self.contents.push('\n');
self.contents.push_str(&script.contents);
self
}
pub fn contents(&self) -> Result<String, ShellError> {
let mut final_contents = String::new();
self.shell.force_utf8(&mut final_contents)?;
final_contents.push_str(&self.contents);
if self.shell.line_ending() == "\n" {
Ok(final_contents)
} else {
Ok(final_contents.replace('\n', self.shell.line_ending()))
}
}
pub fn print_env(&mut self) -> Result<&mut Self, std::fmt::Error> {
self.shell.print_env(&mut self.contents)?;
Ok(self)
}
pub fn echo(&mut self, text: &str) -> Result<&mut Self, std::fmt::Error> {
self.shell.echo(&mut self.contents, text)?;
Ok(self)
}
pub fn restore_env_var(
&mut self,
key: &str,
backup_key: &str,
) -> Result<&mut Self, ShellError> {
self.shell
.restore_env_var(&mut self.contents, key, backup_key)?;
Ok(self)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
#[test]
fn test_bash() {
let mut script = ShellScript::new(Bash, Platform::Linux64);
let paths = vec![PathBuf::from("bar"), PathBuf::from("a/b")];
script
.set_env_var("FOO", "bar")
.unwrap()
.set_env_var("FOO2", "a b")
.unwrap()
.set_env_var("FOO3", "a\\b")
.unwrap()
.set_env_var("FOO4", "${UNEXPANDED_VAR}")
.unwrap()
.unset_env_var("FOO")
.unwrap()
.set_path(&paths, PathModificationBehavior::Append)
.unwrap()
.set_path(&paths, PathModificationBehavior::Prepend)
.unwrap()
.set_path(&paths, PathModificationBehavior::Replace)
.unwrap()
.run_script(&PathBuf::from_str("foo.sh").unwrap())
.unwrap()
.run_script(&PathBuf::from_str("a\\foo.sh").unwrap())
.unwrap();
insta::assert_snapshot!(script.contents);
}
#[test]
fn test_fish() {
let mut script = ShellScript::new(Fish, Platform::Linux64);
script
.set_env_var("FOO", "bar")
.unwrap()
.unset_env_var("FOO")
.unwrap()
.run_script(&PathBuf::from_str("foo.sh").expect("blah"))
.unwrap();
insta::assert_snapshot!(script.contents);
}
#[test]
fn test_xonsh_bash() {
let mut script = ShellScript::new(Xonsh, Platform::Linux64);
script
.run_script(&PathBuf::from_str("foo.sh").unwrap())
.unwrap();
insta::assert_snapshot!(script.contents);
}
#[test]
fn test_xonsh_xsh() {
let mut script = ShellScript::new(Xonsh, Platform::Linux64);
script
.set_env_var("FOO", "bar")
.unwrap()
.unset_env_var("FOO")
.unwrap()
.run_script(&PathBuf::from_str("foo.xsh").unwrap())
.unwrap();
insta::assert_snapshot!(script.contents);
}
#[cfg(feature = "sysinfo")]
#[test]
fn test_from_parent_process_doesnt_crash() {
let shell = ShellEnum::from_parent_process();
println!("Detected shell: {shell:?}");
}
#[test]
fn test_from_env() {
let shell = ShellEnum::from_env();
println!("Detected shell: {shell:?}");
}
#[test]
fn test_path_separator() {
let mut script = ShellScript::new(Bash, Platform::Linux64);
script
.set_path(
&[PathBuf::from("/foo"), PathBuf::from("/bar")],
PathModificationBehavior::Prepend,
)
.unwrap();
assert!(script.contents.contains("/foo:/bar"));
let mut script = ShellScript::new(Bash, Platform::Win64);
script
.set_path(
&[PathBuf::from("/foo"), PathBuf::from("/bar")],
PathModificationBehavior::Prepend,
)
.unwrap();
assert!(script.contents.contains("/foo:/bar"));
}
#[test]
fn test_env_var_name_validation() {
assert!(validate_env_var_name("PATH").is_ok());
assert!(validate_env_var_name("_PATH").is_ok());
assert!(validate_env_var_name("MY_VAR_123").is_ok());
assert!(validate_env_var_name("ProgramFiles(x86)").is_ok());
assert!(validate_env_var_name("").is_err());
assert!(validate_env_var_name("VAR=1").is_err());
assert!(validate_env_var_name("VAR\n").is_err());
assert!(validate_env_var_name("VAR\x00123").is_err());
assert!(validate_env_var_name("VAR\r123").is_err());
}
#[test]
fn test_parse_env() {
let script = ShellScript::new(CmdExe, Platform::Win64);
let input = "VAR1=\"value1\"\nNUM=1\nNUM2=\"2\"";
let parsed_env = script.shell.parse_env(input);
let expected_env: HashMap<&str, &str> =
vec![("VAR1", "value1"), ("NUM", "1"), ("NUM2", "2")]
.into_iter()
.collect();
assert_eq!(parsed_env, expected_env);
}
}