Skip to content

Commit

Permalink
Add support for --changed-before and --changed-with for modification …
Browse files Browse the repository at this point in the history
…time based search
  • Loading branch information
Karim SENHAJI committed Oct 9, 2018
1 parent 8691ab4 commit 6e093a5
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 4 deletions.
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ num_cpus = "1.8"
regex = "1.0.0"
regex-syntax = "0.6"
ctrlc = "3.1"
humantime = "1.1.1"
if_chain = "0.1.3"

[dependencies.clap]
version = "2.31.2"
Expand All @@ -51,3 +53,4 @@ libc = "0.2"
[dev-dependencies]
diff = "0.1"
tempdir = "0.3"
filetime = "0.2.1"
22 changes: 22 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ pub fn build_app() -> App<'static, 'static> {
.takes_value(true)
.hidden(true),
)
.arg(
arg("changed-within")
.long("changed-within")
.takes_value(true)
.number_of_values(1)
)
.arg(
arg("changed-before")
.long("changed-before")
.takes_value(true)
.number_of_values(1)
)
.arg(arg("pattern"))
.arg(arg("path").multiple(true))
}
Expand Down Expand Up @@ -292,5 +304,15 @@ fn usage() -> HashMap<&'static str, Help> {
'mi': mebibytes\n \
'gi': gibibytes\n \
'ti': tebibytes");
doc!(h, "changed-before"
, "Limit results based on modification time older than duration or date provided."
, "Limit results based on modification time older than duration provided:\n \
using a duration: <NUM>d <NUM>h <NUM>m <NUM>s (e.g. 10h, 1d, 35min...)\n \
or a date and time: YYYY-MM-DD HH:MM:SS");
doc!(h, "changed-within"
, "Limit results based on modification time within the duration provided or between date provided and now."
, "Limit results based on modification time within the duration provided:\n \
using a duration: <NUM>d <NUM>h <NUM>m <NUM>s (e.g. 10h, 1d, 35min...)\n \
or a date and time: YYYY-MM-DD HH:MM:SS");
h
}
51 changes: 50 additions & 1 deletion src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use std::ffi::OsString;
use std::path::PathBuf;
use std::process;
use std::time;
use std::time::{self, SystemTime};

use exec::CommandTemplate;
use lscolors::LsColors;
Expand Down Expand Up @@ -100,6 +100,38 @@ impl SizeFilter {
}
}

/// Filter based on time ranges
#[derive(Debug, PartialEq)]
pub enum TimeFilter {
Before(SystemTime),
After(SystemTime),
}

impl TimeFilter {
fn from_str(s: &str) -> Option<SystemTime> {
use humantime;
humantime::parse_duration(s)
.map(|duration| SystemTime::now() - duration)
.or_else(|_| humantime::parse_rfc3339_weak(s))
.ok()
}

pub fn before(s: &str) -> Option<TimeFilter> {
Some(TimeFilter::Before(TimeFilter::from_str(s)?))
}

pub fn after(s: &str) -> Option<TimeFilter> {
Some(TimeFilter::After(TimeFilter::from_str(s)?))
}

pub fn is_within(&self, t: &SystemTime) -> bool {
match self {
TimeFilter::Before(limit) => t <= limit,
TimeFilter::After(limit) => t >= limit,
}
}
}

/// Configuration options for *fd*.
pub struct FdOptions {
/// Whether the search is case-sensitive or case-insensitive.
Expand Down Expand Up @@ -162,6 +194,9 @@ pub struct FdOptions {

/// The given constraints on the size of returned files
pub size_constraints: Vec<SizeFilter>,

/// Constraints on last modification time of files
pub modification_constraints: Vec<TimeFilter>,
}

/// Print error message to stderr.
Expand Down Expand Up @@ -468,4 +503,18 @@ mod tests {
let f = SizeFilter::from_string("+1K").unwrap();
assert!(f.is_within(1000));
}

#[test]
fn is_time_within() {
let now = SystemTime::now();
assert!(TimeFilter::after("1min").unwrap().is_within(&now));
assert!(!TimeFilter::before("1min").unwrap().is_within(&now));

let t1m_ago = SystemTime::now() - time::Duration::from_secs(60);
assert!(!TimeFilter::after("30sec").unwrap().is_within(&t1m_ago));
assert!(TimeFilter::after("2min").unwrap().is_within(&t1m_ago));

assert!(TimeFilter::before("30sec").unwrap().is_within(&t1m_ago));
assert!(!TimeFilter::before("2min").unwrap().is_within(&t1m_ago));
}
}
25 changes: 22 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ extern crate clap;
extern crate ignore;
#[macro_use]
extern crate lazy_static;
extern crate humantime;
#[cfg(all(unix, not(target_os = "redox")))]
extern crate libc;
extern crate num_cpus;
extern crate regex;
extern crate regex_syntax;
#[macro_use]
extern crate if_chain;

mod app;
mod exec;
Expand All @@ -40,7 +43,7 @@ use regex::{RegexBuilder, RegexSetBuilder};
use exec::CommandTemplate;
use internal::{
pattern_has_uppercase_char, print_error_and_exit, transform_args_with_exec, FdOptions,
FileTypes, SizeFilter,
FileTypes, SizeFilter, TimeFilter,
};
use lscolors::LsColors;

Expand Down Expand Up @@ -150,6 +153,22 @@ fn main() {
})
.unwrap_or_else(|| vec![]);

let mut modification_constraints: Vec<TimeFilter> = Vec::new();
if let Some(t) = matches.value_of("changed-within") {
if let Some(f) = TimeFilter::after(t) {
modification_constraints.push(f);
} else {
print_error_and_exit(&format!("Error: {} is not a valid time.", t));
}
}
if let Some(t) = matches.value_of("changed-before") {
if let Some(f) = TimeFilter::before(t) {
modification_constraints.push(f);
} else {
print_error_and_exit(&format!("Error: {} is not a valid time.", t));
}
}

let config = FdOptions {
case_sensitive,
search_full_path: matches.is_present("full-path"),
Expand Down Expand Up @@ -226,6 +245,7 @@ fn main() {
.map(|vs| vs.map(PathBuf::from).collect())
.unwrap_or_else(|| vec![]),
size_constraints: size_limits,
modification_constraints,
};

match RegexBuilder::new(&pattern_regex)
Expand All @@ -239,8 +259,7 @@ fn main() {
"{}\nHint: You can use the '--fixed-strings' option to search for a \
literal string instead of a regular expression",
err.description()
)
.as_str(),
).as_str(),
),
}
}
15 changes: 15 additions & 0 deletions src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,21 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) {
}
}

// Filter out unwanted modification times
if !config.modification_constraints.is_empty() {
if_chain!{
if entry_path.is_file();
if let Ok(metadata) = entry_path.metadata();
if let Ok(modified) = metadata.modified();
if config.modification_constraints.iter().all(|tf| tf.is_within(&modified));
then {
// When all is good, we just continue with pattern match
} else {
return ignore::WalkState::Continue;
}
}
}

let search_str_o = if config.search_full_path {
match fshelper::path_absolute_form(entry_path) {
Ok(path_abs_buf) => Some(path_abs_buf.to_string_lossy().into_owned().into()),
Expand Down
65 changes: 65 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

//! Integration tests for the CLI interface of fd.

extern crate filetime;
extern crate humantime;
extern crate regex;

mod testenv;
Expand All @@ -16,6 +18,7 @@ use regex::escape;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::time::{Duration, SystemTime};
use testenv::TestEnv;

static DEFAULT_DIRS: &'static [&'static str] = &["one/two/three", "one/two/three/directory_foo"];
Expand Down Expand Up @@ -1110,3 +1113,65 @@ fn test_size() {
// Files with size equal 4 kibibytes.
te.assert_output(&["", "--size", "+4ki", "--size", "-4ki"], "4_kibibytes.foo");
}

#[cfg(test)]
fn create_file_with_modified<P: AsRef<Path>>(path: P, duration_in_secs: u64) {
let st = SystemTime::now() - Duration::from_secs(duration_in_secs);
let ft = filetime::FileTime::from_system_time(st);
fs::File::create(&path).expect("creation failed");
filetime::set_file_times(&path, ft, ft).expect("time modification failed");
}

#[test]
fn test_modified_relative() {
let te = TestEnv::new(&[], &[]);
create_file_with_modified(te.test_root().join("0_now"), 0);
create_file_with_modified(te.test_root().join("1_min"), 60);
create_file_with_modified(te.test_root().join("10_min"), 600);
create_file_with_modified(te.test_root().join("1_h"), 60 * 60);
create_file_with_modified(te.test_root().join("2_h"), 2 * 60 * 60);
create_file_with_modified(te.test_root().join("1_day"), 24 * 60 * 60);

te.assert_output(
&["", "--changed-within", "15min"],
"0_now
1_min
10_min",
);

te.assert_output(
&["", "--changed-before", "15min"],
"1_h
2_h
1_day",
);

te.assert_output(
&["min", "--changed-within", "12h"],
"1_min
10_min",
);
}

#[cfg(test)]
fn change_file_modified<P: AsRef<Path>>(path: P, iso_date: &str) {
let st = humantime::parse_rfc3339(iso_date).expect("invalid date");
let ft = filetime::FileTime::from_system_time(st);
filetime::set_file_times(path, ft, ft).expect("time modification failde");
}

#[test]
fn test_modified_asolute() {
let te = TestEnv::new(&[], &["15mar2018", "30dec2017"]);
change_file_modified(te.test_root().join("15mar2018"), "2018-03-15T12:00:00Z");
change_file_modified(te.test_root().join("30dec2017"), "2017-12-30T23:59:00Z");

te.assert_output(
&["", "--changed-within", "2018-01-01 00:00:00"],
"15mar2018",
);
te.assert_output(
&["", "--changed-before", "2018-01-01 00:00:00"],
"30dec2017",
);
}

0 comments on commit 6e093a5

Please sign in to comment.