use clap::{
Arg, Command, Error,
builder::{PossibleValue, TypedValueParser},
error::{ContextKind, ContextValue, ErrorKind},
};
use core::time::Duration;
use std::{ffi::OsStr, fmt, time::SystemTime};
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum ParseTimeError {
Empty,
InvalidNumber,
InvalidUnit,
InvalidFormat,
InvalidTimestamp,
}
impl fmt::Display for ParseTimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Empty => write!(f, "empty time string"),
Self::InvalidNumber => write!(f, "invalid number"),
Self::InvalidUnit => write!(f, "invalid time unit"),
Self::InvalidFormat => write!(f, "invalid format"),
Self::InvalidTimestamp => write!(f, "invalid timestamp"),
}
}
}
impl core::error::Error for ParseTimeError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum TimeFilter {
Before(SystemTime),
After(SystemTime),
Between(SystemTime, SystemTime),
}
impl TimeFilter {
#[allow(clippy::missing_inline_in_public_items)]
pub fn from_string(s: &str) -> Result<Self, ParseTimeError> {
Self::parse_args(s).ok_or(ParseTimeError::InvalidFormat)
}
fn parse_args(start: &str) -> Option<Self> {
let s = start.trim();
if s.is_empty() {
return None;
}
if let Some((before_str, after_str)) = s.split_once("..") {
let before_time = Self::parse_relative_time(before_str.trim())?;
let after_time = Self::parse_relative_time(after_str.trim())?;
let (older, newer) = if before_time > after_time {
(before_time, after_time)
} else {
(after_time, before_time)
};
return Some(Self::Between(newer, older));
}
let (prefix, remaining) = s
.strip_prefix('+')
.map(|stripped| ("+", stripped))
.or_else(|| s.strip_prefix('-').map(|stripped| ("-", stripped)))
.unwrap_or(("", s));
let time = Self::parse_relative_time(remaining)?;
match prefix {
"+" => Some(Self::Before(time)), "-" | "" => Some(Self::After(time)), _ => None,
}
}
fn parse_relative_time(start_str: &str) -> Option<SystemTime> {
let s = start_str.trim().to_lowercase();
let digit_end = s.chars().position(|c| !c.is_ascii_digit())?;
let (num_str, unit_str) = s.split_at(digit_end);
let quantity: u64 = num_str.parse().ok()?;
let duration = match unit_str.trim() {
"s" | "sec" | "second" | "seconds" => Duration::from_secs(quantity),
"m" | "min" | "minute" | "minutes" => Duration::from_secs(quantity * 60),
"h" | "hour" | "hours" => Duration::from_secs(quantity * 3600),
"d" | "day" | "days" => Duration::from_secs(quantity * 86400),
"w" | "week" | "weeks" => Duration::from_secs(quantity * 604_800),
"y" | "year" | "years" => Duration::from_secs(quantity * 31_536_000),
_ => return None,
};
SystemTime::now().checked_sub(duration)
}
#[must_use]
#[inline]
pub fn matches_time(&self, file_time: SystemTime) -> bool {
match *self {
Self::Before(cutoff) => {
file_time.duration_since(cutoff).is_err()
}
Self::After(cutoff) => {
cutoff.duration_since(file_time).is_err()
}
Self::Between(newer, older) => {
let after_newer = newer.duration_since(file_time).is_err();
let before_older = file_time.duration_since(older).is_err();
after_newer && before_older
}
}
}
}
#[derive(Clone, Debug)]
#[allow(clippy::exhaustive_structs)]
pub struct TimeFilterParser;
impl TypedValueParser for TimeFilterParser {
type Value = TimeFilter;
#[allow(clippy::missing_inline_in_public_items)]
fn parse_ref(
&self,
cmd: &Command,
_arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, Error> {
let value_str = value
.to_str()
.ok_or_else(|| Error::new(ErrorKind::InvalidUtf8).with_cmd(cmd))?;
match TimeFilter::from_string(value_str) {
Ok(filter) => Ok(filter),
Err(err) => {
let mut error = Error::new(ErrorKind::InvalidValue).with_cmd(cmd);
error.insert(
ContextKind::InvalidValue,
ContextValue::String(format!("{err}")),
);
error.insert(
ContextKind::SuggestedValue,
ContextValue::Strings(vec![
"-1h".into(), "-30m".into(), "+2d".into(), "+1w".into(), "1d..2h".into(), ]),
);
error.insert(
ContextKind::Usage,
ContextValue::Strings(vec![
"Prefixes:".into(),
" -TIME - files modified within the last TIME (newer)".into(),
" +TIME - files modified more than TIME ago (older)".into(),
" TIME - same as -TIME (default)".into(),
" TIME..TIME - files modified between two times".into(),
]),
);
error.insert(
ContextKind::ValidValue,
ContextValue::Strings(vec![
"s, sec, second, seconds".into(),
"m, min, minute, minutes".into(),
"h, hour, hours".into(),
"d, day, days".into(),
"w, week, weeks".into(),
"y, year, years".into(),
]),
);
Err(error)
}
}
}
#[allow(clippy::missing_inline_in_public_items)] fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
[
PossibleValue::new("-1h").help("modified within the last hour"),
PossibleValue::new("-30m").help("modified within the last 30 minutes"),
PossibleValue::new("-1d").help("modified within the last day"),
PossibleValue::new("+2d").help("modified more than 2 days ago"),
PossibleValue::new("+1w").help("modified more than 1 week ago"),
PossibleValue::new("1d..2h").help("modified between 1 day and 2 hours ago"),
]
.into_iter(),
))
}
}