diff --git a/Cargo.lock b/Cargo.lock index ddb1447..fc5935b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index 12e2e45..156448f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["David Peter "] build = "build.rs" categories = ["command-line-utilities"] description = "fd is a simple, fast and user-friendly alternative to find." -exclude = ["benchmarks"] +exclude = ["/benchmarks/*"] homepage = "https://github.com/sharkdp/fd" keywords = [ "search", @@ -38,6 +38,7 @@ ignore = "0.2" lazy_static = "0.2.9" num_cpus = "1.6.2" regex = "0.2" +regex-syntax = "0.4" [dev-dependencies] diff = "0.1" diff --git a/src/app.rs b/src/app.rs index fbbea62..b5e91c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -118,11 +118,11 @@ fn usage() -> HashMap<&'static str, Help> { doc!(h, "case-sensitive" , "Case-sensitive search (default: smart case)" , "Perform a case-sensitive search. By default, fd uses case-insensitive searches, \ - unless the pattern contains both upper- and lowercase characters (smart case)."); + unless the pattern contains an uppercase character (smart case)."); doc!(h, "ignore-case" , "Case-insensitive search (default: smart case)" , "Perform a case-insensitive search. By default, fd uses case-insensitive searches, \ - unless the pattern contains both upper- and lowercase characters (smart case)."); + unless the pattern contains an uppercase character (smart case)."); doc!(h, "absolute-path" , "Show absolute instead of relative paths" , "Shows the full path starting from the root as opposed to relative paths."); diff --git a/src/internal.rs b/src/internal.rs index b2e8938..871d1ce 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -5,6 +5,7 @@ use std::io::Write; use exec::TokenizedCommand; use lscolors::LsColors; use walk::FileType; +use regex_syntax::{Expr, ExprBuilder}; /// Root directory #[cfg(unix)] @@ -82,3 +83,28 @@ pub fn error(message: &str) -> ! { writeln!(&mut ::std::io::stderr(), "{}", message).expect("Failed writing to stderr"); process::exit(1); } + +/// Determine if a regex pattern contains a literal uppercase character. +pub fn pattern_has_uppercase_char(pattern: &str) -> bool { + ExprBuilder::new() + .parse(pattern) + .map(|expr| expr_has_uppercase_char(&expr)) + .unwrap_or(false) +} + +/// Determine if a regex expression contains a literal uppercase character. +fn expr_has_uppercase_char(expr: &Expr) -> bool { + match *expr { + Expr::Literal { ref chars, .. } => chars.iter().any(|c| c.is_uppercase()), + Expr::Class(ref ranges) => { + ranges.iter().any(|r| { + r.start.is_uppercase() || r.end.is_uppercase() + }) + } + Expr::Group { ref e, .. } => expr_has_uppercase_char(e), + Expr::Repeat { ref e, .. } => expr_has_uppercase_char(e), + Expr::Concat(ref es) => es.iter().any(expr_has_uppercase_char), + Expr::Alternate(ref es) => es.iter().any(expr_has_uppercase_char), + _ => false, + } +} diff --git a/src/main.rs b/src/main.rs index 856826b..e8f455b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ extern crate ignore; extern crate lazy_static; extern crate num_cpus; extern crate regex; +extern crate regex_syntax; pub mod fshelper; pub mod lscolors; @@ -25,8 +26,8 @@ use std::time; use atty::Stream; use regex::RegexBuilder; -use internal::{error, FdOptions, PathDisplay, ROOT_DIR}; use exec::TokenizedCommand; +use internal::{error, pattern_has_uppercase_char, FdOptions, PathDisplay, ROOT_DIR}; use lscolors::LsColors; use walk::FileType; @@ -69,11 +70,8 @@ fn main() { // The search will be case-sensitive if the command line flag is set or // if the pattern has an uppercase character (smart case). - let case_sensitive = if !matches.is_present("ignore-case") { - matches.is_present("case-sensitive") || pattern.chars().any(char::is_uppercase) - } else { - false - }; + let case_sensitive = !matches.is_present("ignore-case") && + (matches.is_present("case-sensitive") || pattern_has_uppercase_char(pattern)); let colored_output = match matches.value_of("color") { Some("always") => true, @@ -145,6 +143,7 @@ fn main() { match RegexBuilder::new(pattern) .case_insensitive(!config.case_sensitive) + .dot_matches_new_line(true) .build() { Ok(re) => walk::scan(root_dir, Arc::new(re), base, Arc::new(config)), Err(err) => error(err.description()), diff --git a/src/output.rs b/src/output.rs index dbf562d..a32c4d3 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,9 +1,10 @@ -use internal::{FdOptions, PathDisplay, ROOT_DIR}; +use internal::{FdOptions, PathDisplay}; +use lscolors::LsColors; use std::{fs, process}; use std::io::{self, Write}; use std::ops::Deref; -use std::path::{self, Path, PathBuf}; +use std::path::{self, Path, PathBuf, Component}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; @@ -12,99 +13,112 @@ 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, - ) + let path_to_print = if config.path_display == PathDisplay::Absolute { + &path_full + } else { + entry }; - #[cfg(windows)] - let is_executable = |_: Option<&fs::Metadata>| false; + let r = if let Some(ref ls_colors) = config.ls_colors { + print_entry_colorized(base, path_to_print, config, ls_colors) + } else { + print_entry_uncolorized(path_to_print, config) + }; + + if r.is_err() { + // Probably a broken pipe. Exit gracefully. + process::exit(0); + } +} + +fn print_entry_colorized( + base: &Path, + path: &Path, + config: &FdOptions, + ls_colors: &LsColors, +) -> io::Result<()> { + let default_style = ansi_term::Style::default(); 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(); + // Separator to use before the current component. + let mut separator = String::new(); - let mut component_path = base.to_path_buf(); + // Full path to the current component. + 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 path.components() { + let comp_str = component.as_os_str().to_string_lossy(); + component_path.push(Path::new(comp_str.deref())); - // Traverse the path and colorize each component - for component in entry.components() { - let comp_str = component.as_os_str().to_string_lossy(); + let style = get_path_style(&component_path, ls_colors).unwrap_or(&default_style); - component_path.push(Path::new(comp_str.deref())); + write!(handle, "{}{}", separator, style.paint(comp_str))?; - 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, "") + // Determine separator to print before next component. + separator = match component { + // Prefix needs no separator, as it is always followed by RootDir. + Component::Prefix(_) => String::new(), + // RootDir is already a separator. + Component::RootDir => String::new(), + // Everything else uses a separator that is painted the same way as the component. + _ => style.paint(path::MAIN_SEPARATOR.to_string()).to_string(), }; - if r.is_err() { - // Probably a broken pipe. Exit gracefully. - process::exit(0); - } + } + + if config.null_separator { + write!(handle, "\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); - } + writeln!(handle, "") } } + +fn print_entry_uncolorized(path: &Path, config: &FdOptions) -> io::Result<()> { + let separator = if config.null_separator { "\0" } else { "\n" }; + + let path_str = path.to_string_lossy(); + write!(&mut io::stdout(), "{}{}", path_str, separator) +} + +fn get_path_style<'a>(path: &Path, ls_colors: &'a LsColors) -> Option<&'a ansi_term::Style> { + if path.symlink_metadata() + .map(|md| md.file_type().is_symlink()) + .unwrap_or(false) + { + return Some(&ls_colors.symlink); + } + + let metadata = path.metadata(); + + if metadata.as_ref().map(|md| md.is_dir()).unwrap_or(false) { + Some(&ls_colors.directory) + } else if metadata.map(|md| is_executable(&md)).unwrap_or(false) { + Some(&ls_colors.executable) + } else if let Some(filename_style) = + path.file_name().and_then(|n| n.to_str()).and_then(|n| { + ls_colors.filenames.get(n) + }) + { + Some(filename_style) + } else if let Some(extension_style) = + path.extension().and_then(|e| e.to_str()).and_then(|e| { + ls_colors.extensions.get(e) + }) + { + Some(extension_style) + } else { + None + } +} + +#[cfg(unix)] +fn is_executable(md: &fs::Metadata) -> bool { + md.permissions().mode() & 0o111 != 0 +} + +#[cfg(windows)] +fn is_executable(_: &fs::Metadata) -> bool { + false +} diff --git a/tests/tests.rs b/tests/tests.rs index 49faaef..dfd02fb 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -115,6 +115,14 @@ fn test_smart_case() { te.assert_output(&["C.Foo"], "one/two/C.Foo2"); te.assert_output(&["Foo"], "one/two/C.Foo2"); + + // Only literal uppercase chars should trigger case sensitivity. + te.assert_output( + &["\\Ac"], + "one/two/c.foo + one/two/C.Foo2", + ); + te.assert_output(&["\\AC"], "one/two/C.Foo2"); } /// Case sensitivity (--case-sensitive)