use std::collections::HashMap;
use std::path::Path;
use std::process::Stdio;
use tokio::process::Command;
#[cfg(windows)]
pub(crate) fn shell_escape(arg: &str) -> String {
if arg.is_empty() {
return "\"\"".to_string();
}
if arg.chars().all(|c| {
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | '\\' | ':' | '=' | '@')
}) {
return arg.to_string();
}
format!("\"{}\"", arg.replace('"', "\\\""))
}
#[cfg(not(windows))]
pub(crate) fn shell_escape(arg: &str) -> String {
if arg.is_empty() {
return "''".to_string();
}
if arg
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':' | '=' | '@'))
{
return arg.to_string();
}
format!("'{}'", arg.replace('\'', "'\\''"))
}
pub fn wrap_command_with_toolchain(
cmd_str: &str,
toolchain: Option<&HashMap<String, String>>,
) -> String {
let Some(toolchain) = toolchain else {
return cmd_str.to_string();
};
if toolchain.is_empty() {
return cmd_str.to_string();
}
#[cfg(not(unix))]
{
let _ = toolchain;
cmd_str.to_string()
}
#[cfg(unix)]
{
let mut prefixes: Vec<String> = Vec::new();
for (lang, ver) in toolchain {
let l = lang.to_ascii_lowercase();
match l.as_str() {
"node" | "nodejs" => {
prefixes.push(format!(
"(export NVM_DIR=\"$HOME/.nvm\"; [ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\" && nvm use {} >/dev/null 2>&1) || (which fnm >/dev/null 2>&1 && eval \"$(fnm env)\" && fnm use {} >/dev/null 2>&1) || true",
ver, ver
));
}
"go" | "golang" => {
prefixes.push(format!(
"(which goenv >/dev/null 2>&1 && export GOENV_VERSION={} && eval \"$(goenv init -)\") || true",
ver
));
}
"python" | "pyenv" => {
prefixes.push(
"(which pyenv >/dev/null 2>&1 && eval \"$(pyenv init -)\") || true"
.to_string(),
);
}
_ => {}
}
}
if prefixes.is_empty() {
cmd_str.to_string()
} else {
format!("{} && {}", prefixes.join(" && "), cmd_str)
}
}
}
pub fn apply_toolchain_env(cmd: &mut Command, toolchain: Option<&HashMap<String, String>>) {
if let Some(tc) = toolchain {
for (lang, ver) in tc {
let l = lang.to_ascii_lowercase();
match l.as_str() {
"rust" | "rustup" => {
cmd.env("RUSTUP_TOOLCHAIN", ver);
}
"python" | "pyenv" => {
cmd.env("PYENV_VERSION", ver);
}
"node" | "nodejs" => {
cmd.env("NODE_VERSION", ver);
}
_ => {}
}
let env_key = format!("FARHAND_TOOLCHAIN_{}", l.to_ascii_uppercase());
cmd.env(env_key, ver);
}
}
}
pub fn apply_toolchain_pty(
cmd_builder: &mut portable_pty::CommandBuilder,
toolchain: Option<&HashMap<String, String>>,
) {
if let Some(tc) = toolchain {
for (lang, ver) in tc {
let l = lang.to_ascii_lowercase();
match l.as_str() {
"rust" | "rustup" => {
cmd_builder.env("RUSTUP_TOOLCHAIN", ver);
}
"python" | "pyenv" => {
cmd_builder.env("PYENV_VERSION", ver);
}
"node" | "nodejs" => {
cmd_builder.env("NODE_VERSION", ver);
}
_ => {}
}
let env_key = format!("FARHAND_TOOLCHAIN_{}", l.to_ascii_uppercase());
cmd_builder.env(env_key, ver);
}
}
}
pub fn parse_custom_shell(shell: &str) -> Option<Vec<String>> {
let tokens: Vec<String> = shell.split_whitespace().map(str::to_string).collect();
if tokens.is_empty() {
None
} else {
Some(tokens)
}
}
pub fn build_shell_command(
cwd: &Path,
argv: &[String],
custom_shell: Option<&str>,
toolchain: Option<&HashMap<String, String>>,
) -> Command {
let joined_cmd = argv
.iter()
.map(|a| shell_escape(a))
.collect::<Vec<_>>()
.join(" ");
let wrapped_cmd = wrap_command_with_toolchain(&joined_cmd, toolchain);
let mut cmd = if let Some(shell_override) = custom_shell {
let shell_tokens = parse_custom_shell(shell_override)
.unwrap_or_else(|| vec!["/bin/sh".to_string(), "-c".to_string()]);
let mut c = Command::new(&shell_tokens[0]);
for part in &shell_tokens[1..] {
c.arg(part);
}
c.arg(&wrapped_cmd);
c
} else if cfg!(windows) {
let mut c = Command::new("cmd.exe");
#[cfg(windows)]
c.raw_arg(format!("/C \"{}\"", wrapped_cmd));
#[cfg(not(windows))]
c.arg("/C").arg(&wrapped_cmd);
c
} else {
let mut c = Command::new("/bin/sh");
c.arg("-c").arg(&wrapped_cmd);
c
};
cmd.current_dir(cwd);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
#[cfg(unix)]
cmd.process_group(0);
cmd
}
pub fn build_raw_shell_command(
cwd: &Path,
raw_cmd: &str,
custom_shell: Option<&str>,
toolchain: Option<&HashMap<String, String>>,
) -> Command {
let wrapped_cmd = wrap_command_with_toolchain(raw_cmd, toolchain);
let mut cmd = if let Some(shell_override) = custom_shell {
let shell_tokens = parse_custom_shell(shell_override)
.unwrap_or_else(|| vec!["/bin/sh".to_string(), "-c".to_string()]);
let mut c = Command::new(&shell_tokens[0]);
for part in &shell_tokens[1..] {
c.arg(part);
}
c.arg(&wrapped_cmd);
c
} else if cfg!(windows) {
let mut c = Command::new("cmd.exe");
#[cfg(windows)]
c.raw_arg(format!("/C \"{}\"", wrapped_cmd));
#[cfg(not(windows))]
c.arg("/C").arg(&wrapped_cmd);
c
} else {
let mut c = Command::new("/bin/sh");
c.arg("-c").arg(&wrapped_cmd);
c
};
cmd.current_dir(cwd);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
#[cfg(unix)]
cmd.process_group(0);
cmd
}
#[allow(unsafe_code)] pub async fn kill_process_group(child: &mut tokio::process::Child) {
#[cfg(unix)]
if let Some(pid) = child.id() {
unsafe {
libc::kill(-(pid as i32), libc::SIGTERM);
}
tokio::select! {
_ = child.wait() => {}
_ = tokio::time::sleep(std::time::Duration::from_secs(3)) => {
unsafe {
libc::kill(-(pid as i32), libc::SIGKILL);
}
}
}
}
#[cfg(windows)]
if let Some(pid) = child.id() {
let _ = tokio::process::Command::new("taskkill")
.args(["/F", "/T", "/PID", &pid.to_string()])
.output()
.await;
let _ = child.kill().await;
}
#[cfg(not(any(unix, windows)))]
{
let _ = child.kill().await;
}
}
pub fn resolve_shell_executable(requested: &str) -> String {
if requested != "$SHELL" && !requested.is_empty() {
if Path::new(requested).is_file() {
return requested.to_string();
}
if !requested.contains('/') && !requested.contains('\\') {
for dir in &["/bin", "/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"] {
let candidate = Path::new(dir).join(requested);
if candidate.is_file() {
return candidate.to_string_lossy().to_string();
}
}
}
}
if let Ok(sh) = std::env::var("SHELL") {
if Path::new(&sh).is_file() {
return sh;
}
}
for candidate in &[
"/bin/zsh",
"/bin/bash",
"/usr/bin/zsh",
"/usr/bin/bash",
"/bin/sh",
] {
if Path::new(candidate).is_file() {
return candidate.to_string();
}
}
#[cfg(windows)]
{
"powershell.exe".to_string()
}
#[cfg(not(windows))]
{
"/bin/sh".to_string()
}
}