Added support for -exec[dir] (#17)

* Deleted vertical whitespace

In an attempt to get around a mistake I made when merging from the
master fork

* Added a default implementation for has_side_effects

And removed all the "return false" implementations (as specified by most
of the Matchers).

* Added support for -exec and -execdir

* Fixed path_to_testing_commandline

* Minor tweaks from code review
This commit is contained in:
mcharsley
2017-03-17 17:02:45 +00:00
committed by GitHub
parent 593ede810f
commit 8b3842b1f9
15 changed files with 582 additions and 45 deletions
+5 -1
View File
@@ -14,4 +14,8 @@ regex = "0.2"
[[bin]]
name = "find"
path = "src/find/main.rs"
path = "src/find/main.rs"
[[bin]]
name = "testing-commandline"
path = "src/testing/commandline/main.rs"
+97
View File
@@ -0,0 +1,97 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use std::error::Error;
use std::ffi::OsString;
use std::io::{Write, stderr};
use std::path::Path;
use std::process::Command;
use walkdir::DirEntry;
use find::matchers::{Matcher, MatcherIO};
enum Arg {
Filename,
LiteralArg(OsString),
}
pub struct SingleExecMatcher {
executable: String,
args: Vec<Arg>,
exec_in_parent_dir: bool,
}
impl SingleExecMatcher {
pub fn new(executable: &str,
args: &[&str],
exec_in_parent_dir: bool)
-> Result<SingleExecMatcher, Box<Error>> {
let transformed_args = args.iter()
.map(|&a| match a {
"{}" => Arg::Filename,
_ => Arg::LiteralArg(OsString::from(a)),
})
.collect();
Ok(SingleExecMatcher {
executable: executable.to_string(),
args: transformed_args,
exec_in_parent_dir: exec_in_parent_dir,
})
}
pub fn new_box(executable: &str,
args: &[&str],
exec_in_parent_dir: bool)
-> Result<Box<Matcher>, Box<Error>> {
Ok(Box::new(SingleExecMatcher::new(executable, args, exec_in_parent_dir)?))
}
}
impl Matcher for SingleExecMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
let mut command = Command::new(&self.executable);
let path_to_file = if self.exec_in_parent_dir {
if let Some(f) = file_info.path().file_name() {
Path::new(".").join(f)
} else {
file_info.path().to_path_buf()
}
} else {
file_info.path().to_path_buf()
};
for arg in &self.args {
command.arg(match arg {
&Arg::LiteralArg(ref a) => a.as_os_str(),
&Arg::Filename => path_to_file.as_os_str(),
});
}
if self.exec_in_parent_dir {
if let Some(parent) = file_info.path().parent() {
command.current_dir(parent);
}
}
match command.status() {
Ok(status) => return status.success(),
Err(e) => {
writeln!(&mut stderr(), "Failed to run {}: {}", self.executable, e).unwrap();
return false;
}
}
}
fn has_side_effects(&self) -> bool {
return true;
}
}
#[cfg(test)]
/// No tests here, because we need to call out to an external executable. See
/// tests/exec_unit_tests.rs instead.
mod tests {}
+45 -8
View File
@@ -11,6 +11,7 @@
//! to "-foo -o ( -bar -baz )", not "( -foo -o -bar ) -baz").
use std::error::Error;
use std::iter::Iterator;
use std::path::PathBuf;
use walkdir::DirEntry;
use find::matchers::{Matcher, MatcherIO};
@@ -41,6 +42,18 @@ impl Matcher for AndMatcher {
fn has_side_effects(&self) -> bool {
self.submatchers.iter().any(|x| x.has_side_effects())
}
fn finished_dir(&self, dir: &PathBuf) {
for m in &self.submatchers {
m.finished_dir(dir);
}
}
fn finished(&self) {
for m in &self.submatchers {
m.finished();
}
}
}
pub struct AndMatcherBuilder {
@@ -97,6 +110,18 @@ impl Matcher for OrMatcher {
fn has_side_effects(&self) -> bool {
self.submatchers.iter().any(|x| x.has_side_effects())
}
fn finished_dir(&self, dir: &PathBuf) {
for m in &self.submatchers {
m.finished_dir(dir);
}
}
fn finished(&self) {
for m in &self.submatchers {
m.finished();
}
}
}
pub struct OrMatcherBuilder {
@@ -171,6 +196,18 @@ impl Matcher for ListMatcher {
fn has_side_effects(&self) -> bool {
self.submatchers.iter().any(|x| x.has_side_effects())
}
fn finished_dir(&self, dir: &PathBuf) {
for m in &self.submatchers {
m.finished_dir(dir);
}
}
fn finished(&self) {
for m in &self.submatchers {
m.finished();
}
}
}
pub struct ListMatcherBuilder {
@@ -249,10 +286,6 @@ impl Matcher for TrueMatcher {
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
true
}
fn has_side_effects(&self) -> bool {
false
}
}
/// A simple matcher that never matches.
@@ -262,10 +295,6 @@ impl Matcher for FalseMatcher {
fn matches(&self, _dir_entry: &DirEntry, _: &mut MatcherIO) -> bool {
false
}
fn has_side_effects(&self) -> bool {
false
}
}
impl FalseMatcher {
@@ -298,6 +327,14 @@ impl Matcher for NotMatcher {
fn has_side_effects(&self) -> bool {
self.submatcher.has_side_effects()
}
fn finished_dir(&self, dir: &PathBuf) {
self.submatcher.finished_dir(dir);
}
fn finished(&self) {
self.submatcher.finished();
}
}
#[cfg(test)]
+74 -6
View File
@@ -4,6 +4,7 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
pub mod exec;
mod logical_matchers;
mod name;
mod printer;
@@ -14,6 +15,7 @@ mod type_matcher;
use regex::Regex;
use std::error::Error;
use std::path::PathBuf;
use std::time::SystemTime;
use walkdir::DirEntry;
@@ -57,11 +59,23 @@ pub trait Matcher {
/// Returns whether the given file matches the object's predicate.
fn matches(&self, file_info: &DirEntry, matcher_io: &mut MatcherIO) -> bool;
/// Returns whether the matcher has any side-effects. Iff no such matcher
/// exists in the chain, then the filename will be printed to stdout. While
/// this is a compile-time fact for most matchers, it's run-time for matchers
/// that contain a collection of sub-Matchers.
fn has_side_effects(&self) -> bool;
/// Returns whether the matcher has any side-effects (e.g. executing a
/// command, deleting a file). Iff no such matcher exists in the chain, then
/// the filename will be printed to stdout. While this is a compile-time
/// fact for most matchers, it's run-time for matchers that contain a
/// collection of sub-Matchers.
fn has_side_effects(&self) -> bool {
// most matchers don't have side-effects, so supply a default implementation.
return false;
}
/// Notification that find has finished processing a given directory.
fn finished_dir(&self, _finished_directory: &PathBuf) {}
/// Notification that find has finished processing all directories -
/// allowing for any cleanup that isn't suitable for destructors (e.g.
/// blocking calls, I/O etc.)
fn finished(&self) {}
}
pub enum ComparableValue {
@@ -239,7 +253,30 @@ fn build_matcher_tree(args: &[&str],
i += 1;
Some(size::SizeMatcher::new_box(size, &unit)?)
}
"-exec" | "-execdir" => {
let mut arg_index = i + 1;
while arg_index < args.len() && args[arg_index] != ";" {
if args[arg_index] == "+" {
// MultiExecMatcher isn't written yet
return Err(From::from(format!("{} [args...] + isn't supported yet. \
Only {} [args...] ;",
args[i],
args[i])));
}
arg_index += 1;
}
if arg_index < i + 2 || arg_index == args.len() {
// at the minimum we need the executable and the ';'
return Err(From::from(format!("missing argument to {}", args[i])));
}
let expression = args[i];
let executable = args[i + 1];
let exec_args = &args[i + 2..arg_index];
i = arg_index;
Some(exec::SingleExecMatcher::new_box(executable,
exec_args,
expression == "-execdir")?)
}
"-prune" => Some(prune::PruneMatcher::new_box()),
"-not" | "!" => {
if !are_more_expressions(args, i) {
@@ -728,4 +765,35 @@ mod tests {
panic!("parsing a bad ctime value should fail");
}
}
#[test]
fn build_top_level_exec_not_enough_args() {
let mut config = Config::default();
if let Err(e) = build_top_level_matcher(&["-exec"], &mut config) {
assert!(e.description().contains("missing argument"));
} else {
panic!("parsing argument list with exec and no executable or semi-colon should fail");
}
if let Err(e) = build_top_level_matcher(&["-exec", ";"], &mut config) {
assert!(e.description().contains("missing argument"));
} else {
panic!("parsing argument list with exec and no executable should fail");
}
if let Err(e) = build_top_level_matcher(&["-exec", "foo"], &mut config) {
assert!(e.description().contains("missing argument"));
} else {
panic!("parsing argument list with exec and no executable should fail");
}
}
#[test]
fn build_top_level_exec_should_eat_args() {
let mut config = Config::default();
build_top_level_matcher(&["-exec", "foo", "-o", "(", ";"], &mut config)
.expect("parsing argument list with exec that takes brackets and -os should work");
}
}
-8
View File
@@ -32,10 +32,6 @@ impl Matcher for NameMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
self.pattern.matches(file_info.file_name().to_string_lossy().as_ref())
}
fn has_side_effects(&self) -> bool {
false
}
}
/// This matcher makes a case-insensitive comparison of the name against a
@@ -61,10 +57,6 @@ impl super::Matcher for CaselessNameMatcher {
self.pattern
.matches(file_info.file_name().to_string_lossy().to_lowercase().as_ref())
}
fn has_side_effects(&self) -> bool {
false
}
}
-4
View File
@@ -26,10 +26,6 @@ impl Matcher for PruneMatcher {
matcher_io.mark_current_dir_to_be_skipped();
true
}
fn has_side_effects(&self) -> bool {
false
}
}
#[cfg(test)]
-4
View File
@@ -103,10 +103,6 @@ impl Matcher for SizeMatcher {
}
}
}
fn has_side_effects(&self) -> bool {
false
}
}
#[cfg(test)]
-8
View File
@@ -57,10 +57,6 @@ impl Matcher for NewerMatcher {
Ok(t) => t,
}
}
fn has_side_effects(&self) -> bool {
false
}
}
#[derive(Clone, Copy, Debug)]
@@ -102,10 +98,6 @@ impl Matcher for FileTimeMatcher {
Ok(t) => t,
}
}
fn has_side_effects(&self) -> bool {
false
}
}
-4
View File
@@ -37,10 +37,6 @@ impl Matcher for TypeMatcher {
fn matches(&self, file_info: &DirEntry, _: &mut MatcherIO) -> bool {
(self.file_type_fn)(&file_info.file_type())
}
fn has_side_effects(&self) -> bool {
false
}
}
#[cfg(test)]
+2 -2
View File
@@ -4,7 +4,7 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
mod matchers;
pub mod matchers;
use std::cell::RefCell;
use std::error::Error;
@@ -171,6 +171,7 @@ Early alpha implementation. Currently the only expressions supported are
-atime [+-]N
-mtime [+-]N
-newer path_to_file
-exec[dir] executable [args] [{{}}] [more args] ;
-sorted
a non-standard extension that sorts directory contents by name before
processing them. Less efficient, but allows for deterministic output.
@@ -499,5 +500,4 @@ mod tests {
assert_eq!(deps.get_output_as_string(), "");
}
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use std::env;
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
fn usage() -> ! {
println!("Simple command-line app just used for testing -exec flags!");
std::process::exit(2);
}
#[derive(Default)]
struct Config {
exit_with_failure: bool,
destination_dir: String,
}
fn open_file(destination_dir: &str) -> File {
let mut file_number =
fs::read_dir(destination_dir).expect("failed to read destination").count();
loop {
file_number += 1;
let mut file_path: PathBuf = PathBuf::from(destination_dir);
file_path.push(format!("{}.txt", file_number));
if let Ok(f) = OpenOptions::new()
.write(true)
.create_new(true)
.open(file_path) {
return f;
}
}
}
fn main() {
let args = env::args().collect::<Vec<String>>();
if args.len() < 2 || args[1] == "-h" || args[1] == "--help" {
usage();
}
let mut config = Config::default();
config.destination_dir = args[1].clone();
for arg in &args[2..] {
if arg.starts_with("--") {
match arg.as_ref() {
"--exit_with_failure" => {
config.exit_with_failure = true;
}
_ => {
usage();
}
}
}
}
{
let mut f = open_file(&config.destination_dir);
// first two args are going to be the path to this executable and
// the destination_dir we want to write to. Don't write either of those
// as they'll be non-deterministic.
f.write_fmt(format_args!("cwd={}\nargs={:?}\n",
env::current_dir().unwrap().to_string_lossy(),
&args[2..]))
.expect("failed to write to file");
}
std::process::exit(if config.exit_with_failure { 2 } else { 0 });
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// As this module is included by all the integration tests, any function used
// in one test but not another can cause a dead code warning.
#[allow(dead_code)]
pub mod test_helpers;
+86
View File
@@ -0,0 +1,86 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
use std::cell::RefCell;
use std::env;
use std::io::{Cursor, Read, Write};
use std::time::SystemTime;
use std::vec::Vec;
use walkdir::{DirEntry, WalkDir};
use findutils::find::matchers::MatcherIO;
use findutils::find::Dependencies;
/// A copy of find::tests::FakeDependencies.
/// TODO: find out how to share #[cfg(test)] functions/structs between unit
/// and integration tests.
pub struct FakeDependencies {
pub output: RefCell<Cursor<Vec<u8>>>,
now: SystemTime,
}
impl<'a> FakeDependencies {
pub fn new() -> FakeDependencies {
FakeDependencies {
output: RefCell::new(Cursor::new(Vec::<u8>::new())),
now: SystemTime::now(),
}
}
pub fn new_matcher_io(&'a self) -> MatcherIO<'a> {
MatcherIO::new(self)
}
pub fn get_output_as_string(&self) -> String {
let mut cursor = self.output.borrow_mut();
cursor.set_position(0);
let mut contents = String::new();
cursor.read_to_string(&mut contents).unwrap();
contents
}
}
impl<'a> Dependencies<'a> for FakeDependencies {
fn get_output(&'a self) -> &'a RefCell<Write> {
&self.output
}
fn now(&'a self) -> SystemTime {
self.now
}
}
pub fn path_to_testing_commandline() -> String {
let mut path_to_use = env::current_exe()
// this will be something along the lines of /my/homedir/findutils/target/debug/deps/findutils-5532804878869ef1
.expect("can't find path of this executable")
.parent()
.expect("can't find parent directory of this executable")
.to_path_buf();
// and we want /my/homedir/findutils/target/debug/testing-commandline
if path_to_use.ends_with("deps") {
path_to_use.pop();
}
path_to_use = path_to_use.join("testing-commandline");
path_to_use.to_string_lossy()
.to_string()
}
/// A copy of find::tests::FakeDependencies.
/// TODO: find out how to share #[cfg(test)] functions/structs between unit
/// and integration tests.
pub fn get_dir_entry_for(directory: &str, filename: &str) -> DirEntry {
for wrapped_dir_entry in WalkDir::new(directory) {
let dir_entry = wrapped_dir_entry.unwrap();
if dir_entry.file_name().to_string_lossy() == filename {
return dir_entry;
}
}
panic!("Couldn't find {} in {}", directory, filename);
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
/// ! This file contains what would be normally be unit tests for find::matchers::exec.
/// ! But as the tests require running an external executable, they need to be run
/// ! as integration tests so we can ensure that our testing-commandline binary
/// ! has been built.
extern crate findutils;
extern crate tempdir;
extern crate walkdir;
use std::env;
use std::fs::File;
use std::io::Read;
use tempdir::TempDir;
use findutils::find::matchers::Matcher;
use findutils::find::matchers::exec::*;
use common::test_helpers::*;
mod common;
#[test]
fn matching_executes_code() {
let temp_dir = TempDir::new("matching_executes_code").unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = SingleExecMatcher::new(&path_to_testing_commandline(),
&vec![temp_dir_path.as_ref(), "abc", "{}", "xyz"],
false)
.expect("Failed to create matcher");
let deps = FakeDependencies::new();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
let mut s = String::new();
f.read_to_string(&mut s).expect("failed to read output file");
assert_eq!(s,
format!("cwd={}\nargs=[\"abc\", \"test_data/simple/abbbc\", \"xyz\"]\n",
env::current_dir().unwrap().to_string_lossy()));
}
#[test]
fn matching_executes_code_in_files_directory() {
let temp_dir = TempDir::new("matching_executes_code_in_files_directory").unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = SingleExecMatcher::new(&path_to_testing_commandline(),
&vec![temp_dir_path.as_ref(), "abc", "{}", "xyz"],
true)
.expect("Failed to create matcher");
let deps = FakeDependencies::new();
assert!(matcher.matches(&abbbc, &mut deps.new_matcher_io()));
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
let mut s = String::new();
f.read_to_string(&mut s).expect("failed to read output file");
assert_eq!(s,
format!("cwd={}/test_data/simple\nargs=[\"abc\", \"./abbbc\", \"xyz\"]\n",
env::current_dir().unwrap().to_string_lossy()));
}
#[test]
fn matching_fails_if_executable_fails() {
let temp_dir = TempDir::new("matching_fails_if_executable_fails").unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let abbbc = get_dir_entry_for("test_data/simple", "abbbc");
let matcher = SingleExecMatcher::new(&path_to_testing_commandline(),
&vec![temp_dir_path.as_ref(),
"--exit_with_failure",
"abc",
"{}",
"xyz"],
true)
.expect("Failed to create matcher");
let deps = FakeDependencies::new();
assert!(!matcher.matches(&abbbc, &mut deps.new_matcher_io()));
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
let mut s = String::new();
f.read_to_string(&mut s).expect("failed to read output file");
assert_eq!(s,
format!("cwd={}/test_data/simple\nargs=[\"--exit_with_failure\", \"abc\", \
\"./abbbc\", \"xyz\"]\n",
env::current_dir().unwrap().to_string_lossy()));
}
+93
View File
@@ -0,0 +1,93 @@
// Copyright 2017 Google Inc.
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
/// ! This file contains what would be normally be unit tests for find::find_main
/// ! related to -exec[dir] and ok[dir] clauses.
/// ! But as the tests require running an external executable, they need to be run
/// ! as integration tests so we can ensure that our testing-commandline binary
/// ! has been built.
extern crate findutils;
extern crate tempdir;
extern crate walkdir;
use std::env;
use std::fs::File;
use std::io::Read;
use tempdir::TempDir;
use findutils::find::find_main;
use common::test_helpers::*;
mod common;
#[test]
fn find_exec() {
let temp_dir = TempDir::new("find_exec").unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let deps = FakeDependencies::new();
let rc = find_main(&["find",
"./test_data/simple/subdir",
"-type",
"f",
"-exec",
&path_to_testing_commandline(),
temp_dir_path.as_ref(),
"(",
"{}",
"-o",
";"],
&deps);
assert_eq!(rc, 0);
// exec has side effects, so we won't output anything unless -print is
// explicitly passed in.
assert_eq!(deps.get_output_as_string(), "");
// check the executable ran as expected
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
let mut s = String::new();
f.read_to_string(&mut s).expect("failed to read output file");
assert_eq!(s,
format!("cwd={}\nargs=[\"(\", \"./test_data/simple/subdir/ABBBC\", \"-o\"]\n",
env::current_dir().unwrap().to_string_lossy()));
}
#[test]
fn find_execdir() {
let temp_dir = TempDir::new("find_execdir").unwrap();
let temp_dir_path = temp_dir.path().to_string_lossy();
let deps = FakeDependencies::new();
// only look at files because the "size" of a directory is a system (and filesystem)
// dependent thing and we want these tests to be universal.
let rc = find_main(&["find",
"./test_data/simple/subdir",
"-type",
"f",
"-execdir",
&path_to_testing_commandline(),
temp_dir_path.as_ref(),
")",
"{}",
",",
";"],
&deps);
assert_eq!(rc, 0);
// exec has side effects, so we won't output anything unless -print is
// explicitly passed in.
assert_eq!(deps.get_output_as_string(), "");
// check the executable ran as expected
let mut f = File::open(temp_dir.path().join("1.txt")).expect("Failed to open output file");
let mut s = String::new();
f.read_to_string(&mut s).expect("failed to read output file");
assert_eq!(s,
format!("cwd={}/test_data/simple/subdir\nargs=[\")\", \"./ABBBC\", \",\"]\n",
env::current_dir().unwrap().to_string_lossy()));
}