cron_when/cli/commands/
mod.rs1use 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
10pub 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}