Add support for --changed-before and --changed-with for modification time based search

This commit is contained in:
Karim SENHAJI 2018-10-09 13:47:42 +02:00 committed by David Peter
parent 8691ab4bed
commit 54c117d72f
7 changed files with 211 additions and 2 deletions

35
Cargo.lock generated
View File

@ -82,6 +82,9 @@ dependencies = [
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ignore 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
@ -92,6 +95,16 @@ dependencies = [
"version_check 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "filetime"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "fnv"
version = "1.0.6"
@ -123,6 +136,19 @@ dependencies = [
"regex 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "humantime"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "if_chain"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ignore"
version = "0.4.3"
@ -198,6 +224,11 @@ dependencies = [
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quick-error"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rand"
version = "0.4.3"
@ -387,10 +418,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum crossbeam 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "24ce9782d4d5c53674646a6a4c1863a21a8fc0cb649b3c94dfc16e45071dea19"
"checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e"
"checksum diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3c2b69f912779fbb121ceb775d74d51e915af17aaebc38d28a592843a2dd0a3a"
"checksum filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "da4b9849e77b13195302c174324b5ba73eec9b236b24c221a61000daefb95c5f"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
"checksum globset 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8e49edbcc9c7fc5beb8c0a54e7319ff8bed353a2b55e85811c6281188c2a6c84"
"checksum humantime 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0484fda3e7007f2a4a0d9c3a703ca38c71c54c55602ce4660c419fd32e188c9e"
"checksum if_chain 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4bac95d9aa0624e7b78187d6fb8ab012b41d9f6f54b1bcb61e61c4845f8357ec"
"checksum ignore 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3e9faa7c84064f07b40da27044af629f578bc7994b650d3e458d0c29183c1d91"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca488b89a5657b0a2ecd45b95609b3e848cf1755da332a0da46e2b2b1cb371a7"
@ -399,6 +433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
"checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17"
"checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
"checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd"
"checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"

View File

@ -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"
@ -51,3 +53,4 @@ libc = "0.2"
[dev-dependencies]
diff = "0.1"
tempdir = "0.3"
filetime = "0.2.1"

View File

@ -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))
}
@ -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
}

View File

@ -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;
@ -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.
@ -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.
@ -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));
}
}

View File

@ -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;
@ -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;
@ -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"),
@ -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)

View File

@ -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()),

View File

@ -8,6 +8,8 @@
//! Integration tests for the CLI interface of fd.
extern crate filetime;
extern crate humantime;
extern crate regex;
mod testenv;
@ -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"];
@ -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",
);
}