Rust · 5357 bytes Raw Blame History
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