1use crate::error::Error;
10use crate::mode::ExplicitChoice;
11use clap::{Parser, Subcommand};
12
13#[derive(Parser, Debug, Clone)]
16#[command(
17 name = "rusty-ts",
18 version,
19 about = "Prefix each line of stdin with a timestamp (Rust port of moreutils ts)",
20 long_about = None,
21)]
22pub struct Cli {
23 #[arg(
26 short = 'i',
27 long = "incremental",
28 conflicts_with = "since_start",
29 action = clap::ArgAction::SetTrue
30 )]
31 pub incremental: bool,
32
33 #[arg(
35 short = 's',
36 long = "since-start",
37 conflicts_with = "incremental",
38 action = clap::ArgAction::SetTrue
39 )]
40 pub since_start: bool,
41
42 #[arg(short = 'm', long = "monotonic", action = clap::ArgAction::SetTrue)]
45 pub monotonic: bool,
46
47 #[arg(short = 'r', long = "relative", action = clap::ArgAction::SetTrue)]
52 pub relative: bool,
53
54 #[arg(
58 short = 'u',
59 long = "utc",
60 conflicts_with = "tz",
61 action = clap::ArgAction::SetTrue
62 )]
63 pub utc: bool,
64
65 #[arg(long = "tz", value_name = "IANA-NAME", conflicts_with = "utc")]
69 pub tz: Option<String>,
70
71 #[arg(
77 long = "strict",
78 alias = "moreutils-compat",
79 conflicts_with = "no_strict",
80 action = clap::ArgAction::SetTrue
81 )]
82 pub strict: bool,
83
84 #[arg(
87 long = "no-strict",
88 alias = "no-moreutils-compat",
89 conflicts_with = "strict",
90 action = clap::ArgAction::SetTrue
91 )]
92 pub no_strict: bool,
93
94 #[arg(value_name = "FORMAT")]
99 pub format: Option<String>,
100
101 #[command(subcommand)]
103 pub subcommand: Option<CliCommand>,
104}
105
106#[derive(Subcommand, Debug, Clone)]
108pub enum CliCommand {
109 Completions {
112 #[arg(value_enum)]
114 shell: clap_complete::Shell,
115 },
116}
117
118impl Cli {
119 pub fn explicit_compat_choice(&self) -> Option<ExplicitChoice> {
122 match (self.strict, self.no_strict) {
123 (true, false) => Some(ExplicitChoice::Strict),
124 (false, true) => Some(ExplicitChoice::Default),
125 (false, false) => None,
126 (true, true) => None,
129 }
130 }
131
132 pub fn validate(&self) -> Result<(), Error> {
136 if self.utc {
137 if let Some(tz) = &self.tz {
138 return Err(Error::InvalidUtcWithNamedTz { tz: tz.clone() });
139 }
140 }
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use clap::CommandFactory;
149
150 #[test]
151 fn cli_command_factory_builds_without_panic() {
152 let _cmd = Cli::command();
155 }
156
157 #[test]
158 fn explicit_choice_signals_strict_when_strict_flag_set() {
159 let cli = Cli::parse_from(["rusty-ts", "--strict"]);
160 assert_eq!(cli.explicit_compat_choice(), Some(ExplicitChoice::Strict));
161 }
162
163 #[test]
164 fn explicit_choice_signals_default_when_no_strict_flag_set() {
165 let cli = Cli::parse_from(["rusty-ts", "--no-strict"]);
166 assert_eq!(cli.explicit_compat_choice(), Some(ExplicitChoice::Default));
167 }
168
169 #[test]
170 fn explicit_choice_none_when_neither_flag() {
171 let cli = Cli::parse_from(["rusty-ts"]);
172 assert_eq!(cli.explicit_compat_choice(), None);
173 }
174
175 #[test]
176 fn positional_format_captured() {
177 let cli = Cli::parse_from(["rusty-ts", "%Y-%m-%d %H:%M:%S"]);
178 assert_eq!(cli.format.as_deref(), Some("%Y-%m-%d %H:%M:%S"));
179 }
180
181 #[test]
182 fn utc_flag_parsed() {
183 let cli = Cli::parse_from(["rusty-ts", "-u"]);
184 assert!(cli.utc);
185 assert!(cli.tz.is_none());
186 }
187
188 #[test]
189 fn tz_flag_parsed() {
190 let cli = Cli::parse_from(["rusty-ts", "--tz=Asia/Tokyo"]);
191 assert_eq!(cli.tz.as_deref(), Some("Asia/Tokyo"));
192 assert!(!cli.utc);
193 }
194
195 #[test]
196 fn utc_and_tz_rejected_by_clap_conflict() {
197 let err = Cli::try_parse_from(["rusty-ts", "-u", "--tz=Asia/Tokyo"]);
198 assert!(
199 err.is_err(),
200 "clap should reject -u + --tz=... via conflicts_with"
201 );
202 }
203
204 #[test]
205 fn incremental_and_since_start_rejected_by_clap_conflict() {
206 let err = Cli::try_parse_from(["rusty-ts", "-i", "-s"]);
207 assert!(
208 err.is_err(),
209 "clap should reject -i + -s via conflicts_with"
210 );
211 }
212
213 #[test]
214 fn validate_passes_when_only_utc() {
215 let cli = Cli::parse_from(["rusty-ts", "-u"]);
216 assert!(cli.validate().is_ok());
217 }
218
219 #[test]
220 fn completions_subcommand_parsed() {
221 let cli = Cli::parse_from(["rusty-ts", "completions", "bash"]);
222 assert!(matches!(
223 cli.subcommand,
224 Some(CliCommand::Completions { shell: _ })
225 ));
226 }
227}