1use chrono::Datelike;
2use chrono::NaiveDate;
3use chrono::Utc;
4use clap::{App, Arg, ArgGroup, ArgMatches};
5use distro_info::Distro;
6use distro_info::{DistroInfo, DistroRelease};
7use failure::{bail, format_err, Error, ResultExt};
8
9pub const OUTDATED_MSG: &str = "Distribution data outdated.
10Please check for an update for distro-info-data. See /usr/share/doc/distro-info-data/README.Debian for details.";
11
12pub enum DaysMode {
13 Created,
14 Eol,
15 EolServer,
16 Release,
17}
18
19pub enum OutputMode {
20 Codename,
21 FullName,
22 Release,
23 Suppress,
24}
25
26pub fn add_common_args<'a>(app: App<'a, 'a>, additional_selectors: &'a [&str]) -> App<'a, 'a> {
28 let mut selectors = vec![
29 "all",
30 "devel",
31 "series",
32 "stable",
33 "supported",
34 "unsupported",
35 ];
36 selectors.extend(additional_selectors);
37 app.version("0.1.0")
38 .author("Daniel Watkins <daniel@daniel-watkins.co.uk>")
39 .arg(
40 Arg::with_name("all")
41 .short("a")
42 .long("all")
43 .help("list all known versions"),
44 )
45 .arg(
46 Arg::with_name("devel")
47 .short("d")
48 .long("devel")
49 .help("latest development version"),
50 )
51 .arg(
52 Arg::with_name("series")
53 .long("series")
54 .takes_value(true)
55 .help("series to calculate the version for"),
56 )
57 .arg(
58 Arg::with_name("stable")
59 .short("s")
60 .long("stable")
61 .help("latest stable version"),
62 )
63 .arg(
64 Arg::with_name("supported")
65 .long("supported")
66 .help("list of all supported stable versions"),
67 )
68 .arg(
69 Arg::with_name("unsupported")
70 .long("unsupported")
71 .help("list of all unsupported stable versions"),
72 )
73 .arg(
74 Arg::with_name("codename")
75 .short("c")
76 .long("codename")
77 .help("print the codename (default)"),
78 )
79 .arg(
80 Arg::with_name("fullname")
81 .short("f")
82 .long("fullname")
83 .help("print the full name"),
84 )
85 .arg(
86 Arg::with_name("release")
87 .short("r")
88 .long("release")
89 .help("print the release version"),
90 )
91 .arg(
92 Arg::with_name("date")
93 .long("date")
94 .takes_value(true)
95 .help("date for calculating the version (default: today)"),
96 )
97 .arg(
98 Arg::with_name("days")
99 .short("y")
100 .long("days")
101 .takes_value(true)
102 .default_value("release")
103 .possible_values(&["created", "eol", "eol-server", "release"])
104 .value_name("milestone")
105 .help("additionally, display days until milestone"),
106 )
107 .group(
108 ArgGroup::with_name("selector")
109 .args(&selectors)
110 .required(true),
111 )
112 .group(ArgGroup::with_name("output").args(&["codename", "fullname", "release"]))
113}
114
115pub fn common_run(matches: &ArgMatches, distro_info: &impl DistroInfo) -> Result<(), Error> {
116 let date = match matches.value_of("date") {
117 Some(date_str) => NaiveDate::parse_from_str(date_str, "%Y-%m-%d").context(format!(
118 "Failed to parse date '{}'; must be YYYY-MM-DD format",
119 date_str
120 ))?,
121 None => today(),
122 };
123 let distro_releases_iter = select_distro_releases(&matches, date, distro_info)?;
124 let days_mode = if matches.occurrences_of("days") == 0 {
125 None
126 } else {
127 matches.value_of("days").map(|value| match value {
128 "created" => DaysMode::Created,
129 "eol" => DaysMode::Eol,
130 "eol-server" => DaysMode::EolServer,
131 "release" => DaysMode::Release,
132 _ => panic!("unknown days mode found; please report a bug"),
133 })
134 };
135 let distro_name = distro_info.distro().to_string();
136 if matches.is_present("fullname") {
137 output(
138 distro_name,
139 distro_releases_iter,
140 &OutputMode::FullName,
141 &days_mode,
142 date,
143 )?;
144 } else if matches.is_present("release") {
145 output(
146 distro_name,
147 distro_releases_iter,
148 &OutputMode::Release,
149 &days_mode,
150 date,
151 )?;
152 } else if matches.is_present("codename") || days_mode.is_none() {
153 output(
155 distro_name,
156 distro_releases_iter,
157 &OutputMode::Codename,
158 &days_mode,
159 date,
160 )?;
161 } else {
162 output(
163 distro_name,
164 distro_releases_iter,
165 &OutputMode::Suppress,
166 &days_mode,
167 date,
168 )?;
169 }
170 Ok(())
171}
172
173fn determine_day_delta(current_date: NaiveDate, target_date: NaiveDate) -> i64 {
174 target_date.signed_duration_since(current_date).num_days()
175}
176
177pub fn output(
178 distro_name: &str,
179 distro_releases: Vec<&DistroRelease>,
180 output_mode: &OutputMode,
181 days_mode: &Option<DaysMode>,
182 date: NaiveDate,
183) -> Result<(), Error> {
184 if distro_releases.len() == 0 {
185 bail!(OUTDATED_MSG);
186 }
187 for distro_release in distro_releases {
188 let mut output_parts = vec![];
189 match output_mode {
190 OutputMode::Codename => output_parts.push(distro_release.series().to_string()),
191 OutputMode::Release => output_parts.push(
192 distro_release
193 .version()
194 .as_ref()
195 .unwrap_or_else(|| distro_release.series())
196 .to_string(),
197 ),
198 OutputMode::FullName => output_parts.push(format!(
199 "{} {} \"{}\"",
200 distro_name,
201 match distro_release.version() {
202 Some(version) => version,
203 None => "",
204 },
205 &distro_release.codename()
206 )),
207 OutputMode::Suppress => (),
208 }
209 let target_date = match days_mode {
210 Some(DaysMode::Created) => Some(distro_release.created().ok_or(format_err!(
211 "No creation date found for {}",
212 &distro_release.series()
213 ))?),
214 Some(DaysMode::Eol) => *distro_release.eol(),
215 Some(DaysMode::EolServer) => *distro_release.eol_server(),
216 Some(DaysMode::Release) => Some(distro_release.release().ok_or(format_err!(
217 "No release date found for {}",
218 &distro_release.series()
219 ))?),
220 None => None,
221 };
222 match target_date {
223 Some(target_date) => {
224 output_parts.push(format!("{}", determine_day_delta(date, target_date)));
225 }
226 None => match days_mode {
227 Some(DaysMode::EolServer) | Some(DaysMode::Eol) => {
228 output_parts.push("(unknown)".to_string())
229 }
230 _ => (),
231 },
232 };
233 if !output_parts.is_empty() {
234 println!("{}", output_parts.join(" "));
235 }
236 }
237 Ok(())
238}
239
240pub fn select_distro_releases<'a>(
241 matches: &ArgMatches,
242 date: NaiveDate,
243 distro_info: &'a impl DistroInfo,
244) -> Result<Vec<&'a DistroRelease>, Error> {
245 Ok(if matches.is_present("all") {
246 distro_info.iter().collect()
247 } else if matches.is_present("supported") {
248 distro_info.supported(date)
249 } else if matches.is_present("unsupported") {
250 distro_info.unsupported(date)
251 } else if matches.is_present("devel") {
252 match distro_info.distro() {
253 Distro::Ubuntu => distro_info.ubuntu_devel(date),
254 Distro::Debian => distro_info.debian_devel(date),
255 }
256 } else if matches.is_present("testing") {
257 distro_info.ubuntu_devel(date)
259 } else if matches.is_present("latest") {
260 let devel_result = distro_info.ubuntu_devel(date);
261 if devel_result.len() > 0 {
262 vec![*devel_result.last().unwrap()]
263 } else {
264 distro_info
265 .latest(date)
266 .map(|distro_release| vec![distro_release])
267 .unwrap_or_else(|| vec![])
268 }
269 } else if matches.is_present("lts") {
270 let mut lts_releases = vec![];
271 for distro_release in distro_info.all_at(date) {
272 if distro_release.is_lts() {
273 lts_releases.push(distro_release);
274 }
275 }
276 match lts_releases.last() {
277 Some(release) => vec![*release],
278 None => bail!(OUTDATED_MSG),
279 }
280 } else if matches.is_present("stable") {
281 distro_info
282 .latest(date)
283 .map(|distro_release| vec![distro_release])
284 .unwrap_or_else(|| vec![])
285 } else if matches.is_present("series") {
286 match matches.value_of("series") {
287 Some(needle_series) => {
288 if !needle_series.chars().all(|c| c.is_lowercase()) {
289 bail!("invalid distribution series `{}'", needle_series);
290 };
291 let candidates: Vec<&DistroRelease> = distro_info
292 .iter()
293 .filter(|distro_release| distro_release.series() == needle_series)
294 .collect();
295 if candidates.is_empty() {
296 bail!("unknown distribution series `{}'", needle_series);
297 };
298 Ok(candidates)
299 }
300 None => Err(format_err!(
301 "--series requires an argument; please report a bug about this \
302 error"
303 )),
304 }?
305 } else {
306 panic!("clap prevent us from reaching here; report a bug if you see this")
307 })
308}
309
310fn today() -> NaiveDate {
311 let now = Utc::now();
312 NaiveDate::from_ymd(now.year(), now.month(), now.day())
313}