grox 0.10.0

Command-line tool that searches for regex matches in a file tree.
Documentation
use std::error::Error;
use std::fs::File;
use std::io::{self, Write};
use std::path::Path;

use serde::{Deserialize, Serialize};

/// Contents of a history file.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct History {
    /// Path to the starting directory.
    pub start: String,
    /// Maximum search depth.
    pub depth: Option<usize>,
    /// Search pattern.
    pub pattern: String,
    /// File pattern.
    pub file_pattern: Option<String>,
    /// Were only file names displayed?
    pub names_only: bool,
    /// Were hidden files searched?
    pub search_hidden: bool,
    /// Directories excluded from the search.
    pub excluded_dirs: Vec<String>,
    /// Locations found.
    pub locations: Vec<Location>,
}

impl History {
    /// Build a history object.
    ///
    /// # Arguments
    ///
    /// * `start` - Starting directory.
    /// * `depth` - Maximum search depth.
    /// * `pattern` - Search pattern.
    /// * `file_pattern` - File pattern.
    /// * `names_only` - Were only file names displayed?
    /// * `search_hidden` - Were hidden files searched?
    /// * `excluded_dirs` - Excluded directories.
    ///
    /// # Returns
    ///
    /// History object if successful and an `io:Error` if the starting directory couldn't be resolved.
    pub fn build(
        start: &Path,
        depth: Option<usize>,
        pattern: &str,
        file_pattern: Option<&str>,
        names_only: bool,
        search_hidden: bool,
        excluded_dirs: &[String],
    ) -> io::Result<Self> {
        let start = start.canonicalize()?;
        let mut excluded_dirs = super::Rebaser::new(&start, excluded_dirs)
            .map(|p| p.map(|p| p.to_string_lossy().to_string()))
            .collect::<io::Result<Vec<String>>>()?;
        excluded_dirs.sort();
        Ok(Self {
            start: start.to_string_lossy().to_string(),
            depth,
            pattern: pattern.to_string(),
            file_pattern: file_pattern.map(String::from),
            names_only,
            search_hidden,
            excluded_dirs,
            locations: Vec::new(),
        })
    }

    /// Load a history object from a JSON file.
    ///
    /// # Arguments
    ///
    /// * `file` - Path to the file.
    ///
    /// # Returns
    ///
    /// History object if successful and an error otherwise.
    pub fn from_file(file: &Path) -> Result<Self, Box<dyn Error>> {
        let file = File::open(file)?;
        Ok(serde_json::from_reader(file)?)
    }

    /// Add a location to the history object.
    ///
    /// # Arguments
    ///
    /// * `file` - Path to the file.
    /// * `line` - Line number.
    ///
    /// # Returns
    ///
    /// `()` if successful and an `io::Error` if the path couldn't be resolved.
    pub fn add_location(&mut self, file: &Path, line: usize) -> io::Result<()> {
        let absolute = file.canonicalize()?.to_string_lossy().to_string();
        self.locations.push(Location { path: absolute, line });
        Ok(())
    }

    /// Save the history object to a file.
    ///
    /// # Arguments
    ///
    /// * `file` - Path to the file.
    ///
    /// # Returns
    ///
    /// `()` if successful and an error otherwise.
    pub fn save(&self, file: &Path) -> io::Result<()> {
        let dump = serde_json::to_string_pretty(self).unwrap();
        let mut file = File::create(file)?;
        file.write_all(dump.as_bytes())?;
        Ok(())
    }
}

/// Location of match.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Location {
    /// Path to the file.
    pub path: String,
    /// Line number of match.
    pub line: usize,
}