codex_shell_command/
powershell.rs1use std::path::PathBuf;
2
3use codex_utils_absolute_path::AbsolutePathBuf;
4
5use crate::command_safety::try_parse_powershell_ast_commands;
6use crate::shell_detect::ShellType;
7use crate::shell_detect::detect_shell_type;
8
9const POWERSHELL_FLAGS: &[&str] = &["-nologo", "-noprofile", "-command", "-c"];
10
11pub const UTF8_OUTPUT_PREFIX: &str =
13 "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n";
14
15pub fn prefix_powershell_script_with_utf8(command: &[String]) -> Vec<String> {
16 let Some((_, script)) = extract_powershell_command(command) else {
17 return command.to_vec();
18 };
19
20 let trimmed = script.trim_start();
21 let script = if trimmed.starts_with(UTF8_OUTPUT_PREFIX) {
22 script.to_string()
23 } else {
24 format!("{UTF8_OUTPUT_PREFIX}{script}")
25 };
26
27 let mut command: Vec<String> = command[..(command.len() - 1)]
28 .iter()
29 .map(std::string::ToString::to_string)
30 .collect();
31 command.push(script);
32 command
33}
34
35pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
44 if command.len() < 3 {
45 return None;
46 }
47
48 let shell = &command[0];
49 if !matches!(
50 detect_shell_type(PathBuf::from(shell)),
51 Some(ShellType::PowerShell)
52 ) {
53 return None;
54 }
55
56 let mut i = 1usize;
58 while i + 1 < command.len() {
59 let flag = &command[i];
60 if !POWERSHELL_FLAGS.contains(&flag.to_ascii_lowercase().as_str()) {
62 return None;
63 }
64 if flag.eq_ignore_ascii_case("-Command") || flag.eq_ignore_ascii_case("-c") {
65 let script = &command[i + 1];
66 return Some((shell, script));
67 }
68 i += 1;
69 }
70 None
71}
72
73pub fn parse_powershell_command_into_plain_commands(
79 command: &[String],
80) -> Option<Vec<Vec<String>>> {
81 let (executable, script) = extract_powershell_command(command)?;
82 try_parse_powershell_ast_commands(executable, script)
83}
84
85pub fn try_find_powershell_executable_blocking() -> Option<AbsolutePathBuf> {
87 try_find_powershellish_executable_in_path(&["powershell.exe"])
88}
89
90pub fn try_find_pwsh_executable_blocking() -> Option<AbsolutePathBuf> {
101 if let Some(ps_home) = std::process::Command::new("cmd")
102 .args(["/C", "pwsh", "-NoProfile", "-Command", "$PSHOME"])
103 .output()
104 .ok()
105 .and_then(|out| {
106 if !out.status.success() {
107 return None;
108 }
109 let stdout = String::from_utf8_lossy(&out.stdout);
110 let trimmed = stdout.trim();
111 (!trimmed.is_empty()).then(|| trimmed.to_string())
112 })
113 {
114 let candidate = AbsolutePathBuf::resolve_path_against_base("pwsh.exe", &ps_home);
115
116 if is_powershellish_executable_available(candidate.as_path()) {
117 return Some(candidate);
118 }
119 }
120
121 try_find_powershellish_executable_in_path(&["pwsh.exe"])
122}
123
124fn try_find_powershellish_executable_in_path(candidates: &[&str]) -> Option<AbsolutePathBuf> {
125 for candidate in candidates {
126 let Ok(resolved_path) = which::which(candidate) else {
127 continue;
128 };
129
130 if !is_powershellish_executable_available(&resolved_path) {
131 continue;
132 }
133
134 let Ok(abs_path) = AbsolutePathBuf::from_absolute_path(resolved_path) else {
135 continue;
136 };
137
138 return Some(abs_path);
139 }
140
141 None
142}
143
144fn is_powershellish_executable_available(powershell_or_pwsh_exe: &std::path::Path) -> bool {
145 std::process::Command::new(powershell_or_pwsh_exe)
147 .args(["-NoLogo", "-NoProfile", "-Command", "Write-Output ok"])
148 .output()
149 .map(|output| output.status.success())
150 .unwrap_or(false)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::UTF8_OUTPUT_PREFIX;
156 use super::extract_powershell_command;
157 #[cfg(windows)]
158 use super::parse_powershell_command_into_plain_commands;
159 use super::prefix_powershell_script_with_utf8;
160
161 #[test]
162 fn extracts_basic_powershell_command() {
163 let cmd = vec![
164 "powershell".to_string(),
165 "-Command".to_string(),
166 "Write-Host hi".to_string(),
167 ];
168 let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
169 assert_eq!(script, "Write-Host hi");
170 }
171
172 #[test]
173 fn extracts_lowercase_flags() {
174 let cmd = vec![
175 "powershell".to_string(),
176 "-nologo".to_string(),
177 "-command".to_string(),
178 "Write-Host hi".to_string(),
179 ];
180 let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
181 assert_eq!(script, "Write-Host hi");
182 }
183
184 #[test]
185 fn extracts_full_path_powershell_command() {
186 let command = if cfg!(windows) {
187 "C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe".to_string()
188 } else {
189 "/usr/local/bin/powershell.exe".to_string()
190 };
191 let cmd = vec![command, "-Command".to_string(), "Write-Host hi".to_string()];
192 let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
193 assert_eq!(script, "Write-Host hi");
194 }
195
196 #[test]
197 fn extracts_with_noprofile_and_alias() {
198 let cmd = vec![
199 "pwsh".to_string(),
200 "-NoProfile".to_string(),
201 "-c".to_string(),
202 "Get-ChildItem | Select-String foo".to_string(),
203 ];
204 let (_shell, script) = extract_powershell_command(&cmd).expect("extract");
205 assert_eq!(script, "Get-ChildItem | Select-String foo");
206 }
207
208 #[test]
209 fn prefixes_powershell_command_with_best_effort_utf8() {
210 let cmd = vec![
211 "powershell".to_string(),
212 "-Command".to_string(),
213 "Write-Host hi".to_string(),
214 ];
215
216 let prefixed = prefix_powershell_script_with_utf8(&cmd);
217
218 assert_eq!(
219 prefixed,
220 vec![
221 "powershell".to_string(),
222 "-Command".to_string(),
223 format!("{UTF8_OUTPUT_PREFIX}Write-Host hi"),
224 ]
225 );
226 }
227
228 #[test]
229 fn does_not_duplicate_utf8_prefix() {
230 let cmd = vec![
231 "powershell".to_string(),
232 "-Command".to_string(),
233 format!("{UTF8_OUTPUT_PREFIX}Write-Host hi"),
234 ];
235
236 assert_eq!(prefix_powershell_script_with_utf8(&cmd), cmd);
237 }
238
239 #[cfg(windows)]
240 #[test]
241 fn parses_plain_powershell_commands() {
242 let commands = parse_powershell_command_into_plain_commands(&[
243 "powershell.exe".to_string(),
244 "-NoProfile".to_string(),
245 "-Command".to_string(),
246 "echo hi".to_string(),
247 ])
248 .expect("parse");
249
250 assert_eq!(commands, vec![vec!["echo".to_string(), "hi".to_string()]]);
251 }
252
253 #[cfg(windows)]
254 #[test]
255 fn parses_multiple_plain_powershell_commands() {
256 let commands = parse_powershell_command_into_plain_commands(&[
257 "powershell.exe".to_string(),
258 "-NoProfile".to_string(),
259 "-Command".to_string(),
260 "Write-Output foo | Measure-Object".to_string(),
261 ])
262 .expect("parse");
263
264 assert_eq!(
265 commands,
266 vec![
267 vec!["Write-Output".to_string(), "foo".to_string()],
268 vec!["Measure-Object".to_string()],
269 ]
270 );
271 }
272}