cron_when/cli/commands/
mod.rs

1use clap::{
2    Arg, ArgAction, ColorChoice, Command,
3    builder::styling::{AnsiColor, Effects, Styles},
4};
5
6pub mod built_info {
7    include!(concat!(env!("OUT_DIR"), "/built.rs"));
8}
9
10/// Build the CLI command structure
11pub fn new() -> Command {
12    let styles = Styles::styled()
13        .header(AnsiColor::Yellow.on_default() | Effects::BOLD)
14        .usage(AnsiColor::Green.on_default() | Effects::BOLD)
15        .literal(AnsiColor::Blue.on_default() | Effects::BOLD)
16        .placeholder(AnsiColor::Green.on_default());
17
18    let git_hash = built_info::GIT_COMMIT_HASH.unwrap_or("unknown");
19    let long_version: &'static str =
20        Box::leak(format!("{} - {}", env!("CARGO_PKG_VERSION"), git_hash).into_boxed_str());
21
22    Command::new(env!("CARGO_PKG_NAME"))
23        .version(env!("CARGO_PKG_VERSION"))
24        .long_version(long_version)
25        .author(env!("CARGO_PKG_AUTHORS"))
26        .about(env!("CARGO_PKG_DESCRIPTION"))
27        .color(ColorChoice::Auto)
28        .styles(styles)
29        .arg(
30            Arg::new("cron")
31                .value_name("CRON_EXPRESSION")
32                .help("Cron expression (e.g., \"*/5 * * * *\")")
33                .index(1),
34        )
35        .arg(
36            Arg::new("file")
37                .short('f')
38                .long("file")
39                .value_name("FILE")
40                .help("Read from file (crontab format)"),
41        )
42        .arg(
43            Arg::new("crontab")
44                .short('l')
45                .long("crontab")
46                .help("Parse current user's crontab")
47                .action(ArgAction::SetTrue),
48        )
49        .arg(
50            Arg::new("verbose")
51                .short('v')
52                .long("verbose")
53                .help("Show verbose output with cron expression")
54                .action(ArgAction::Count),
55        )
56        .arg(
57            Arg::new("next")
58                .short('n')
59                .long("next")
60                .value_name("COUNT")
61                .help("Show next N occurrences of the cron expression")
62                .value_parser(clap::value_parser!(u32).range(1..=100)),
63        )
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_cli_structure() {
72        let cmd = new();
73        assert_eq!(cmd.get_name(), env!("CARGO_PKG_NAME"));
74    }
75
76    #[test]
77    fn test_parse_cron_expression() {
78        let matches = new().get_matches_from(vec!["cron-when", "*/5 * * * *"]);
79        assert!(matches.contains_id("cron"));
80        assert_eq!(matches.get_one::<String>("cron").unwrap(), "*/5 * * * *");
81    }
82
83    #[test]
84    fn test_parse_file_flag() {
85        let matches = new().get_matches_from(vec!["cron-when", "-f", "test.crontab"]);
86        assert!(matches.contains_id("file"));
87        assert_eq!(matches.get_one::<String>("file").unwrap(), "test.crontab");
88    }
89
90    #[test]
91    fn test_parse_crontab_flag() {
92        let matches = new().get_matches_from(vec!["cron-when", "--crontab"]);
93        assert!(matches.get_flag("crontab"));
94    }
95
96    #[test]
97    fn test_verbose_count() {
98        let matches = new().get_matches_from(vec!["cron-when", "-vvv", "*/5 * * * *"]);
99        assert_eq!(matches.get_count("verbose"), 3);
100    }
101
102    #[test]
103    fn test_parse_next_flag() {
104        let matches = new().get_matches_from(vec!["cron-when", "--next", "5", "*/5 * * * *"]);
105        assert_eq!(matches.get_one::<u32>("next"), Some(&5));
106    }
107
108    #[test]
109    fn test_parse_next_short() {
110        let matches = new().get_matches_from(vec!["cron-when", "-n", "10", "*/5 * * * *"]);
111        assert_eq!(matches.get_one::<u32>("next"), Some(&10));
112    }
113}