mirror of
https://github.com/openai/codex.git
synced 2026-04-29 00:55:38 +00:00
chore: rename codex-command to codex-shell-command (#11378)
This addresses some post-merge feedback on https://github.com/openai/codex/pull/11361: - crate rename - reuse `detect_shell_type()` utility
This commit is contained in:
204
codex-rs/shell-command/src/powershell.rs
Normal file
204
codex-rs/shell-command/src/powershell.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use crate::shell_detect::ShellType;
|
||||
use crate::shell_detect::detect_shell_type;
|
||||
|
||||
const POWERSHELL_FLAGS: &[&str] = &["-nologo", "-noprofile", "-command", "-c"];
|
||||
|
||||
/// Prefixed command for powershell shell calls to force UTF-8 console output.
|
||||
pub const UTF8_OUTPUT_PREFIX: &str = "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;\n";
|
||||
|
||||
pub fn prefix_powershell_script_with_utf8(command: &[String]) -> Vec<String> {
|
||||
let Some((_, script)) = extract_powershell_command(command) else {
|
||||
return command.to_vec();
|
||||
};
|
||||
|
||||
let trimmed = script.trim_start();
|
||||
let script = if trimmed.starts_with(UTF8_OUTPUT_PREFIX) {
|
||||
script.to_string()
|
||||
} else {
|
||||
format!("{UTF8_OUTPUT_PREFIX}{script}")
|
||||
};
|
||||
|
||||
let mut command: Vec<String> = command[..(command.len() - 1)]
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
command.push(script);
|
||||
command
|
||||
}
|
||||
|
||||
/// Extract the PowerShell script body from an invocation such as:
|
||||
///
|
||||
/// - ["pwsh", "-NoProfile", "-Command", "Get-ChildItem -Recurse | Select-String foo"]
|
||||
/// - ["powershell.exe", "-Command", "Write-Host hi"]
|
||||
/// - ["powershell", "-NoLogo", "-NoProfile", "-Command", "...script..."]
|
||||
///
|
||||
/// Returns (`shell`, `script`) when the first arg is a PowerShell executable and a
|
||||
/// `-Command` (or `-c`) flag is present followed by a script string.
|
||||
pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
|
||||
if command.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let shell = &command[0];
|
||||
if !matches!(
|
||||
detect_shell_type(&PathBuf::from(shell)),
|
||||
Some(ShellType::PowerShell)
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the first occurrence of -Command (accept common short alias -c as well)
|
||||
let mut i = 1usize;
|
||||
while i + 1 < command.len() {
|
||||
let flag = &command[i];
|
||||
// Reject unknown flags
|
||||
if !POWERSHELL_FLAGS.contains(&flag.to_ascii_lowercase().as_str()) {
|
||||
return None;
|
||||
}
|
||||
if flag.eq_ignore_ascii_case("-Command") || flag.eq_ignore_ascii_case("-c") {
|
||||
let script = &command[i + 1];
|
||||
return Some((shell, script));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// This function attempts to find a valid PowerShell executable on the system.
|
||||
/// It first tries to find pwsh.exe, and if that fails, it tries to find
|
||||
/// powershell.exe.
|
||||
#[cfg(windows)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn try_find_powershellish_executable_blocking() -> Option<AbsolutePathBuf> {
|
||||
if let Some(pwsh_path) = try_find_pwsh_executable_blocking() {
|
||||
Some(pwsh_path)
|
||||
} else {
|
||||
try_find_powershell_executable_blocking()
|
||||
}
|
||||
}
|
||||
|
||||
/// This function attempts to find a powershell.exe executable on the system.
|
||||
pub fn try_find_powershell_executable_blocking() -> Option<AbsolutePathBuf> {
|
||||
try_find_powershellish_executable_in_path(&["powershell.exe"])
|
||||
}
|
||||
|
||||
/// This function attempts to find a pwsh.exe executable on the system.
|
||||
/// Note that pwsh.exe and powershell.exe are different executables:
|
||||
///
|
||||
/// - pwsh.exe is the cross-platform PowerShell Core (v6+) executable
|
||||
/// - powershell.exe is the Windows PowerShell (v5.1 and earlier) executable
|
||||
///
|
||||
/// Further, while powershell.exe is included by default on Windows systems,
|
||||
/// pwsh.exe must be installed separately by the user. And even when the user
|
||||
/// has installed pwsh.exe, it may not be available in the system PATH, in which
|
||||
/// case we attempt to locate it via other means.
|
||||
pub fn try_find_pwsh_executable_blocking() -> Option<AbsolutePathBuf> {
|
||||
if let Some(ps_home) = std::process::Command::new("cmd")
|
||||
.args(["/C", "pwsh", "-NoProfile", "-Command", "$PSHOME"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|out| {
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let trimmed = stdout.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
})
|
||||
{
|
||||
let candidate = AbsolutePathBuf::resolve_path_against_base("pwsh.exe", &ps_home);
|
||||
|
||||
if let Ok(candidate_abs_path) = candidate
|
||||
&& is_powershellish_executable_available(candidate_abs_path.as_path())
|
||||
{
|
||||
return Some(candidate_abs_path);
|
||||
}
|
||||
}
|
||||
|
||||
try_find_powershellish_executable_in_path(&["pwsh.exe"])
|
||||
}
|
||||
|
||||
fn try_find_powershellish_executable_in_path(candidates: &[&str]) -> Option<AbsolutePathBuf> {
|
||||
for candidate in candidates {
|
||||
let Ok(resolved_path) = which::which(candidate) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !is_powershellish_executable_available(&resolved_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(abs_path) = AbsolutePathBuf::from_absolute_path(resolved_path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
return Some(abs_path);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_powershellish_executable_available(powershell_or_pwsh_exe: &std::path::Path) -> bool {
|
||||
// This test works for both powershell.exe and pwsh.exe.
|
||||
std::process::Command::new(powershell_or_pwsh_exe)
|
||||
.args(["-NoLogo", "-NoProfile", "-Command", "Write-Output ok"])
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::extract_powershell_command;
|
||||
|
||||
#[test]
|
||||
fn extracts_basic_powershell_command() {
|
||||
let cmd = vec![
|
||||
"powershell".to_string(),
|
||||
"-Command".to_string(),
|
||||
"Write-Host hi".to_string(),
|
||||
];
|
||||
let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
|
||||
assert_eq!(script, "Write-Host hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_lowercase_flags() {
|
||||
let cmd = vec![
|
||||
"powershell".to_string(),
|
||||
"-nologo".to_string(),
|
||||
"-command".to_string(),
|
||||
"Write-Host hi".to_string(),
|
||||
];
|
||||
let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
|
||||
assert_eq!(script, "Write-Host hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_full_path_powershell_command() {
|
||||
let command = if cfg!(windows) {
|
||||
"C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe".to_string()
|
||||
} else {
|
||||
"/usr/local/bin/powershell.exe".to_string()
|
||||
};
|
||||
let cmd = vec![command, "-Command".to_string(), "Write-Host hi".to_string()];
|
||||
let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
|
||||
assert_eq!(script, "Write-Host hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_with_noprofile_and_alias() {
|
||||
let cmd = vec![
|
||||
"pwsh".to_string(),
|
||||
"-NoProfile".to_string(),
|
||||
"-c".to_string(),
|
||||
"Get-ChildItem | Select-String foo".to_string(),
|
||||
];
|
||||
let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
|
||||
assert_eq!(script, "Get-ChildItem | Select-String foo");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user