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        .long_about(
28            "A CLI tool to parse cron expressions and display next execution times with human-readable durations.\n\n\
29             EXAMPLES:\n  \
30             cron-when \"*/5 * * * *\"\n    \
31             Show next execution time for a cron expression\n\n  \
32             cron-when \"0 0 * * *\" --next 10\n    \
33             Show next 10 execution times\n\n  \
34             cron-when --file /etc/cron.d/jobs\n    \
35             Parse and display all cron jobs from a file\n\n  \
36             cron-when --crontab\n    \
37             Parse current user's crontab\n\n  \
38             cron-when --file /etc/cron.d/jobs --color\n    \
39             Show output with colors"
40        )
41        .color(ColorChoice::Auto)
42        .styles(styles)
43        .arg(
44            Arg::new("cron")
45                .value_name("CRON_EXPRESSION")
46                .help("Cron expression (e.g., \"*/5 * * * *\")")
47                .long_help(
48                    "A standard cron expression with 5 fields:\n  \
49                     minute hour day month weekday\n\n\
50                     Examples:\n  \
51                     \"*/5 * * * *\"    - Every 5 minutes\n  \
52                     \"0 * * * *\"      - Every hour\n  \
53                     \"0 0 * * *\"      - Every day at midnight\n  \
54                     \"0 9-17 * * 1-5\" - Weekdays 9am-5pm"
55                )
56                .index(1),
57        )
58        .arg(
59            Arg::new("file")
60                .short('f')
61                .long("file")
62                .value_name("FILE")
63                .help("Read from file (crontab format)")
64                .long_help(
65                    "Read cron expressions from a file in standard crontab format.\n\n\
66                     The file should contain lines in the format:\n  \
67                     <cron-expression> <command>\n\n\
68                     Lines starting with '#' are treated as comments.\n\
69                     Empty lines and environment variable assignments (e.g., SHELL=/bin/bash) are ignored."
70                ),
71        )
72        .arg(
73            Arg::new("crontab")
74                .short('l')
75                .long("crontab")
76                .help("Parse current user's crontab")
77                .long_help(
78                    "Parse and display all cron jobs from the current user's crontab.\n\n\
79                     This is equivalent to parsing the output of 'crontab -l'.\n\
80                     Requires the 'crontab' command to be available on the system."
81                )
82                .action(ArgAction::SetTrue),
83        )
84        .arg(
85            Arg::new("verbose")
86                .short('v')
87                .long("verbose")
88                .help("Show verbose output with cron expression")
89                .long_help(
90                    "Enable verbose logging for debugging purposes.\n\n\
91                     Can be specified multiple times to increase verbosity:\n  \
92                     -v    = INFO level\n  \
93                     -vv   = DEBUG level\n  \
94                     -vvv  = TRACE level\n\n\
95                     Note: Verbose output is sent to stderr via the RUST_LOG environment variable."
96                )
97                .action(ArgAction::Count),
98        )
99        .arg(
100            Arg::new("next")
101                .short('n')
102                .long("next")
103                .value_name("COUNT")
104                .help("Show next N occurrences of the cron expression")
105                .long_help(
106                    "Show the next N execution times for a cron expression.\n\n\
107                     This option displays multiple consecutive execution times with their\n\
108                     corresponding delays from the current time.\n\n\
109                     CONSTRAINTS:\n  \
110                     COUNT must be between 1 and 100 (inclusive)\n\n\
111                     EXAMPLES:\n  \
112                     cron-when \"0 * * * *\" --next 5\n    \
113                     Show next 5 hourly executions\n\n  \
114                     cron-when \"0 0 * * 0\" -n 10\n    \
115                     Show next 10 weekly executions (every Sunday)"
116                )
117                .value_parser(clap::value_parser!(u32).range(1..=100)),
118        )
119        .arg(
120            Arg::new("color")
121                .short('c')
122                .long("color")
123                .help("Enable colored output")
124                .long_help(
125                    "Enable colored output for better readability.\n\n\
126                     When enabled, output labels and values are colored:\n  \
127                     - Cron: label (Green bold), expression (Yellow)\n  \
128                     - Command: label (Red bold), command (White)\n  \
129                     - Next: label (Blue bold), datetime (default)\n  \
130                     - Left: label (Yellow bold), duration (default)\n  \
131                     - Comments: Gray\n\n\
132                     This flag is useful when viewing output in a terminal that supports ANSI colors.\n\n\
133                     ENVIRONMENT VARIABLE:\n  \
134                     Color output can also be enabled by setting the environment variable:\n  \
135                     CRON_WHEN_COLOR=1\n\n\
136                     The --color flag takes precedence over the environment variable."
137                )
138                .action(ArgAction::SetTrue),
139        )
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_cli_structure() {
148        let cmd = new();
149        assert_eq!(cmd.get_name(), env!("CARGO_PKG_NAME"));
150    }
151
152    #[test]
153    fn test_parse_cron_expression() {
154        let matches = new().get_matches_from(vec!["cron-when", "*/5 * * * *"]);
155        assert!(matches.contains_id("cron"));
156        assert_eq!(matches.get_one::<String>("cron").unwrap(), "*/5 * * * *");
157    }
158
159    #[test]
160    fn test_parse_file_flag() {
161        let matches = new().get_matches_from(vec!["cron-when", "-f", "test.crontab"]);
162        assert!(matches.contains_id("file"));
163        assert_eq!(matches.get_one::<String>("file").unwrap(), "test.crontab");
164    }
165
166    #[test]
167    fn test_parse_crontab_flag() {
168        let matches = new().get_matches_from(vec!["cron-when", "--crontab"]);
169        assert!(matches.get_flag("crontab"));
170    }
171
172    #[test]
173    fn test_verbose_count() {
174        let matches = new().get_matches_from(vec!["cron-when", "-vvv", "*/5 * * * *"]);
175        assert_eq!(matches.get_count("verbose"), 3);
176    }
177
178    #[test]
179    fn test_parse_next_flag() {
180        let matches = new().get_matches_from(vec!["cron-when", "--next", "5", "*/5 * * * *"]);
181        assert_eq!(matches.get_one::<u32>("next"), Some(&5));
182    }
183
184    #[test]
185    fn test_parse_next_short() {
186        let matches = new().get_matches_from(vec!["cron-when", "-n", "10", "*/5 * * * *"]);
187        assert_eq!(matches.get_one::<u32>("next"), Some(&10));
188    }
189}