grox 0.10.0

Command-line tool that searches for regex matches in a file tree.
Documentation
use std::env;
use std::ffi::OsStr;
use std::fs::{create_dir, File};
use std::io::Write;
use std::path::{Path, PathBuf};

use regex::Regex;

#[test]
fn use_searcher() {
    let workspace = tempfile::tempdir().unwrap();
    setup_files(workspace.path());

    let pattern = Regex::new("fo.").unwrap();
    let mut builder = grox::Builder::new(&pattern, workspace.path());
    builder.set_depth(1);
    let searcher = builder.build();
    assert!(searcher.is_ok());

    let mut locations: Vec<grox::Location> = searcher.unwrap().collect();
    assert_eq!(locations.len(), 2);
    locations.sort_by_key(|loc| loc.file.file_name().unwrap().to_string_lossy().to_string());
    println!("{:?}", locations);

    assert_eq!(
        &locations[0].file.file_name().unwrap().to_string_lossy(),
        "baz.txt"
    );
    assert_eq!(locations[0].line, 1);

    assert_eq!(
        &locations[1].file.file_name().unwrap().to_string_lossy(),
        "foo.txt"
    );
    assert_eq!(locations[1].line, 2);
}

#[test]
fn ignore_hidden_files_by_default() {
    let workspace = tempfile::tempdir().unwrap();
    setup_hidden_file(workspace.path());

    let pattern = Regex::new("fo.").unwrap();
    let searcher = grox::Builder::new(&pattern, workspace.path()).build();
    assert!(searcher.is_ok());

    let locations: Vec<grox::Location> = searcher.unwrap().collect();
    assert_eq!(locations.len(), 0);
}

#[test]
fn search_hidden_files() {
    let workspace = tempfile::tempdir().unwrap();
    setup_hidden_file(workspace.path());

    let pattern = Regex::new("fo.").unwrap();
    let mut builder = grox::Builder::new(&pattern, workspace.path());
    builder.set_flags(grox::SEARCH_HIDDEN);
    let searcher = builder.build();
    assert!(searcher.is_ok());

    let locations: Vec<grox::Location> = searcher.unwrap().collect();
    assert_eq!(locations.len(), 1);
}

#[test]
fn only_one_match_per_file() {
    let workspace = tempfile::tempdir().unwrap();
    let mut file = File::create(workspace.path().join("foo.txt")).unwrap();
    file.write_all(b"foo\nfoz").unwrap();

    let pattern = Regex::new("fo.").unwrap();
    let mut builder = grox::Builder::new(&pattern, workspace.path());
    builder.set_flags(grox::SINGLE_MATCH);
    let searcher = builder.build();
    assert!(searcher.is_ok());

    let locations: Vec<grox::Location> = searcher.unwrap().collect();
    assert_eq!(locations.len(), 1);
}

#[test]
fn only_scan_files_matching_a_pattern() {
    let workspace = tempfile::tempdir().unwrap();
    setup_files(workspace.path());

    let pattern = Regex::new("fo.").unwrap();
    let file_pattern = Regex::new("f.z").unwrap();
    let mut builder = grox::Builder::new(&pattern, workspace.path());
    builder.set_file_pattern(&file_pattern);
    builder.set_flags(grox::SEARCH_HIDDEN);
    let searcher = builder.build();
    assert!(searcher.is_ok());

    let locations: Vec<grox::Location> = searcher.unwrap().collect();
    assert_eq!(locations.len(), 1);
}

#[test]
fn multiple_matches_per_line() {
    let workspace = tempfile::tempdir().unwrap();
    let mut file = File::create(workspace.path().join("foo.txt")).unwrap();
    file.write_all(b"foo bar foz").unwrap();

    let pattern = Regex::new("fo.").unwrap();
    let searcher = grox::Builder::new(&pattern, workspace.path()).build().unwrap();

    let locations: Vec<grox::Location> = searcher.collect();
    assert_eq!(locations.len(), 2);
}

#[test]
fn exclude_dir() {
    let workspace = tempfile::tempdir().unwrap();
    let subsubdir = setup_files_for_exclusion(workspace.path());

    let pattern = Regex::new("text").unwrap();
    let mut builder = grox::Builder::new(&pattern, workspace.path());
    builder.exclude_dir(subsubdir.to_str().unwrap());
    let searcher = builder.build().unwrap();

    let locations: Vec<grox::Location> = searcher.collect();
    assert_eq!(locations.len(), 1);
    let name = locations[0].file.file_name().and_then(OsStr::to_str).unwrap();
    assert_eq!(name, "foo.txt");
}

#[test]
fn excluded_dir_can_be_relative() {
    let workspace = tempfile::tempdir().unwrap();
    let subsubdir = setup_files_for_exclusion(workspace.path());

    let pattern = Regex::new("text").unwrap();
    let mut builder = grox::Builder::new(&pattern, workspace.path());
    let subsubdir = pathdiff::diff_paths(subsubdir, workspace.path()).unwrap();
    builder.exclude_dir(subsubdir.to_str().unwrap());
    let searcher = builder.build().unwrap();
    let locations: Vec<grox::Location> = searcher.collect();
    assert_eq!(locations.len(), 1);
}

#[test]
fn trailing_slash_allowed_for_excluded_dir() {
    let workspace = tempfile::tempdir().unwrap();
    let subsubdir = setup_files_for_exclusion(workspace.path());

    let pattern = Regex::new("text").unwrap();
    let mut builder = grox::Builder::new(&pattern, workspace.path());
    let excluded_dir = format!("{}/", subsubdir.to_str().unwrap());
    builder.exclude_dir(&excluded_dir);
    let searcher = builder.build().unwrap();

    let locations: Vec<grox::Location> = searcher.collect();
    assert_eq!(locations.len(), 1);
    let name = locations[0]
        .file
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap();
    assert_eq!(name, "foo.txt");
}

#[test]
fn relative_subdir_locations() {
    let workspace = tempfile::tempdir().unwrap();
    setup_files(workspace.path());
    env::set_current_dir(workspace.path()).unwrap();

    let pattern = Regex::new("forget").unwrap();
    let directory = PathBuf::from(".");
    let mut searcher = grox::Builder::new(&pattern, &directory).build().unwrap();

    let location = searcher.next().unwrap();
    assert_eq!(location.file.to_string_lossy(), "./subdir/baz.txt");
}

#[test]
fn can_exclude_nonexistent_dir() {
    let pattern = Regex::new("foo").unwrap();
    let directory = PathBuf::from(".");
    let mut builder = grox::Builder::new(&pattern, &directory);
    builder.exclude_dir("/doesnt/exist");
    assert!(builder.build().is_ok());
}

fn setup_files(workspace: &Path) {
    let mut file = File::create(workspace.join("foo.txt")).unwrap();
    file.write_all(b"line 1\nsome foo text").unwrap();

    let mut file = File::create(workspace.join("bar.txt")).unwrap();
    file.write_all(b"bar text").unwrap();

    let subdir = workspace.join("subdir");
    create_dir(&subdir).unwrap();

    let mut file = File::create(subdir.join("baz.txt")).unwrap();
    file.write_all(b"forget what you know").unwrap();

    let subsubdir = subdir.join("subdir");
    create_dir(&subsubdir).unwrap();

    let mut file = File::create(subsubdir.join("foz.txt")).unwrap();
    file.write_all(b"I dream of you fondly").unwrap();
}

fn setup_hidden_file(workspace: &Path) {
    let mut file = File::create(workspace.join(".foo")).unwrap();
    file.write_all(b"foo").unwrap();
}

fn setup_files_for_exclusion(workspace: &Path) -> PathBuf {
    let subdir = workspace.join("subdir");
    create_dir(&subdir).unwrap();
    let mut file = File::create(subdir.join("foo.txt")).unwrap();
    file.write_all(b"some foo text").unwrap();

    let subsubdir = subdir.join("subsubdir");
    create_dir(&subsubdir).unwrap();
    let mut file = File::create(subsubdir.join("bar.txt")).unwrap();
    file.write_all(b"bar text").unwrap();

    subsubdir
}