Skip to main content

cron_when/cli/dispatch/
mod.rs

1use crate::cli::actions::Action;
2use anyhow::Result;
3use clap::ArgMatches;
4use std::env;
5use std::io::{IsTerminal, stdout};
6use std::path::PathBuf;
7
8/// Convert `ArgMatches` into an Action
9///
10/// # Errors
11///
12/// Returns an error if no valid action can be determined from the matches
13pub fn handler(matches: &ArgMatches) -> Result<Action> {
14    // Determine verbosity level
15    let verbose = matches.get_count("verbose") > 0;
16
17    // Extract next count if provided
18    let next = matches.get_one::<u32>("next").copied();
19
20    // Determine if color should be enabled based on hierarchy:
21    // 1. --no-color flag (highest priority)
22    // 2. --color flag
23    // 3. NO_COLOR environment variable (https://no-color.org/)
24    // 4. CLICOLOR_FORCE environment variable
25    // 5. stdout is a terminal (auto-detection)
26    let color = if matches.get_flag("no-color") {
27        false
28    } else if matches.get_flag("color") {
29        true
30    } else if env::var_os("NO_COLOR").is_some() {
31        false
32    } else if env::var_os("CLICOLOR_FORCE").is_some_and(|v| v != "0") {
33        true
34    } else {
35        stdout().is_terminal()
36    };
37
38    // Check which mode was requested
39    if matches.get_flag("crontab") {
40        Ok(Action::Crontab { verbose, color })
41    } else if let Some(file_path) = matches.get_one::<String>("file") {
42        Ok(Action::File {
43            path: PathBuf::from(file_path),
44            verbose,
45            color,
46        })
47    } else if let Some(expression) = matches.get_one::<String>("cron") {
48        Ok(Action::Single {
49            expression: expression.clone(),
50            verbose,
51            next,
52            color,
53        })
54    } else {
55        anyhow::bail!(
56            "Please provide a cron expression, use --file, or use --crontab\n\
57             Run with --help for usage information"
58        )
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::cli::commands;
66    use std::sync::{Mutex, MutexGuard};
67
68    static ENV_LOCK: Mutex<()> = Mutex::new(());
69
70    fn env_guard() -> MutexGuard<'static, ()> {
71        ENV_LOCK
72            .lock()
73            .unwrap_or_else(std::sync::PoisonError::into_inner)
74    }
75
76    #[test]
77    fn test_handler_single() {
78        let matches = commands::new().get_matches_from(vec!["cron-when", "*/5 * * * *"]);
79        let action = handler(&matches);
80        assert!(action.is_ok());
81        if let Ok(action) = action {
82            assert!(matches!(action, Action::Single { .. }));
83        }
84    }
85
86    #[test]
87    fn test_handler_file() {
88        let matches = commands::new().get_matches_from(vec!["cron-when", "-f", "test.crontab"]);
89        let action = handler(&matches);
90        assert!(action.is_ok());
91        if let Ok(action) = action {
92            assert!(matches!(action, Action::File { .. }));
93        }
94    }
95
96    #[test]
97    fn test_handler_crontab() {
98        let matches = commands::new().get_matches_from(vec!["cron-when", "--crontab"]);
99        let action = handler(&matches);
100        assert!(action.is_ok());
101        if let Ok(action) = action {
102            assert!(matches!(action, Action::Crontab { verbose: false, .. }));
103        }
104    }
105
106    #[test]
107    fn test_handler_verbose() {
108        let matches = commands::new().get_matches_from(vec!["cron-when", "-v", "*/5 * * * *"]);
109        let action = handler(&matches);
110        assert!(action.is_ok());
111        if let Ok(Action::Single { verbose, .. }) = action {
112            assert!(verbose);
113        }
114    }
115
116    #[test]
117    fn test_handler_no_args() {
118        let matches = commands::new().get_matches_from(vec!["cron-when"]);
119        let result = handler(&matches);
120        assert!(result.is_err());
121    }
122
123    #[test]
124    fn test_handler_next() {
125        let matches =
126            commands::new().get_matches_from(vec!["cron-when", "--next", "5", "*/5 * * * *"]);
127        let action = handler(&matches);
128        assert!(action.is_ok());
129        if let Ok(Action::Single { next, .. }) = action {
130            assert_eq!(next, Some(5));
131        }
132    }
133
134    #[test]
135    #[allow(clippy::undocumented_unsafe_blocks)]
136    fn test_handler_no_color_env() {
137        let _guard = env_guard();
138        unsafe {
139            env::set_var("NO_COLOR", "1");
140        }
141        let matches = commands::new().get_matches_from(vec!["cron-when", "*/5 * * * *"]);
142        let action = handler(&matches);
143        assert!(action.is_ok());
144        if let Ok(Action::Single { color, .. }) = action {
145            // Should be false because of NO_COLOR
146            assert!(!color);
147        }
148        unsafe {
149            env::remove_var("NO_COLOR");
150        }
151    }
152
153    #[test]
154    #[allow(clippy::undocumented_unsafe_blocks)]
155    fn test_handler_empty_no_color_env_disables_color() {
156        let _guard = env_guard();
157        unsafe {
158            env::set_var("NO_COLOR", "");
159            env::set_var("CLICOLOR_FORCE", "1");
160        }
161        let matches = commands::new().get_matches_from(vec!["cron-when", "*/5 * * * *"]);
162        let action = handler(&matches);
163        assert!(action.is_ok());
164        if let Ok(Action::Single { color, .. }) = action {
165            assert!(!color);
166        }
167        unsafe {
168            env::remove_var("NO_COLOR");
169            env::remove_var("CLICOLOR_FORCE");
170        }
171    }
172
173    #[test]
174    #[allow(clippy::undocumented_unsafe_blocks)]
175    fn test_handler_color_flag_overrides_no_color_env() {
176        let _guard = env_guard();
177        unsafe {
178            env::set_var("NO_COLOR", "1");
179        }
180        let matches = commands::new().get_matches_from(vec!["cron-when", "--color", "*/5 * * * *"]);
181        let action = handler(&matches);
182        assert!(action.is_ok());
183        if let Ok(Action::Single { color, .. }) = action {
184            // Flag should override environment variable
185            assert!(color);
186        }
187        unsafe {
188            env::remove_var("NO_COLOR");
189        }
190    }
191
192    #[test]
193    #[allow(clippy::undocumented_unsafe_blocks)]
194    fn test_handler_no_color_flag_overrides_all() {
195        let _guard = env_guard();
196        unsafe {
197            env::set_var("CLICOLOR_FORCE", "1");
198        }
199        let matches =
200            commands::new().get_matches_from(vec!["cron-when", "--no-color", "*/5 * * * *"]);
201        let action = handler(&matches);
202        assert!(action.is_ok());
203        if let Ok(Action::Single { color, .. }) = action {
204            // --no-color flag should override CLICOLOR_FORCE
205            assert!(!color);
206        }
207        unsafe {
208            env::remove_var("CLICOLOR_FORCE");
209        }
210    }
211}