diff --git a/Cargo.toml b/Cargo.toml index 463ec41..ea2c067 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,6 @@ num_cpus = "1.6.2" regex = "0.2" regex-syntax = "0.4" ctrlc = "3.0" -shell-escape = "0.1.3" [target.'cfg(all(unix, not(target_os = "redox")))'.dependencies] libc = "0.2" diff --git a/src/app.rs b/src/app.rs index 9eadbd8..3575c68 100644 --- a/src/app.rs +++ b/src/app.rs @@ -124,7 +124,7 @@ pub fn build_app() -> App<'static, 'static> { .hidden(true), ) .arg(arg("pattern")) - .arg(arg("path")) + .arg(arg("path").multiple(true)) } #[cfg_attr(rustfmt, rustfmt_skip)] diff --git a/src/internal.rs b/src/internal.rs index 699aeac..0f1e42d 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -15,16 +15,6 @@ use lscolors::LsColors; use walk::FileType; use regex_syntax::{Expr, ExprBuilder}; -/// Defines how to display search result paths. -#[derive(PartialEq)] -pub enum PathDisplay { - /// As an absolute path - Absolute, - - /// As a relative path - Relative, -} - /// Configuration options for *fd*. pub struct FdOptions { /// Whether the search is case-sensitive or case-insensitive. @@ -63,9 +53,6 @@ pub struct FdOptions { /// `max_buffer_time`. pub max_buffer_time: Option, - /// Display results as relative or absolute path. - pub path_display: PathDisplay, - /// `None` if the output should not be colorized. Otherwise, a `LsColors` instance that defines /// how to style different filetypes. pub ls_colors: Option, diff --git a/src/main.rs b/src/main.rs index 0b42b25..2b02079 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ use atty::Stream; use regex::RegexBuilder; use exec::CommandTemplate; -use internal::{error, pattern_has_uppercase_char, FdOptions, PathDisplay}; +use internal::{error, pattern_has_uppercase_char, FdOptions}; use lscolors::LsColors; use walk::FileType; @@ -56,28 +56,31 @@ fn main() { error("Error: could not get current directory."); } - // Get the root directory for the search - let mut root_dir_buf = match matches.value_of("path") { - Some(path) => PathBuf::from(path), - None => current_dir.to_path_buf(), - }; - if !fshelper::is_dir(&root_dir_buf) { - error(&format!( - "Error: '{}' is not a directory.", - root_dir_buf.to_string_lossy() - )); - } - - let path_display = if matches.is_present("absolute-path") || root_dir_buf.is_absolute() { - PathDisplay::Absolute - } else { - PathDisplay::Relative + //Get one or more root directories to search. + let mut dir_vec: Vec<_> = match matches.values_of("path") { + Some(paths) => { + paths + .map(|path| { + let path_buffer = PathBuf::from(path); + if !fshelper::is_dir(&path_buffer) { + error(&format!( + "Error: '{}' is not a directory.", + path_buffer.to_string_lossy() + )); + } + path_buffer + }) + .collect::>() + } + None => vec![current_dir.to_path_buf()], }; - if path_display == PathDisplay::Absolute && root_dir_buf.is_relative() { - root_dir_buf = fshelper::absolute_path(root_dir_buf.as_path()).unwrap(); + if matches.is_present("absolute-path") { + dir_vec = dir_vec + .iter() + .map(|path_buffer| fshelper::absolute_path(&path_buffer).unwrap()) + .collect(); } - let root_dir = root_dir_buf.as_path(); // The search will be case-sensitive if the command line flag is set or // if the pattern has an uppercase character (smart case). @@ -132,7 +135,6 @@ fn main() { .value_of("max-buffer-time") .and_then(|n| u64::from_str_radix(n, 10).ok()) .map(time::Duration::from_millis), - path_display, ls_colors, file_type: match matches.value_of("file-type") { Some("f") | Some("file") => FileType::RegularFile, @@ -151,11 +153,12 @@ fn main() { .unwrap_or_else(|| vec![]), }; + match RegexBuilder::new(pattern) .case_insensitive(!config.case_sensitive) .dot_matches_new_line(true) .build() { - Ok(re) => walk::scan(root_dir, Arc::new(re), Arc::new(config)), + Ok(re) => walk::scan(&dir_vec, Arc::new(re), Arc::new(config)), Err(err) => error(err.description()), } } diff --git a/src/walk.rs b/src/walk.rs index 4737787..626ea19 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -13,7 +13,7 @@ use fshelper; use internal::{error, FdOptions}; use output; -use std::path::Path; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::channel; @@ -48,11 +48,15 @@ pub enum FileType { /// If the `--exec` argument was supplied, this will create a thread pool for executing /// jobs in parallel from a given command line and the discovered paths. Otherwise, each /// path will simply be written to standard output. -pub fn scan(root: &Path, pattern: Arc, config: Arc) { +pub fn scan(path_vec: &Vec, pattern: Arc, config: Arc) { + let mut path_iter = path_vec.iter(); + let first_path_buf = path_iter.next().expect( + "Error: Path vector can not be empty", + ); let (tx, rx) = channel(); let threads = config.threads; - let mut override_builder = OverrideBuilder::new(root); + let mut override_builder = OverrideBuilder::new(first_path_buf.as_path()); for pattern in &config.exclude_patterns { let res = override_builder.add(pattern); @@ -64,7 +68,8 @@ pub fn scan(root: &Path, pattern: Arc, config: Arc) { error("Mismatch in exclude patterns"); }); - let walker = WalkBuilder::new(root) + let mut walker = WalkBuilder::new(first_path_buf.as_path()); + walker .hidden(config.ignore_hidden) .ignore(config.read_ignore) .git_ignore(config.read_gitignore) @@ -73,16 +78,20 @@ pub fn scan(root: &Path, pattern: Arc, config: Arc) { .git_exclude(config.read_gitignore) .overrides(overrides) .follow_links(config.follow_links) - .max_depth(config.max_depth) - .threads(threads) - .build_parallel(); + .max_depth(config.max_depth); + + for path_entry in path_iter { + walker.add(path_entry.as_path()); + } + + let parallel_walker = walker.threads(threads).build_parallel(); let wants_to_quit = Arc::new(AtomicBool::new(false)); - if let Some(_) = config.ls_colors { let wq = Arc::clone(&wants_to_quit); ctrlc::set_handler(move || { wq.store(true, Ordering::Relaxed); }).unwrap(); } + // Spawn the thread that receives all results through the channel. let rx_config = Arc::clone(&config); let receiver_thread = thread::spawn(move || { @@ -164,11 +173,10 @@ pub fn scan(root: &Path, pattern: Arc, config: Arc) { }); // Spawn the sender threads. - walker.run(|| { + parallel_walker.run(|| { let config = Arc::clone(&config); let pattern = Arc::clone(&pattern); let tx_thread = tx.clone(); - let root = root.to_owned(); Box::new(move |entry_o| { let entry = match entry_o { @@ -178,7 +186,7 @@ pub fn scan(root: &Path, pattern: Arc, config: Arc) { let entry_path = entry.path(); - if entry_path == root { + if entry.depth() == 0 { return ignore::WalkState::Continue; } diff --git a/tests/testenv/mod.rs b/tests/testenv/mod.rs index ceed8fe..eb75d48 100644 --- a/tests/testenv/mod.rs +++ b/tests/testenv/mod.rs @@ -35,24 +35,22 @@ pub struct TestEnv { } /// Create the working directory and the test files. -fn create_working_directory() -> Result { +fn create_working_directory( + directories: &[&'static str], + files: &[&'static str], +) -> Result { let temp_dir = TempDir::new("fd-tests")?; { let root = temp_dir.path(); - fs::create_dir_all(root.join("one/two/three"))?; + for directory in directories { + fs::create_dir_all(root.join(directory))?; + } - fs::File::create(root.join("a.foo"))?; - fs::File::create(root.join("one/b.foo"))?; - fs::File::create(root.join("one/two/c.foo"))?; - fs::File::create(root.join("one/two/C.Foo2"))?; - fs::File::create(root.join("one/two/three/d.foo"))?; - fs::create_dir(root.join("one/two/three/directory_foo"))?; - fs::File::create(root.join("ignored.foo"))?; - fs::File::create(root.join("gitignored.foo"))?; - fs::File::create(root.join(".hidden.foo"))?; - fs::File::create(root.join("e1 e2"))?; + for file in files { + fs::File::create(root.join(file))?; + } #[cfg(unix)] unix::fs::symlink(root.join("one/two"), root.join("symlink"))?; @@ -138,8 +136,8 @@ fn normalize_output(s: &str, trim_left: bool) -> String { } impl TestEnv { - pub fn new() -> TestEnv { - let temp_dir = create_working_directory().expect("working directory"); + pub fn new(directories: &[&'static str], files: &[&'static str]) -> TestEnv { + let temp_dir = create_working_directory(directories, files).expect("working directory"); let fd_exe = find_fd_exe(); TestEnv { diff --git a/tests/tests.rs b/tests/tests.rs index 975a91b..261f3a1 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -15,6 +15,20 @@ mod testenv; use testenv::TestEnv; use regex::escape; +static DEFAULT_DIRS: &'static [&'static str] = &["one/two/three", "one/two/three/directory_foo"]; + +static DEFAULT_FILES: &'static [&'static str] = &[ + "a.foo", + "one/b.foo", + "one/two/c.foo", + "one/two/C.Foo2", + "one/two/three/d.foo", + "ignored.foo", + "gitignored.foo", + ".hidden.foo", + "e1 e2", +]; + fn get_absolute_root_path(env: &TestEnv) -> String { let path = env.test_root() .canonicalize() @@ -32,7 +46,7 @@ fn get_absolute_root_path(env: &TestEnv) -> String { /// Simple tests #[test] fn test_simple() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output(&["a.foo"], "a.foo"); te.assert_output(&["b.foo"], "one/b.foo"); @@ -64,10 +78,34 @@ fn test_simple() { ); } +/// Test multiple directory searches +#[test] +fn test_multi_file() { + let dirs = &["test1", "test2"]; + let files = &["test1/a.foo", "test1/b.foo", "test2/a.foo"]; + let te = TestEnv::new(dirs, files); + te.assert_output( + &["a.foo", "test1", "test2"], + "test1/a.foo + test2/a.foo", + ); + + te.assert_output( + &["", "test1", "test2"], + "test1/a.foo + test2/a.foo + test1/b.foo", + ); + + te.assert_output(&["a.foo", "test1"], "test1/a.foo"); + + te.assert_output(&["b.foo", "test1", "test2"], "test1/b.foo"); +} + /// Explicit root path #[test] fn test_explicit_root_path() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["foo", "one"], @@ -109,7 +147,7 @@ fn test_explicit_root_path() { /// Regex searches #[test] fn test_regex_searches() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["[a-c].foo"], @@ -130,7 +168,7 @@ fn test_regex_searches() { /// Smart case #[test] fn test_smart_case() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["c.foo"], @@ -154,7 +192,7 @@ fn test_smart_case() { /// Case sensitivity (--case-sensitive) #[test] fn test_case_sensitive() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output(&["--case-sensitive", "c.foo"], "one/two/c.foo"); @@ -169,7 +207,7 @@ fn test_case_sensitive() { /// Case insensitivity (--ignore-case) #[test] fn test_case_insensitive() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--ignore-case", "C.Foo"], @@ -187,7 +225,7 @@ fn test_case_insensitive() { /// Full path search (--full-path) #[test] fn test_full_path() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); let root = te.system_root(); let prefix = escape(&root.to_string_lossy()); @@ -205,7 +243,7 @@ fn test_full_path() { /// Hidden files (--hidden) #[test] fn test_hidden() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--hidden", "foo"], @@ -222,7 +260,7 @@ fn test_hidden() { /// Ignored files (--no-ignore) #[test] fn test_no_ignore() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--no-ignore", "foo"], @@ -253,7 +291,7 @@ fn test_no_ignore() { /// VCS ignored files (--no-ignore-vcs) #[test] fn test_no_ignore_vcs() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--no-ignore-vcs", "foo"], @@ -270,7 +308,7 @@ fn test_no_ignore_vcs() { /// Ignored files with ripgrep aliases (-u / -uu) #[test] fn test_no_ignore_aliases() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["-u", "foo"], @@ -301,7 +339,7 @@ fn test_no_ignore_aliases() { /// Symlinks (--follow) #[test] fn test_follow() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--follow", "c.foo"], @@ -315,7 +353,7 @@ fn test_follow() { /// Null separator (--print0) #[test] fn test_print0() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--print0", "foo"], @@ -331,7 +369,7 @@ fn test_print0() { /// Maximum depth (--max-depth) #[test] fn test_max_depth() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--max-depth", "3"], @@ -368,7 +406,7 @@ fn test_max_depth() { /// Absolute paths (--absolute-path) #[test] fn test_absolute_path() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); let abs_path = get_absolute_root_path(&te); @@ -420,7 +458,7 @@ fn test_absolute_path() { /// File type filter (--type) #[test] fn test_type() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--type", "f"], @@ -446,7 +484,7 @@ fn test_type() { /// File extension (--extension) #[test] fn test_extension() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--extension", "foo"], @@ -470,7 +508,7 @@ fn test_extension() { /// Symlinks misc #[test] fn test_symlink() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); let abs_path = get_absolute_root_path(&te); @@ -570,7 +608,7 @@ fn test_symlink() { /// Exclude patterns (--exclude) #[test] fn test_excludes() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); te.assert_output( &["--exclude", "*.foo"], @@ -620,7 +658,7 @@ fn test_excludes() { /// Shell script execution (--exec) #[test] fn test_exec() { - let te = TestEnv::new(); + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); let abs_path = get_absolute_root_path(&te);