cron_when/cli/commands/
mod.rs

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