mirror of
https://github.com/sharkdp/fd.git
synced 2024-11-18 18:00:35 +01:00
break main into separate modules
This commit is contained in:
parent
affe41949d
commit
ccb899a511
4 changed files with 354 additions and 330 deletions
80
src/internal.rs
Normal file
80
src/internal.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use std::process;
|
||||||
|
use std::time;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use lscolors::LsColors;
|
||||||
|
use walk::FileType;
|
||||||
|
|
||||||
|
/// Root directory
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub static ROOT_DIR: &'static str = "/";
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub static ROOT_DIR: &'static str = "";
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
/// Determines whether the regex search is case-sensitive or case-insensitive.
|
||||||
|
pub case_sensitive: bool,
|
||||||
|
|
||||||
|
/// Whether to search within the full file path or just the base name (filename or directory
|
||||||
|
/// name).
|
||||||
|
pub search_full_path: bool,
|
||||||
|
|
||||||
|
/// Whether to ignore hidden files and directories (or not).
|
||||||
|
pub ignore_hidden: bool,
|
||||||
|
|
||||||
|
/// Whether to respect VCS ignore files (`.gitignore`, `.ignore`, ..) or not.
|
||||||
|
pub read_ignore: bool,
|
||||||
|
|
||||||
|
/// Whether to follow symlinks or not.
|
||||||
|
pub follow_links: bool,
|
||||||
|
|
||||||
|
/// Whether elements of output should be separated by a null character
|
||||||
|
pub null_separator: bool,
|
||||||
|
|
||||||
|
/// The maximum search depth, or `None` if no maximum search depth should be set.
|
||||||
|
///
|
||||||
|
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
|
||||||
|
/// all files under subdirectories of the current directory, etc.
|
||||||
|
pub max_depth: Option<usize>,
|
||||||
|
|
||||||
|
/// The number of threads to use.
|
||||||
|
pub threads: usize,
|
||||||
|
|
||||||
|
/// Time to buffer results internally before streaming to the console. This is useful to
|
||||||
|
/// provide a sorted output, in case the total execution time is shorter than
|
||||||
|
/// `max_buffer_time`.
|
||||||
|
pub max_buffer_time: Option<time::Duration>,
|
||||||
|
|
||||||
|
/// 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<LsColors>,
|
||||||
|
|
||||||
|
/// The type of file to search for. All files other than the specified type will be ignored.
|
||||||
|
pub file_type: FileType,
|
||||||
|
|
||||||
|
/// The extension to search for. Only entries matching the extension will be included.
|
||||||
|
///
|
||||||
|
/// The value (if present) will be a lowercase string without leading dots.
|
||||||
|
pub extension: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print error message to stderr and exit with status `1`.
|
||||||
|
pub fn error(message: &str) -> ! {
|
||||||
|
writeln!(&mut ::std::io::stderr(), "{}", message).expect("Failed writing to stderr");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
338
src/main.rs
338
src/main.rs
|
@ -9,344 +9,22 @@ extern crate num_cpus;
|
||||||
pub mod lscolors;
|
pub mod lscolors;
|
||||||
pub mod fshelper;
|
pub mod fshelper;
|
||||||
mod app;
|
mod app;
|
||||||
|
mod internal;
|
||||||
|
mod output;
|
||||||
|
mod walk;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::Write;
|
use std::path::Path;
|
||||||
use std::ops::Deref;
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::mpsc::channel;
|
|
||||||
use std::thread;
|
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use atty::Stream;
|
use atty::Stream;
|
||||||
use regex::{Regex, RegexBuilder};
|
use regex::RegexBuilder;
|
||||||
use ignore::WalkBuilder;
|
|
||||||
|
|
||||||
|
use internal::{error, FdOptions, PathDisplay, ROOT_DIR};
|
||||||
use lscolors::LsColors;
|
use lscolors::LsColors;
|
||||||
|
use walk::FileType;
|
||||||
/// Defines how to display search result paths.
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum PathDisplay {
|
|
||||||
/// As an absolute path
|
|
||||||
Absolute,
|
|
||||||
|
|
||||||
/// As a relative path
|
|
||||||
Relative,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The type of file to search for.
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
enum FileType {
|
|
||||||
Any,
|
|
||||||
RegularFile,
|
|
||||||
Directory,
|
|
||||||
SymLink,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration options for *fd*.
|
|
||||||
struct FdOptions {
|
|
||||||
/// Determines whether the regex search is case-sensitive or case-insensitive.
|
|
||||||
case_sensitive: bool,
|
|
||||||
|
|
||||||
/// Whether to search within the full file path or just the base name (filename or directory
|
|
||||||
/// name).
|
|
||||||
search_full_path: bool,
|
|
||||||
|
|
||||||
/// Whether to ignore hidden files and directories (or not).
|
|
||||||
ignore_hidden: bool,
|
|
||||||
|
|
||||||
/// Whether to respect VCS ignore files (`.gitignore`, `.ignore`, ..) or not.
|
|
||||||
read_ignore: bool,
|
|
||||||
|
|
||||||
/// Whether to follow symlinks or not.
|
|
||||||
follow_links: bool,
|
|
||||||
|
|
||||||
/// Whether elements of output should be separated by a null character
|
|
||||||
null_separator: bool,
|
|
||||||
|
|
||||||
/// The maximum search depth, or `None` if no maximum search depth should be set.
|
|
||||||
///
|
|
||||||
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
|
|
||||||
/// all files under subdirectories of the current directory, etc.
|
|
||||||
max_depth: Option<usize>,
|
|
||||||
|
|
||||||
/// The number of threads to use.
|
|
||||||
threads: usize,
|
|
||||||
|
|
||||||
/// Time to buffer results internally before streaming to the console. This is useful to
|
|
||||||
/// provide a sorted output, in case the total execution time is shorter than
|
|
||||||
/// `max_buffer_time`.
|
|
||||||
max_buffer_time: Option<time::Duration>,
|
|
||||||
|
|
||||||
/// Display results as relative or absolute path.
|
|
||||||
path_display: PathDisplay,
|
|
||||||
|
|
||||||
/// `None` if the output should not be colorized. Otherwise, a `LsColors` instance that defines
|
|
||||||
/// how to style different filetypes.
|
|
||||||
ls_colors: Option<LsColors>,
|
|
||||||
|
|
||||||
/// The type of file to search for. All files other than the specified type will be ignored.
|
|
||||||
file_type: FileType,
|
|
||||||
|
|
||||||
/// The extension to search for. Only entries matching the extension will be included.
|
|
||||||
///
|
|
||||||
/// The value (if present) will be a lowercase string without leading dots.
|
|
||||||
extension: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The receiver thread can either be buffering results or directly streaming to the console.
|
|
||||||
enum ReceiverMode {
|
|
||||||
/// Receiver is still buffering in order to sort the results, if the search finishes fast
|
|
||||||
/// enough.
|
|
||||||
Buffering,
|
|
||||||
|
|
||||||
/// Receiver is directly printing results to the output.
|
|
||||||
Streaming,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Root directory
|
|
||||||
#[cfg(unix)]
|
|
||||||
static ROOT_DIR: &'static str = "/";
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
static ROOT_DIR: &'static str = "";
|
|
||||||
|
|
||||||
/// Print a search result to the console.
|
|
||||||
fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) {
|
|
||||||
let path_full = base.join(entry);
|
|
||||||
|
|
||||||
let path_str = entry.to_string_lossy();
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
let is_executable = |p: Option<&std::fs::Metadata>| {
|
|
||||||
p.map(|f| f.permissions().mode() & 0o111 != 0)
|
|
||||||
.unwrap_or(false)
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
let is_executable = |_: Option<&std::fs::Metadata>| false;
|
|
||||||
|
|
||||||
let stdout = std::io::stdout();
|
|
||||||
let mut handle = stdout.lock();
|
|
||||||
|
|
||||||
if let Some(ref ls_colors) = config.ls_colors {
|
|
||||||
let default_style = ansi_term::Style::default();
|
|
||||||
|
|
||||||
let mut component_path = base.to_path_buf();
|
|
||||||
|
|
||||||
if config.path_display == PathDisplay::Absolute {
|
|
||||||
print!("{}", ls_colors.directory.paint(ROOT_DIR));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traverse the path and colorize each component
|
|
||||||
for component in entry.components() {
|
|
||||||
let comp_str = component.as_os_str().to_string_lossy();
|
|
||||||
|
|
||||||
component_path.push(Path::new(comp_str.deref()));
|
|
||||||
|
|
||||||
let metadata = component_path.metadata().ok();
|
|
||||||
let is_directory = metadata.as_ref().map(|md| md.is_dir()).unwrap_or(false);
|
|
||||||
|
|
||||||
let style =
|
|
||||||
if component_path.symlink_metadata()
|
|
||||||
.map(|md| md.file_type().is_symlink())
|
|
||||||
.unwrap_or(false) {
|
|
||||||
&ls_colors.symlink
|
|
||||||
} else if is_directory {
|
|
||||||
&ls_colors.directory
|
|
||||||
} else if is_executable(metadata.as_ref()) {
|
|
||||||
&ls_colors.executable
|
|
||||||
} else {
|
|
||||||
// Look up file name
|
|
||||||
let o_style =
|
|
||||||
component_path.file_name()
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
.and_then(|n| ls_colors.filenames.get(n));
|
|
||||||
|
|
||||||
match o_style {
|
|
||||||
Some(s) => s,
|
|
||||||
None =>
|
|
||||||
// Look up file extension
|
|
||||||
component_path.extension()
|
|
||||||
.and_then(|e| e.to_str())
|
|
||||||
.and_then(|e| ls_colors.extensions.get(e))
|
|
||||||
.unwrap_or(&default_style)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
write!(handle, "{}", style.paint(comp_str)).ok();
|
|
||||||
|
|
||||||
if is_directory && component_path != path_full {
|
|
||||||
let sep = std::path::MAIN_SEPARATOR.to_string();
|
|
||||||
write!(handle, "{}", style.paint(sep)).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let r = if config.null_separator {
|
|
||||||
write!(handle, "\0")
|
|
||||||
} else {
|
|
||||||
writeln!(handle, "")
|
|
||||||
};
|
|
||||||
if r.is_err() {
|
|
||||||
// Probably a broken pipe. Exit gracefully.
|
|
||||||
process::exit(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Uncolorized output
|
|
||||||
|
|
||||||
let prefix = if config.path_display == PathDisplay::Absolute { ROOT_DIR } else { "" };
|
|
||||||
let separator = if config.null_separator { "\0" } else { "\n" };
|
|
||||||
|
|
||||||
let r = write!(&mut std::io::stdout(), "{}{}{}", prefix, path_str, separator);
|
|
||||||
|
|
||||||
if r.is_err() {
|
|
||||||
// Probably a broken pipe. Exit gracefully.
|
|
||||||
process::exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively scan the given search path and search for files / pathnames matching the pattern.
|
|
||||||
fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions>) {
|
|
||||||
let (tx, rx) = channel();
|
|
||||||
|
|
||||||
let walker = WalkBuilder::new(root)
|
|
||||||
.hidden(config.ignore_hidden)
|
|
||||||
.ignore(config.read_ignore)
|
|
||||||
.git_ignore(config.read_ignore)
|
|
||||||
.parents(config.read_ignore)
|
|
||||||
.git_global(config.read_ignore)
|
|
||||||
.git_exclude(config.read_ignore)
|
|
||||||
.follow_links(config.follow_links)
|
|
||||||
.max_depth(config.max_depth)
|
|
||||||
.threads(config.threads)
|
|
||||||
.build_parallel();
|
|
||||||
|
|
||||||
// Spawn the thread that receives all results through the channel.
|
|
||||||
let rx_config = Arc::clone(&config);
|
|
||||||
let rx_base = base.to_owned();
|
|
||||||
let receiver_thread = thread::spawn(move || {
|
|
||||||
let start = time::Instant::now();
|
|
||||||
|
|
||||||
let mut buffer = vec![];
|
|
||||||
|
|
||||||
// Start in buffering mode
|
|
||||||
let mut mode = ReceiverMode::Buffering;
|
|
||||||
|
|
||||||
// Maximum time to wait before we start streaming to the console.
|
|
||||||
let max_buffer_time = rx_config.max_buffer_time
|
|
||||||
.unwrap_or_else(|| time::Duration::from_millis(100));
|
|
||||||
|
|
||||||
for value in rx {
|
|
||||||
match mode {
|
|
||||||
ReceiverMode::Buffering => {
|
|
||||||
buffer.push(value);
|
|
||||||
|
|
||||||
// Have we reached the maximum time?
|
|
||||||
if time::Instant::now() - start > max_buffer_time {
|
|
||||||
// Flush the buffer
|
|
||||||
for v in &buffer {
|
|
||||||
print_entry(&rx_base, v, &rx_config);
|
|
||||||
}
|
|
||||||
buffer.clear();
|
|
||||||
|
|
||||||
// Start streaming
|
|
||||||
mode = ReceiverMode::Streaming;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ReceiverMode::Streaming => {
|
|
||||||
print_entry(&rx_base, &value, &rx_config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have finished fast enough (faster than max_buffer_time), we haven't streamed
|
|
||||||
// anything to the console, yet. In this case, sort the results and print them:
|
|
||||||
if !buffer.is_empty() {
|
|
||||||
buffer.sort();
|
|
||||||
for value in buffer {
|
|
||||||
print_entry(&rx_base, &value, &rx_config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spawn the sender threads.
|
|
||||||
walker.run(|| {
|
|
||||||
let base = base.to_owned();
|
|
||||||
let config = Arc::clone(&config);
|
|
||||||
let pattern = Arc::clone(&pattern);
|
|
||||||
let tx_thread = tx.clone();
|
|
||||||
|
|
||||||
Box::new(move |entry_o| {
|
|
||||||
let entry = match entry_o {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(_) => return ignore::WalkState::Continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter out unwanted file types.
|
|
||||||
match config.file_type {
|
|
||||||
FileType::Any => (),
|
|
||||||
FileType::RegularFile => if entry.file_type().map_or(false, |ft| !ft.is_file()) {
|
|
||||||
return ignore::WalkState::Continue;
|
|
||||||
},
|
|
||||||
FileType::Directory => if entry.file_type().map_or(false, |ft| !ft.is_dir()) {
|
|
||||||
return ignore::WalkState::Continue;
|
|
||||||
},
|
|
||||||
FileType::SymLink => if entry.file_type().map_or(false, |ft| !ft.is_symlink()) {
|
|
||||||
return ignore::WalkState::Continue;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out unwanted extensions.
|
|
||||||
if let Some(ref filter_ext) = config.extension {
|
|
||||||
let entry_ext = entry.path().extension().map(|e| e.to_string_lossy().to_lowercase());
|
|
||||||
if entry_ext.map_or(true, |ext| ext != *filter_ext) {
|
|
||||||
return ignore::WalkState::Continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_rel_buf = match fshelper::path_relative_from(entry.path(), &*base) {
|
|
||||||
Some(p) => p,
|
|
||||||
None => error("Error: could not get relative path for directory entry.")
|
|
||||||
};
|
|
||||||
let path_rel = path_rel_buf.as_path();
|
|
||||||
|
|
||||||
let search_str_o =
|
|
||||||
if config.search_full_path {
|
|
||||||
Some(path_rel.to_string_lossy())
|
|
||||||
} else {
|
|
||||||
path_rel.file_name()
|
|
||||||
.map(|f| f.to_string_lossy())
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(search_str) = search_str_o {
|
|
||||||
// TODO: take care of the unwrap call
|
|
||||||
pattern.find(&*search_str)
|
|
||||||
.map(|_| tx_thread.send(path_rel_buf.to_owned()).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
ignore::WalkState::Continue
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drop the initial sender. If we don't do this, the receiver will block even
|
|
||||||
// if all threads have finished, since there is still one sender around.
|
|
||||||
drop(tx);
|
|
||||||
|
|
||||||
// Wait for the receiver thread to print out all results.
|
|
||||||
receiver_thread.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print error message to stderr and exit with status `1`.
|
|
||||||
fn error(message: &str) -> ! {
|
|
||||||
writeln!(&mut std::io::stderr(), "{}", message).expect("Failed writing to stderr");
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = app::build_app().get_matches();
|
let matches = app::build_app().get_matches();
|
||||||
|
@ -448,7 +126,7 @@ fn main() {
|
||||||
match RegexBuilder::new(pattern)
|
match RegexBuilder::new(pattern)
|
||||||
.case_insensitive(!config.case_sensitive)
|
.case_insensitive(!config.case_sensitive)
|
||||||
.build() {
|
.build() {
|
||||||
Ok(re) => scan(root_dir, Arc::new(re), base, Arc::new(config)),
|
Ok(re) => walk::scan(root_dir, Arc::new(re), base, Arc::new(config)),
|
||||||
Err(err) => error(err.description())
|
Err(err) => error(err.description())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
104
src/output.rs
Normal file
104
src/output.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use internal::{FdOptions, PathDisplay, ROOT_DIR};
|
||||||
|
|
||||||
|
use std::{fs, process};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::{self, Path, PathBuf};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
use ansi_term;
|
||||||
|
|
||||||
|
pub fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) {
|
||||||
|
let path_full = base.join(entry);
|
||||||
|
|
||||||
|
let path_str = entry.to_string_lossy();
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
let is_executable = |p: Option<&fs::Metadata>| {
|
||||||
|
p.map(|f| f.permissions().mode() & 0o111 != 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
let is_executable = |_: Option<&fs::Metadata>| false;
|
||||||
|
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut handle = stdout.lock();
|
||||||
|
|
||||||
|
if let Some(ref ls_colors) = config.ls_colors {
|
||||||
|
let default_style = ansi_term::Style::default();
|
||||||
|
|
||||||
|
let mut component_path = base.to_path_buf();
|
||||||
|
|
||||||
|
if config.path_display == PathDisplay::Absolute {
|
||||||
|
print!("{}", ls_colors.directory.paint(ROOT_DIR));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse the path and colorize each component
|
||||||
|
for component in entry.components() {
|
||||||
|
let comp_str = component.as_os_str().to_string_lossy();
|
||||||
|
|
||||||
|
component_path.push(Path::new(comp_str.deref()));
|
||||||
|
|
||||||
|
let metadata = component_path.metadata().ok();
|
||||||
|
let is_directory = metadata.as_ref().map(|md| md.is_dir()).unwrap_or(false);
|
||||||
|
|
||||||
|
let style =
|
||||||
|
if component_path.symlink_metadata()
|
||||||
|
.map(|md| md.file_type().is_symlink())
|
||||||
|
.unwrap_or(false) {
|
||||||
|
&ls_colors.symlink
|
||||||
|
} else if is_directory {
|
||||||
|
&ls_colors.directory
|
||||||
|
} else if is_executable(metadata.as_ref()) {
|
||||||
|
&ls_colors.executable
|
||||||
|
} else {
|
||||||
|
// Look up file name
|
||||||
|
let o_style =
|
||||||
|
component_path.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.and_then(|n| ls_colors.filenames.get(n));
|
||||||
|
|
||||||
|
match o_style {
|
||||||
|
Some(s) => s,
|
||||||
|
None =>
|
||||||
|
// Look up file extension
|
||||||
|
component_path.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.and_then(|e| ls_colors.extensions.get(e))
|
||||||
|
.unwrap_or(&default_style)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(handle, "{}", style.paint(comp_str)).ok();
|
||||||
|
|
||||||
|
if is_directory && component_path != path_full {
|
||||||
|
let sep = path::MAIN_SEPARATOR.to_string();
|
||||||
|
write!(handle, "{}", style.paint(sep)).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = if config.null_separator {
|
||||||
|
write!(handle, "\0")
|
||||||
|
} else {
|
||||||
|
writeln!(handle, "")
|
||||||
|
};
|
||||||
|
if r.is_err() {
|
||||||
|
// Probably a broken pipe. Exit gracefully.
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Uncolorized output
|
||||||
|
|
||||||
|
let prefix = if config.path_display == PathDisplay::Absolute { ROOT_DIR } else { "" };
|
||||||
|
let separator = if config.null_separator { "\0" } else { "\n" };
|
||||||
|
|
||||||
|
let r = write!(&mut io::stdout(), "{}{}{}", prefix, path_str, separator);
|
||||||
|
|
||||||
|
if r.is_err() {
|
||||||
|
// Probably a broken pipe. Exit gracefully.
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
162
src/walk.rs
Normal file
162
src/walk.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use internal::{error, FdOptions};
|
||||||
|
use fshelper;
|
||||||
|
use output;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::thread;
|
||||||
|
use std::time;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use ignore::{self, WalkBuilder};
|
||||||
|
|
||||||
|
/// The receiver thread can either be buffering results or directly streaming to the console.
|
||||||
|
enum ReceiverMode {
|
||||||
|
/// Receiver is still buffering in order to sort the results, if the search finishes fast
|
||||||
|
/// enough.
|
||||||
|
Buffering,
|
||||||
|
|
||||||
|
/// Receiver is directly printing results to the output.
|
||||||
|
Streaming,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of file to search for.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum FileType {
|
||||||
|
Any,
|
||||||
|
RegularFile,
|
||||||
|
Directory,
|
||||||
|
SymLink,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively scan the given search path and search for files / pathnames matching the pattern.
|
||||||
|
pub fn scan(root: &Path, pattern: Arc<Regex>, base: &Path, config: Arc<FdOptions>) {
|
||||||
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
|
let walker = WalkBuilder::new(root)
|
||||||
|
.hidden(config.ignore_hidden)
|
||||||
|
.ignore(config.read_ignore)
|
||||||
|
.git_ignore(config.read_ignore)
|
||||||
|
.parents(config.read_ignore)
|
||||||
|
.git_global(config.read_ignore)
|
||||||
|
.git_exclude(config.read_ignore)
|
||||||
|
.follow_links(config.follow_links)
|
||||||
|
.max_depth(config.max_depth)
|
||||||
|
.threads(config.threads)
|
||||||
|
.build_parallel();
|
||||||
|
|
||||||
|
// Spawn the thread that receives all results through the channel.
|
||||||
|
let rx_config = Arc::clone(&config);
|
||||||
|
let rx_base = base.to_owned();
|
||||||
|
let receiver_thread = thread::spawn(move || {
|
||||||
|
let start = time::Instant::now();
|
||||||
|
|
||||||
|
let mut buffer = vec![];
|
||||||
|
|
||||||
|
// Start in buffering mode
|
||||||
|
let mut mode = ReceiverMode::Buffering;
|
||||||
|
|
||||||
|
// Maximum time to wait before we start streaming to the console.
|
||||||
|
let max_buffer_time = rx_config.max_buffer_time
|
||||||
|
.unwrap_or_else(|| time::Duration::from_millis(100));
|
||||||
|
|
||||||
|
for value in rx {
|
||||||
|
match mode {
|
||||||
|
ReceiverMode::Buffering => {
|
||||||
|
buffer.push(value);
|
||||||
|
|
||||||
|
// Have we reached the maximum time?
|
||||||
|
if time::Instant::now() - start > max_buffer_time {
|
||||||
|
// Flush the buffer
|
||||||
|
for v in &buffer {
|
||||||
|
output::print_entry(&rx_base, v, &rx_config);
|
||||||
|
}
|
||||||
|
buffer.clear();
|
||||||
|
|
||||||
|
// Start streaming
|
||||||
|
mode = ReceiverMode::Streaming;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ReceiverMode::Streaming => {
|
||||||
|
output::print_entry(&rx_base, &value, &rx_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have finished fast enough (faster than max_buffer_time), we haven't streamed
|
||||||
|
// anything to the console, yet. In this case, sort the results and print them:
|
||||||
|
if !buffer.is_empty() {
|
||||||
|
buffer.sort();
|
||||||
|
for value in buffer {
|
||||||
|
output::print_entry(&rx_base, &value, &rx_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn the sender threads.
|
||||||
|
walker.run(|| {
|
||||||
|
let base = base.to_owned();
|
||||||
|
let config = Arc::clone(&config);
|
||||||
|
let pattern = Arc::clone(&pattern);
|
||||||
|
let tx_thread = tx.clone();
|
||||||
|
|
||||||
|
Box::new(move |entry_o| {
|
||||||
|
let entry = match entry_o {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return ignore::WalkState::Continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter out unwanted file types.
|
||||||
|
match config.file_type {
|
||||||
|
FileType::Any => (),
|
||||||
|
FileType::RegularFile => if entry.file_type().map_or(false, |ft| !ft.is_file()) {
|
||||||
|
return ignore::WalkState::Continue;
|
||||||
|
},
|
||||||
|
FileType::Directory => if entry.file_type().map_or(false, |ft| !ft.is_dir()) {
|
||||||
|
return ignore::WalkState::Continue;
|
||||||
|
},
|
||||||
|
FileType::SymLink => if entry.file_type().map_or(false, |ft| !ft.is_symlink()) {
|
||||||
|
return ignore::WalkState::Continue;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out unwanted extensions.
|
||||||
|
if let Some(ref filter_ext) = config.extension {
|
||||||
|
let entry_ext = entry.path().extension().map(|e| e.to_string_lossy().to_lowercase());
|
||||||
|
if entry_ext.map_or(true, |ext| ext != *filter_ext) {
|
||||||
|
return ignore::WalkState::Continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_rel_buf = match fshelper::path_relative_from(entry.path(), &*base) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => error("Error: could not get relative path for directory entry.")
|
||||||
|
};
|
||||||
|
let path_rel = path_rel_buf.as_path();
|
||||||
|
|
||||||
|
let search_str_o =
|
||||||
|
if config.search_full_path {
|
||||||
|
Some(path_rel.to_string_lossy())
|
||||||
|
} else {
|
||||||
|
path_rel.file_name()
|
||||||
|
.map(|f| f.to_string_lossy())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(search_str) = search_str_o {
|
||||||
|
// TODO: take care of the unwrap call
|
||||||
|
pattern.find(&*search_str)
|
||||||
|
.map(|_| tx_thread.send(path_rel_buf.to_owned()).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore::WalkState::Continue
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drop the initial sender. If we don't do this, the receiver will block even
|
||||||
|
// if all threads have finished, since there is still one sender around.
|
||||||
|
drop(tx);
|
||||||
|
|
||||||
|
// Wait for the receiver thread to print out all results.
|
||||||
|
receiver_thread.join().unwrap();
|
||||||
|
}
|
Loading…
Reference in a new issue