diff --git a/doc/rg.1 b/doc/rg.1 index 87c563b1..06325661 100644 --- a/doc/rg.1 +++ b/doc/rg.1 @@ -238,6 +238,12 @@ Alias for \-\-color=always \-\-heading \-n. .RS .RE .TP +.B \-S, \-\-smart\-case +Search case insensitively if the pattern is all lowercase. +Search case sensitively otherwise. +.RS +.RE +.TP .B \-j, \-\-threads \f[I]ARG\f[] The number of threads to use. Defaults to the number of logical CPUs (capped at 6). diff --git a/doc/rg.1.md b/doc/rg.1.md index f87c25c0..b9f68968 100644 --- a/doc/rg.1.md +++ b/doc/rg.1.md @@ -154,6 +154,10 @@ the raw speed of grep. -p, --pretty : Alias for --color=always --heading -n. +-S, --smart-case +: Search case insensitively if the pattern is all lowercase. + Search case sensitively otherwise. + -j, --threads *ARG* : The number of threads to use. Defaults to the number of logical CPUs (capped at 6). [default: 0] diff --git a/grep/src/search.rs b/grep/src/search.rs index 6bff2ba9..a91e5e4c 100644 --- a/grep/src/search.rs +++ b/grep/src/search.rs @@ -52,6 +52,7 @@ pub struct GrepBuilder { #[derive(Clone, Debug)] struct Options { case_insensitive: bool, + case_smart: bool, line_terminator: u8, size_limit: usize, dfa_size_limit: usize, @@ -61,6 +62,7 @@ impl Default for Options { fn default() -> Options { Options { case_insensitive: false, + case_smart: false, line_terminator: b'\n', size_limit: 10 * (1 << 20), dfa_size_limit: 10 * (1 << 20), @@ -98,6 +100,18 @@ impl GrepBuilder { self } + /// Whether to enable smart case search or not (disabled by default). + /// + /// Smart case uses case insensitive search if the regex is contains all + /// lowercase literal characters. Otherwise, a case sensitive search is + /// used instead. + /// + /// Enabling the case_insensitive flag overrides this. + pub fn case_smart(mut self, yes: bool) -> GrepBuilder { + self.opts.case_smart = yes; + self + } + /// Set the approximate size limit of the compiled regular expression. /// /// This roughly corresponds to the number of bytes occupied by a @@ -148,8 +162,11 @@ impl GrepBuilder { /// Creates a new regex from the given expression with the current /// configuration. fn regex(&self, expr: &Expr) -> Result { + let casei = + self.opts.case_insensitive + || (self.opts.case_smart && !has_uppercase_literal(expr)); RegexBuilder::new(&expr.to_string()) - .case_insensitive(self.opts.case_insensitive) + .case_insensitive(casei) .multi_line(true) .unicode(true) .size_limit(self.opts.size_limit) @@ -274,6 +291,23 @@ impl<'b, 's> Iterator for Iter<'b, 's> { } } +fn has_uppercase_literal(expr: &Expr) -> bool { + use syntax::Expr::*; + match *expr { + Literal { ref chars, casei } => { + casei || chars.iter().any(|c| c.is_uppercase()) + } + LiteralBytes { ref bytes, casei } => { + casei || bytes.iter().any(|&b| b'A' <= b && b <= b'Z') + } + Group { ref e, .. } => has_uppercase_literal(e), + Repeat { ref e, .. } => has_uppercase_literal(e), + Concat(ref es) => es.iter().any(has_uppercase_literal), + Alternate(ref es) => es.iter().any(has_uppercase_literal), + _ => false, + } +} + #[cfg(test)] mod tests { #![allow(unused_imports)] diff --git a/src/args.rs b/src/args.rs index ddd5e768..99e01fdd 100644 --- a/src/args.rs +++ b/src/args.rs @@ -154,6 +154,10 @@ Less common options: -p, --pretty Alias for --color=always --heading -n. + -S, --smart-case + Search case insensitively if the pattern is all lowercase. + Search case sensitively otherwise. + -j, --threads ARG The number of threads to use. Defaults to the number of logical CPUs (capped at 6). [default: 0] @@ -217,6 +221,7 @@ pub struct RawArgs { flag_quiet: bool, flag_regexp: Vec, flag_replace: Option, + flag_smart_case: bool, flag_text: bool, flag_threads: usize, flag_type: Vec, @@ -348,6 +353,7 @@ impl RawArgs { let types = try!(btypes.build()); let grep = try!( GrepBuilder::new(&pattern) + .case_smart(self.flag_smart_case) .case_insensitive(self.flag_ignore_case) .line_terminator(eol) .build() diff --git a/tests/tests.rs b/tests/tests.rs index e94ec35f..72f5ab69 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -699,6 +699,18 @@ clean!(feature_68, "test", ".", |wd: WorkDir, mut cmd: Command| { assert_eq!(lines, "foo:test\n"); }); +// See: https://github.com/BurntSushi/ripgrep/issues/70 +sherlock!(feature_70, "sherlock", ".", |wd: WorkDir, mut cmd: Command| { + cmd.arg("--smart-case"); + + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + #[test] fn binary_nosearch() { let wd = WorkDir::new("binary_nosearch");