Skip to main content

ai_agent/utils/
which.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/utils/which.ts
2//! `which` command implementation for finding executable paths.
3
4use std::process::Command;
5
6/// Async version of which - finds the full path to a command executable.
7/// On Windows, uses `where.exe`. On POSIX systems, uses `which`.
8pub async fn which(command: &str) -> Option<String> {
9    which_impl(command).await
10}
11
12/// Sync version of which - finds the full path to a command executable.
13pub fn which_sync(command: &str) -> Option<String> {
14    which_impl_sync(command)
15}
16
17async fn which_impl(command: &str) -> Option<String> {
18    #[cfg(target_os = "windows")]
19    {
20        // On Windows, use where.exe and return the first result
21        let output = Command::new("where.exe").arg(command).output().ok()?;
22
23        if !output.status.success() {
24            return None;
25        }
26
27        let stdout = String::from_utf8_lossy(&output.stdout);
28        let trimmed = stdout.trim();
29        if trimmed.is_empty() {
30            return None;
31        }
32
33        // where.exe returns multiple paths separated by newlines, return the first
34        trimmed.split('\n').next().map(|s| s.trim().to_string())
35    }
36
37    #[cfg(not(target_os = "windows"))]
38    {
39        // On POSIX systems (macOS, Linux, WSL), use which
40        let output = Command::new("which").arg(command).output().ok()?;
41
42        if !output.status.success() {
43            return None;
44        }
45
46        let stdout = String::from_utf8_lossy(&output.stdout);
47        let trimmed = stdout.trim();
48        if trimmed.is_empty() {
49            return None;
50        }
51
52        Some(trimmed.to_string())
53    }
54}
55
56fn which_impl_sync(command: &str) -> Option<String> {
57    #[cfg(target_os = "windows")]
58    {
59        // On Windows, use where.exe and return the first result
60        let output = Command::new("where.exe").arg(command).output().ok()?;
61
62        if !output.status.success() {
63            return None;
64        }
65
66        let stdout = String::from_utf8_lossy(&output.stdout);
67        let trimmed = stdout.trim();
68        if trimmed.is_empty() {
69            return None;
70        }
71
72        // where.exe returns multiple paths separated by newlines, return the first
73        trimmed.split('\n').next().map(|s| s.trim().to_string())
74    }
75
76    #[cfg(not(target_os = "windows"))]
77    {
78        // On POSIX systems (macOS, Linux, WSL), use which
79        let output = Command::new("which").arg(command).output().ok()?;
80
81        if !output.status.success() {
82            return None;
83        }
84
85        let stdout = String::from_utf8_lossy(&output.stdout);
86        let trimmed = stdout.trim();
87        if trimmed.is_empty() {
88            return None;
89        }
90
91        Some(trimmed.to_string())
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_which_sync_existing_command() {
101        // Test finding an existing command
102        let result = which_sync("ls");
103        assert!(result.is_some());
104    }
105
106    #[test]
107    fn test_which_sync_nonexistent_command() {
108        // Test finding a non-existent command
109        let result = which_sync("nonexistent_command_xyz123");
110        assert!(result.is_none());
111    }
112
113    #[tokio::test]
114    async fn test_which_async_existing_command() {
115        let result = which("ls").await;
116        assert!(result.is_some());
117    }
118
119    #[tokio::test]
120    async fn test_which_async_nonexistent_command() {
121        let result = which("nonexistent_command_xyz123").await;
122        assert!(result.is_none());
123    }
124}