#![cfg_attr(not(windows), allow(dead_code))]
use std::path::Path;
use std::process::Command;
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WindowsShell {
Pwsh,
Powershell,
Cmd,
}
impl WindowsShell {
pub(crate) fn binary(self) -> &'static str {
match self {
WindowsShell::Pwsh => "pwsh.exe",
WindowsShell::Powershell => "powershell.exe",
WindowsShell::Cmd => "cmd.exe",
}
}
pub(crate) fn args(self, command: &str) -> Vec<&str> {
match self {
WindowsShell::Pwsh | WindowsShell::Powershell => vec![
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
command,
],
WindowsShell::Cmd => vec!["/D", "/C", command],
}
}
pub(crate) fn command(self, command: &str) -> Command {
let mut cmd = Command::new(self.binary());
cmd.args(self.args(command));
cmd
}
#[allow(dead_code)]
pub(crate) fn bg_command(self, wrapper: &str) -> Command {
let mut cmd = Command::new(self.binary());
match self {
WindowsShell::Pwsh | WindowsShell::Powershell => {
cmd.args(self.args(wrapper));
}
WindowsShell::Cmd => {
cmd.args(["/V:ON", "/D", "/S", "/C", wrapper]);
}
}
cmd
}
pub(crate) fn wrapper_script(self, command: &str, exit_path: &Path) -> String {
match self {
WindowsShell::Pwsh | WindowsShell::Powershell => {
let exit_path = powershell_single_quote(&exit_path.display().to_string());
let command = powershell_single_quote(command);
format!(
concat!(
"$exitPath = {exit_path}; ",
"$tmpPath = $exitPath + '.tmp.' + $PID; ",
"$global:LASTEXITCODE = $null; ",
"Invoke-Expression {command}; ",
"$success = $?; ",
"$nativeCode = $global:LASTEXITCODE; ",
"if ($null -ne $nativeCode) {{ $code = [int]$nativeCode }} ",
"elseif ($success) {{ $code = 0 }} ",
"else {{ $code = 1 }}; ",
"[System.IO.File]::WriteAllText($tmpPath, [string]$code); ",
"Move-Item -LiteralPath $tmpPath -Destination $exitPath -Force; ",
"exit $code"
),
exit_path = exit_path,
command = command
)
}
WindowsShell::Cmd => {
let tmp_path = format!("{}.tmp", exit_path.display());
format!(
"{command} & echo !ERRORLEVEL! > {tmp} & move /Y {tmp} {exit} > nul",
command = command,
tmp = cmd_quote(&tmp_path),
exit = cmd_quote(&exit_path.display().to_string())
)
}
}
}
}
#[allow(dead_code)]
pub(crate) fn resolve_windows_shell() -> WindowsShell {
shell_candidates()
.first()
.copied()
.unwrap_or(WindowsShell::Cmd)
}
pub(crate) fn shell_candidates() -> Vec<WindowsShell> {
static CACHED: OnceLock<Vec<WindowsShell>> = OnceLock::new();
CACHED
.get_or_init(|| shell_candidates_with(|binary| which::which(binary).is_ok()))
.clone()
}
pub(crate) fn shell_candidates_with<F>(exists: F) -> Vec<WindowsShell>
where
F: Fn(&str) -> bool,
{
let mut candidates = Vec::with_capacity(3);
if exists("pwsh.exe") {
log::info!("[aft] bash candidate: pwsh.exe (PowerShell 7+; supports && pipeline operator)");
candidates.push(WindowsShell::Pwsh);
}
if exists("powershell.exe") {
log::info!(
"[aft] bash candidate: powershell.exe (Windows PowerShell 5.1; && in pipelines unsupported, will surface as parse error)"
);
candidates.push(WindowsShell::Powershell);
}
candidates.push(WindowsShell::Cmd);
if candidates.len() == 1 {
log::warn!(
"[aft] PowerShell (pwsh.exe / powershell.exe) is not reachable from \
this aft process — using cmd.exe only. This can occur even \
when PowerShell is installed if PATH inheritance is restricted, \
antivirus / AppLocker / Defender ASR rules block PowerShell as a \
child process, or you're on a stripped Windows SKU. Bash-style \
commands using && and || still work; PowerShell-only cmdlets will \
not. Details: https://github.com/cortexkit/aft/issues/27"
);
}
candidates
}
#[allow(dead_code)] pub(crate) fn resolve_windows_shell_with<F>(exists: F) -> WindowsShell
where
F: Fn(&str) -> bool,
{
let mut candidates = shell_candidates_with(exists);
candidates.remove(0)
}
fn powershell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
#[cfg_attr(not(windows), allow(dead_code))]
fn cmd_quote(value: &str) -> String {
format!("\"{}\"", value.replace('"', "\"\""))
}