| 1 | use std::env; |
| 2 | use std::path::PathBuf; |
| 3 | use std::process::{Command, ExitStatus}; |
| 4 | use thiserror::Error; |
| 5 | |
| 6 | #[cfg(unix)] |
| 7 | use std::os::unix::process::ExitStatusExt; |
| 8 | |
| 9 | #[derive(Error, Debug)] |
| 10 | pub enum ExecutionError { |
| 11 | #[error("Command not found: {0}")] |
| 12 | CommandNotFound(String), |
| 13 | |
| 14 | #[error("I/O error: {0}")] |
| 15 | IoError(#[from] std::io::Error), |
| 16 | |
| 17 | #[error("Empty command")] |
| 18 | EmptyCommand, |
| 19 | } |
| 20 | |
| 21 | pub struct ExecutionResult { |
| 22 | pub exit_status: ExitStatus, |
| 23 | } |
| 24 | |
| 25 | impl ExecutionResult { |
| 26 | pub fn success_code() -> i32 { |
| 27 | 0 |
| 28 | } |
| 29 | |
| 30 | pub fn exit_code(&self) -> i32 { |
| 31 | self.exit_status.code().unwrap_or(1) |
| 32 | } |
| 33 | |
| 34 | pub fn success(&self) -> bool { |
| 35 | self.exit_status.success() |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | /// Execute a simple command (external program) |
| 40 | /// |
| 41 | /// In interactive mode, this sets up proper process groups and terminal control. |
| 42 | /// In non-interactive mode, it runs the command normally. |
| 43 | pub fn execute_command( |
| 44 | command: &str, |
| 45 | args: &[String], |
| 46 | interactive: bool, |
| 47 | ) -> Result<ExecutionResult, ExecutionError> { |
| 48 | if command.is_empty() { |
| 49 | return Err(ExecutionError::EmptyCommand); |
| 50 | } |
| 51 | |
| 52 | // Check if it's a built-in command |
| 53 | if let Some(result) = execute_builtin(command, args) { |
| 54 | return Ok(result); |
| 55 | } |
| 56 | |
| 57 | // Try to find the command in PATH |
| 58 | let program_path = find_in_path(command) |
| 59 | .ok_or_else(|| ExecutionError::CommandNotFound(command.to_string()))?; |
| 60 | |
| 61 | // Build the command |
| 62 | let mut cmd = Command::new(program_path); |
| 63 | cmd.args(args); |
| 64 | |
| 65 | // Execute with proper terminal handling |
| 66 | #[cfg(unix)] |
| 67 | { |
| 68 | crate::terminal::unix::execute_with_terminal_control(cmd, interactive) |
| 69 | } |
| 70 | |
| 71 | #[cfg(not(unix))] |
| 72 | { |
| 73 | crate::terminal::non_unix::execute_with_terminal_control(cmd, interactive) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /// Execute built-in commands |
| 78 | pub(crate) fn execute_builtin(command: &str, args: &[String]) -> Option<ExecutionResult> { |
| 79 | match command { |
| 80 | "exit" => { |
| 81 | let code = args.first() |
| 82 | .and_then(|s| s.parse::<i32>().ok()) |
| 83 | .unwrap_or(0); |
| 84 | std::process::exit(code); |
| 85 | } |
| 86 | "cd" => { |
| 87 | let default_home = env::var("HOME").unwrap_or_else(|_| "/".to_string()); |
| 88 | let dir = args.first() |
| 89 | .map(|s| s.as_str()) |
| 90 | .unwrap_or(&default_home); |
| 91 | |
| 92 | match env::set_current_dir(dir) { |
| 93 | Ok(_) => Some(success_result()), |
| 94 | Err(_) => Some(error_result()), |
| 95 | } |
| 96 | } |
| 97 | "pwd" => { |
| 98 | match env::current_dir() { |
| 99 | Ok(path) => { |
| 100 | println!("{}", path.display()); |
| 101 | Some(success_result()) |
| 102 | } |
| 103 | Err(_) => Some(error_result()), |
| 104 | } |
| 105 | } |
| 106 | "test" | "[" => { |
| 107 | let exit_code = crate::test_builtin::execute_test(args); |
| 108 | Some(exit_code_to_result(exit_code)) |
| 109 | } |
| 110 | _ => None, |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | fn exit_code_to_result(code: i32) -> ExecutionResult { |
| 115 | #[cfg(unix)] |
| 116 | { |
| 117 | ExecutionResult { |
| 118 | exit_status: std::process::ExitStatus::from_raw(code << 8), |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | #[cfg(not(unix))] |
| 123 | { |
| 124 | // On non-Unix, we can't easily create an ExitStatus with a specific code |
| 125 | if code == 0 { |
| 126 | success_result() |
| 127 | } else { |
| 128 | error_result() |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | #[cfg(unix)] |
| 134 | pub(crate) fn success_result() -> ExecutionResult { |
| 135 | ExecutionResult { |
| 136 | exit_status: std::process::ExitStatus::from_raw(0), |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | #[cfg(unix)] |
| 141 | fn error_result() -> ExecutionResult { |
| 142 | ExecutionResult { |
| 143 | exit_status: std::process::ExitStatus::from_raw(1 << 8), |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | #[cfg(not(unix))] |
| 148 | pub(crate) fn success_result() -> ExecutionResult { |
| 149 | ExecutionResult { |
| 150 | exit_status: std::process::ExitStatus::default(), |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | #[cfg(not(unix))] |
| 155 | fn error_result() -> ExecutionResult { |
| 156 | // On non-Unix, we can't easily create a failed ExitStatus |
| 157 | // This is a limitation for now |
| 158 | ExecutionResult { |
| 159 | exit_status: std::process::ExitStatus::default(), |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | /// Find a command in PATH |
| 164 | pub(crate) fn find_in_path(command: &str) -> Option<PathBuf> { |
| 165 | // If the command contains a slash, treat it as a path |
| 166 | if command.contains('/') { |
| 167 | let path = PathBuf::from(command); |
| 168 | if path.exists() && is_executable(&path) { |
| 169 | return Some(path); |
| 170 | } |
| 171 | return None; |
| 172 | } |
| 173 | |
| 174 | // Search in PATH |
| 175 | let path_var = env::var_os("PATH")?; |
| 176 | env::split_paths(&path_var) |
| 177 | .map(|dir| dir.join(command)) |
| 178 | .find(|path| path.exists() && is_executable(path)) |
| 179 | } |
| 180 | |
| 181 | /// Check if a file is executable |
| 182 | #[cfg(unix)] |
| 183 | fn is_executable(path: &PathBuf) -> bool { |
| 184 | use std::os::unix::fs::PermissionsExt; |
| 185 | path.metadata() |
| 186 | .map(|m| m.permissions().mode() & 0o111 != 0) |
| 187 | .unwrap_or(false) |
| 188 | } |
| 189 | |
| 190 | #[cfg(not(unix))] |
| 191 | fn is_executable(_path: &PathBuf) -> bool { |
| 192 | // On non-Unix systems, assume existence is enough |
| 193 | true |
| 194 | } |
| 195 | |
| 196 | #[cfg(test)] |
| 197 | mod tests { |
| 198 | use super::*; |
| 199 | |
| 200 | #[test] |
| 201 | fn test_find_in_path() { |
| 202 | // ls should exist on most Unix systems |
| 203 | let result = find_in_path("ls"); |
| 204 | assert!(result.is_some()); |
| 205 | } |
| 206 | |
| 207 | #[test] |
| 208 | fn test_command_not_found() { |
| 209 | let result = execute_command("nonexistent_command_12345", &[]); |
| 210 | assert!(result.is_err()); |
| 211 | } |
| 212 | } |
| 213 |