loop_lib/
executor.rs

1use crate::args::LoopOptions;
2use crate::config::LoopConfig;
3use regex::Regex;
4use std::io::{self, Write};
5use std::path::Path;
6use std::process::{Command, Stdio};
7
8/// Determines if a directory should be processed based on the provided options and configuration.
9///
10/// This function takes a directory path, options, and configuration, and returns a boolean
11/// indicating whether the directory should be processed.
12pub fn should_process_directory(
13    dir_path: &Path,
14    options: &LoopOptions,
15    config: &LoopConfig,
16) -> bool {
17    let dir_name = dir_path.file_name().unwrap_or_default().to_str().unwrap();
18
19    if let Some(ref include_only) = options.include_only {
20        return include_only.contains(&dir_name.to_string())
21            || include_only.contains(&".".to_string());
22    }
23
24    if let Some(ref exclude_only) = options.exclude_only {
25        return !exclude_only.contains(&dir_name.to_string());
26    }
27
28    if let Some(ref include) = options.include {
29        if include.contains(&dir_name.to_string()) {
30            return true;
31        }
32    }
33
34    if let Some(ref exclude) = options.exclude {
35        if exclude.contains(&dir_name.to_string()) {
36            return false;
37        }
38    }
39
40    if let Some(ref include_pattern) = options.include_pattern {
41        let re = Regex::new(include_pattern).unwrap();
42        if !re.is_match(dir_name) {
43            return false;
44        }
45    }
46
47    if let Some(ref exclude_pattern) = options.exclude_pattern {
48        let re = Regex::new(exclude_pattern).unwrap();
49        if re.is_match(dir_name) {
50            return false;
51        }
52    }
53
54    if config.ignore.contains(&dir_name.to_string()) {
55        return false;
56    }
57
58    true
59}
60
61/// Executes the specified command in the given directory.
62///
63/// This function runs the provided command in the specified directory, handling
64/// different shell configurations and providing appropriate output based on the
65/// command's success or failure.
66pub fn execute_command_in_directory(dir: &Path, command: &[String]) -> i32 {
67    let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
68    let command_str = command.join(" ");
69
70    let script = if shell.ends_with("zsh") {
71        format!(
72            r#"
73            source ~/.zshrc 2>/dev/null
74            eval "{}"
75            "#,
76            command_str.replace('"', r#"\""#)
77        )
78    } else if shell.ends_with("fish") {
79        format!(
80            r#"
81            source ~/.config/fish/config.fish 2>/dev/null
82            {}
83            "#,
84            command_str
85        )
86    } else {
87        // Assume bash-like shell
88        format!(
89            r#"
90            if [ -f ~/.bashrc ]; then . ~/.bashrc; fi
91            {}
92            "#,
93            command_str
94        )
95    };
96
97    println!();
98
99    let status = Command::new(&shell)
100        .arg("-c")
101        .arg(&script)
102        .env(
103            "HOME",
104            std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()),
105        )
106        .current_dir(dir)
107        .stdout(Stdio::inherit())
108        .stderr(Stdio::inherit())
109        .status()
110        .expect("Failed to execute command");
111
112    let exit_code = status.code().unwrap_or(-1);
113    let dir_name = dir.file_name().unwrap_or_default().to_str().unwrap();
114
115    if status.success() {
116        println!("\x1b[32m{} ✓\x1b[0m", dir_name);
117    } else {
118        println!("\x1b[31m{} ✗: exited code {}\x1b[0m", dir_name, exit_code);
119    }
120
121    io::stdout().flush().unwrap();
122
123    exit_code
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::config::LoopConfig;
130    use std::path::PathBuf;
131
132    #[test]
133    fn test_should_process_directory() {
134        let args = LoopOptions {
135            command: vec!["test".to_string()],
136            cwd: None,
137            include: Some(vec!["include_dir".to_string()]),
138            exclude: Some(vec!["exclude_dir".to_string()]),
139            include_only: None,
140            exclude_only: None,
141            include_pattern: None,
142            exclude_pattern: None,
143            init: false,
144        };
145
146        let config = LoopConfig {
147            ignore: vec![".git".to_string()],
148        };
149
150        assert!(should_process_directory(
151            &PathBuf::from("include_dir"),
152            &args,
153            &config
154        ));
155        assert!(!should_process_directory(
156            &PathBuf::from("exclude_dir"),
157            &args,
158            &config
159        ));
160        assert!(!should_process_directory(
161            &PathBuf::from(".git"),
162            &args,
163            &config
164        ));
165        assert!(should_process_directory(
166            &PathBuf::from("normal_dir"),
167            &args,
168            &config
169        ));
170    }
171
172    #[test]
173    fn test_should_process_directory_with_patterns() {
174        let args = LoopOptions {
175            command: vec!["test".to_string()],
176            cwd: None,
177            include: None,
178            exclude: None,
179            include_only: None,
180            exclude_only: None,
181            include_pattern: Some("src.*".to_string()),
182            exclude_pattern: Some("test.*".to_string()),
183            init: false,
184        };
185
186        let config = LoopConfig {
187            ignore: vec!["node_modules".to_string(), "target".to_string()],
188        };
189
190        assert!(should_process_directory(
191            &PathBuf::from("src"),
192            &args,
193            &config
194        ));
195        assert!(!should_process_directory(
196            &PathBuf::from("test"),
197            &args,
198            &config
199        ));
200        assert!(!should_process_directory(
201            &PathBuf::from("node_modules"),
202            &args,
203            &config
204        ));
205        assert!(!should_process_directory(
206            &PathBuf::from("target"),
207            &args,
208            &config
209        ));
210    }
211
212    #[test]
213    fn test_execute_command_in_directory() {
214        let temp_dir = tempfile::tempdir().unwrap();
215        let dir_path = temp_dir.path();
216
217        let exit_code =
218            execute_command_in_directory(dir_path, &["echo".to_string(), "test".to_string()]);
219        assert_eq!(exit_code, 0);
220
221        let exit_code = execute_command_in_directory(dir_path, &["false".to_string()]);
222        assert_ne!(exit_code, 0);
223    }
224}