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