From b4415280673e5d2d9b47265b1cb95c6e94734b49 Mon Sep 17 00:00:00 2001 From: Matthias Reitinger Date: Fri, 13 Oct 2017 21:46:00 +0200 Subject: [PATCH 1/7] Improve smart case to only consider literal uppercase chars (#103) --- Cargo.lock | 1 + Cargo.toml | 1 + src/internal.rs | 26 ++++++++++++++++++++++++++ src/main.rs | 10 ++++------ tests/tests.rs | 8 ++++++++ 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4069054..cd95f0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "ignore 0.2.2 (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 d79807d..a170707 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ ansi_term = "0.9" clap = "2.26.0" atty = "0.2" regex = "0.2" +regex-syntax = "0.4" ignore = "0.2" num_cpus = "1.6.2" diff --git a/src/internal.rs b/src/internal.rs index 1c1d66a..185068a 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -4,6 +4,7 @@ use std::io::Write; use lscolors::LsColors; use walk::FileType; +use regex_syntax::{Expr, ExprBuilder}; /// Root directory #[cfg(unix)] @@ -78,3 +79,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 9912afd..2e8b283 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ extern crate clap; extern crate ansi_term; extern crate atty; extern crate regex; +extern crate regex_syntax; extern crate ignore; extern crate num_cpus; @@ -22,7 +23,7 @@ use std::time; use atty::Stream; use regex::RegexBuilder; -use internal::{error, FdOptions, PathDisplay, ROOT_DIR}; +use internal::{error, pattern_has_uppercase_char, FdOptions, PathDisplay, ROOT_DIR}; use lscolors::LsColors; use walk::FileType; @@ -65,11 +66,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, 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) From 0677e6331ddc50271ddf44b1e3330d15c333d1ea Mon Sep 17 00:00:00 2001 From: Matthias Reitinger Date: Sat, 14 Oct 2017 02:06:25 +0200 Subject: [PATCH 2/7] Fix too many path separators on Windows (#93). --- src/output.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/output.rs b/src/output.rs index dbf562d..4a1ea99 100644 --- a/src/output.rs +++ b/src/output.rs @@ -3,7 +3,7 @@ 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}; +use std::path::{self, Path, PathBuf, Component}; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; @@ -42,6 +42,13 @@ pub fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) { component_path.push(Path::new(comp_str.deref())); + if let Component::RootDir = component { + // Printing the root dir would result in too many path separators on Windows. + // Note that a root dir component won't occur on Unix, because `entry` is never an + // absolute path in that case. + continue; + } + let metadata = component_path.metadata().ok(); let is_directory = metadata.as_ref().map(|md| md.is_dir()).unwrap_or(false); From 0b04f39398e95cbefc26627f5401c0df31b80bb9 Mon Sep 17 00:00:00 2001 From: Matthias Reitinger Date: Sat, 14 Oct 2017 02:30:19 +0200 Subject: [PATCH 3/7] Split up print_entry in colorized and uncolorized helper functions. --- src/output.rs | 166 ++++++++++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 79 deletions(-) diff --git a/src/output.rs b/src/output.rs index 4a1ea99..88664f2 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,4 +1,5 @@ use internal::{FdOptions, PathDisplay, ROOT_DIR}; +use lscolors::LsColors; use std::{fs, process}; use std::io::{self, Write}; @@ -10,66 +11,71 @@ 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, - ) + let r = if let Some(ref ls_colors) = config.ls_colors { + print_entry_colorized(base, entry, config, ls_colors) + } else { + print_entry_uncolorized(entry, config) }; - #[cfg(windows)] - let is_executable = |_: Option<&fs::Metadata>| false; + if r.is_err() { + // Probably a broken pipe. Exit gracefully. + process::exit(0); + } +} + +fn print_entry_colorized( + base: &Path, + entry: &PathBuf, + config: &FdOptions, + ls_colors: &LsColors, +) -> io::Result<()> { + let default_style = ansi_term::Style::default(); + + let mut component_path = base.to_path_buf(); 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 path_full = base.join(entry); - let mut component_path = base.to_path_buf(); + if config.path_display == PathDisplay::Absolute { + write!(handle, "{}", ls_colors.directory.paint(ROOT_DIR))?; + } - 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())); + + if let Component::RootDir = component { + // Printing the root dir would result in too many path separators on Windows. + // Note that a root dir component won't occur on Unix, because `entry` is never an + // absolute path in that case. + continue; } - // Traverse the path and colorize each component - for component in entry.components() { - let comp_str = component.as_os_str().to_string_lossy(); + let metadata = component_path.metadata(); + let is_directory = metadata.as_ref().map(|md| md.is_dir()).unwrap_or(false); - component_path.push(Path::new(comp_str.deref())); + 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 metadata.map(|md| is_executable(&md)).unwrap_or(false) { + &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)); - if let Component::RootDir = component { - // Printing the root dir would result in too many path separators on Windows. - // Note that a root dir component won't occur on Unix, because `entry` is never an - // absolute path in that case. - continue; - } - - 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 { + match o_style { Some(s) => s, None => // Look up file extension @@ -78,40 +84,42 @@ pub fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) { .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" }; + write!(handle, "{}", style.paint(comp_str))?; - let r = write!(&mut io::stdout(), "{}{}{}", prefix, path_str, separator); - - if r.is_err() { - // Probably a broken pipe. Exit gracefully. - process::exit(0); + if is_directory && component_path != path_full { + let sep = path::MAIN_SEPARATOR.to_string(); + write!(handle, "{}", style.paint(sep))?; } } + + if config.null_separator { + write!(handle, "\0") + } else { + writeln!(handle, "") + } +} + +fn print_entry_uncolorized(entry: &PathBuf, config: &FdOptions) -> io::Result<()> { + // Uncolorized output + let prefix = if config.path_display == PathDisplay::Absolute { + ROOT_DIR + } else { + "" + }; + let separator = if config.null_separator { "\0" } else { "\n" }; + + let path_str = entry.to_string_lossy(); + write!(&mut io::stdout(), "{}{}{}", prefix, path_str, separator) +} + +#[cfg(unix)] +fn is_executable(md: &fs::Metadata) -> bool { + md.permissions().mode() & 0o111 != 0 +} + +#[cfg(windows)] +fn is_executable(_: &fs::Metadata) -> bool { + false } From 7150c9a3a98746334d92d7c9fd7d3371d7ec5952 Mon Sep 17 00:00:00 2001 From: Matthias Reitinger Date: Sat, 14 Oct 2017 03:44:24 +0200 Subject: [PATCH 4/7] Refactor output.rs --- src/output.rs | 125 +++++++++++++++++++++++++------------------------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/src/output.rs b/src/output.rs index 88664f2..a32c4d3 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,4 +1,4 @@ -use internal::{FdOptions, PathDisplay, ROOT_DIR}; +use internal::{FdOptions, PathDisplay}; use lscolors::LsColors; use std::{fs, process}; @@ -11,10 +11,18 @@ use std::os::unix::fs::PermissionsExt; use ansi_term; pub fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) { - let r = if let Some(ref ls_colors) = config.ls_colors { - print_entry_colorized(base, entry, config, ls_colors) + let path_full = base.join(entry); + + let path_to_print = if config.path_display == PathDisplay::Absolute { + &path_full } else { - print_entry_uncolorized(entry, config) + entry + }; + + 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() { @@ -25,73 +33,39 @@ pub fn print_entry(base: &Path, entry: &PathBuf, config: &FdOptions) { fn print_entry_colorized( base: &Path, - entry: &PathBuf, + path: &Path, config: &FdOptions, ls_colors: &LsColors, ) -> io::Result<()> { let default_style = ansi_term::Style::default(); - let mut component_path = base.to_path_buf(); - let stdout = io::stdout(); let mut handle = stdout.lock(); - let path_full = base.join(entry); + // Separator to use before the current component. + let mut separator = String::new(); - if config.path_display == PathDisplay::Absolute { - write!(handle, "{}", ls_colors.directory.paint(ROOT_DIR))?; - } + // Full path to the current component. + let mut component_path = base.to_path_buf(); // Traverse the path and colorize each component - for component in entry.components() { + for component in path.components() { let comp_str = component.as_os_str().to_string_lossy(); - component_path.push(Path::new(comp_str.deref())); - if let Component::RootDir = component { - // Printing the root dir would result in too many path separators on Windows. - // Note that a root dir component won't occur on Unix, because `entry` is never an - // absolute path in that case. - continue; - } + let style = get_path_style(&component_path, ls_colors).unwrap_or(&default_style); - let metadata = component_path.metadata(); - let is_directory = metadata.as_ref().map(|md| md.is_dir()).unwrap_or(false); + write!(handle, "{}{}", separator, style.paint(comp_str))?; - 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 metadata.map(|md| is_executable(&md)).unwrap_or(false) { - &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) - } + // 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(), }; - - write!(handle, "{}", style.paint(comp_str))?; - - if is_directory && component_path != path_full { - let sep = path::MAIN_SEPARATOR.to_string(); - write!(handle, "{}", style.paint(sep))?; - } } if config.null_separator { @@ -101,17 +75,42 @@ fn print_entry_colorized( } } -fn print_entry_uncolorized(entry: &PathBuf, config: &FdOptions) -> io::Result<()> { - // Uncolorized output - let prefix = if config.path_display == PathDisplay::Absolute { - ROOT_DIR - } else { - "" - }; +fn print_entry_uncolorized(path: &Path, config: &FdOptions) -> io::Result<()> { let separator = if config.null_separator { "\0" } else { "\n" }; - let path_str = entry.to_string_lossy(); - write!(&mut io::stdout(), "{}{}{}", prefix, path_str, separator) + 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)] From be2238ddf4f26aa139b3047916d8efa966e11686 Mon Sep 17 00:00:00 2001 From: sharkdp Date: Sat, 14 Oct 2017 12:04:57 +0200 Subject: [PATCH 5/7] Fix Cargo warnings --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a170707..4c3ec1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" keywords = ["search", "find", "file", "filesystem", "tool"] categories = ["command-line-utilities"] license = "MIT" -exclude = ["benchmarks"] +exclude = ["/benchmarks/*"] build = "build.rs" [badges] From c38ef0e9b2bb6b2f4247c6e522ac648453d53d1e Mon Sep 17 00:00:00 2001 From: sharkdp Date: Sat, 14 Oct 2017 12:09:34 +0200 Subject: [PATCH 6/7] Fix description of smart-case in help text --- src/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index cec9868..75b3d42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -113,11 +113,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."); From f2632d5fce910849ab60ecd0e47730274d40fda4 Mon Sep 17 00:00:00 2001 From: "J.W" Date: Sat, 14 Oct 2017 21:41:38 +0800 Subject: [PATCH 7/7] Dot "." also matches newline by default (closes #111) --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 2e8b283..3d1435c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -135,6 +135,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()),