ripgrep: add --ignore-file-case-insensitive

The --ignore-file-case-insensitive flag causes all
.gitignore/.rgignore/.ignore files to have their globs matched without
regard for case. Because this introduces a potentially significant
performance regression, this is always disabled by default. Users that
need case insensitive matching can enable it on a case by case basis.

Closes #1164, Closes #1170
This commit is contained in:
David Torosyan
2019-01-20 17:32:34 -08:00
committed by Andrew Gallant
parent 7cbc535d70
commit 718a00f6f2
10 changed files with 156 additions and 21 deletions

View File

@@ -1,3 +1,13 @@
0.11.0 (TBD)
============
TODO.
Feature enhancements:
* [FEATURE #1170](https://github.com/BurntSushi/ripgrep/pull/1170):
Add `--ignore-file-case-insensitive` for case insensitive .ignore globs.
0.10.0 (2018-09-07) 0.10.0 (2018-09-07)
=================== ===================
This is a new minor version release of ripgrep that contains some major new This is a new minor version release of ripgrep that contains some major new

View File

@@ -235,6 +235,11 @@ Like `.gitignore`, a `.ignore` file can be placed in any directory. Its rules
will be processed with respect to the directory it resides in, just like will be processed with respect to the directory it resides in, just like
`.gitignore`. `.gitignore`.
To process `.gitignore` and `.ignore` files case insensitively, use the flag
`--ignore-file-case-insensitive`. This is especially useful on case insensitive
file systems like those on Windows and macOS. Note though that this can come
with a significant performance penalty, and is therefore disabled by default.
For a more in depth description of how glob patterns in a `.gitignore` file For a more in depth description of how glob patterns in a `.gitignore` file
are interpreted, please see `man gitignore`. are interpreted, please see `man gitignore`.

View File

@@ -115,6 +115,10 @@ _rg() {
"(--no-ignore-global --no-ignore-parent --no-ignore-vcs)--no-ignore[don't respect ignore files]" "(--no-ignore-global --no-ignore-parent --no-ignore-vcs)--no-ignore[don't respect ignore files]"
$no'(--ignore-global --ignore-parent --ignore-vcs)--ignore[respect ignore files]' $no'(--ignore-global --ignore-parent --ignore-vcs)--ignore[respect ignore files]'
+ '(ignore-file-case-insensitive)' # Ignore-file case sensitivity options
'--ignore-file-case-insensitive[process ignore files case insensitively]'
$no'--no-ignore-file-case-insensitive[process ignore files case sensitively]'
+ '(ignore-global)' # Global ignore-file options + '(ignore-global)' # Global ignore-file options
"--no-ignore-global[don't respect global ignore files]" "--no-ignore-global[don't respect global ignore files]"
$no'--ignore-global[respect global ignore files]' $no'--ignore-global[respect global ignore files]'

View File

@@ -73,6 +73,8 @@ struct IgnoreOptions {
git_ignore: bool, git_ignore: bool,
/// Whether to read .git/info/exclude files. /// Whether to read .git/info/exclude files.
git_exclude: bool, git_exclude: bool,
/// Whether to ignore files case insensitively
ignore_case_insensitive: bool,
} }
/// Ignore is a matcher useful for recursively walking one or more directories. /// Ignore is a matcher useful for recursively walking one or more directories.
@@ -225,7 +227,11 @@ impl Ignore {
Gitignore::empty() Gitignore::empty()
} else { } else {
let (m, err) = let (m, err) =
create_gitignore(&dir, &self.0.custom_ignore_filenames); create_gitignore(
&dir,
&self.0.custom_ignore_filenames,
self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err); errs.maybe_push(err);
m m
}; };
@@ -233,7 +239,12 @@ impl Ignore {
if !self.0.opts.ignore { if !self.0.opts.ignore {
Gitignore::empty() Gitignore::empty()
} else { } else {
let (m, err) = create_gitignore(&dir, &[".ignore"]); let (m, err) =
create_gitignore(
&dir,
&[".ignore"],
self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err); errs.maybe_push(err);
m m
}; };
@@ -241,7 +252,12 @@ impl Ignore {
if !self.0.opts.git_ignore { if !self.0.opts.git_ignore {
Gitignore::empty() Gitignore::empty()
} else { } else {
let (m, err) = create_gitignore(&dir, &[".gitignore"]); let (m, err) =
create_gitignore(
&dir,
&[".gitignore"],
self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err); errs.maybe_push(err);
m m
}; };
@@ -249,7 +265,12 @@ impl Ignore {
if !self.0.opts.git_exclude { if !self.0.opts.git_exclude {
Gitignore::empty() Gitignore::empty()
} else { } else {
let (m, err) = create_gitignore(&dir, &[".git/info/exclude"]); let (m, err) =
create_gitignore(
&dir,
&[".git/info/exclude"],
self.0.opts.ignore_case_insensitive,
);
errs.maybe_push(err); errs.maybe_push(err);
m m
}; };
@@ -483,6 +504,7 @@ impl IgnoreBuilder {
git_global: true, git_global: true,
git_ignore: true, git_ignore: true,
git_exclude: true, git_exclude: true,
ignore_case_insensitive: false,
}, },
} }
} }
@@ -496,7 +518,11 @@ impl IgnoreBuilder {
if !self.opts.git_global { if !self.opts.git_global {
Gitignore::empty() Gitignore::empty()
} else { } else {
let (gi, err) = Gitignore::global(); let mut builder = GitignoreBuilder::new("");
builder
.case_insensitive(self.opts.ignore_case_insensitive)
.unwrap();
let (gi, err) = builder.build_global();
if let Some(err) = err { if let Some(err) = err {
debug!("{}", err); debug!("{}", err);
} }
@@ -627,6 +653,17 @@ impl IgnoreBuilder {
self.opts.git_exclude = yes; self.opts.git_exclude = yes;
self self
} }
/// Process ignore files case insensitively
///
/// This is disabled by default.
pub fn ignore_case_insensitive(
&mut self,
yes: bool,
) -> &mut IgnoreBuilder {
self.opts.ignore_case_insensitive = yes;
self
}
} }
/// Creates a new gitignore matcher for the directory given. /// Creates a new gitignore matcher for the directory given.
@@ -638,9 +675,11 @@ impl IgnoreBuilder {
pub fn create_gitignore<T: AsRef<OsStr>>( pub fn create_gitignore<T: AsRef<OsStr>>(
dir: &Path, dir: &Path,
names: &[T], names: &[T],
case_insensitive: bool,
) -> (Gitignore, Option<Error>) { ) -> (Gitignore, Option<Error>) {
let mut builder = GitignoreBuilder::new(dir); let mut builder = GitignoreBuilder::new(dir);
let mut errs = PartialErrorBuilder::default(); let mut errs = PartialErrorBuilder::default();
builder.case_insensitive(case_insensitive).unwrap();
for name in names { for name in names {
let gipath = dir.join(name.as_ref()); let gipath = dir.join(name.as_ref());
errs.maybe_push_ignore_io(builder.add(gipath)); errs.maybe_push_ignore_io(builder.add(gipath));

View File

@@ -127,16 +127,7 @@ impl Gitignore {
/// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not /// `$XDG_CONFIG_HOME/git/ignore` is read. If `$XDG_CONFIG_HOME` is not
/// set or is empty, then `$HOME/.config/git/ignore` is used instead. /// set or is empty, then `$HOME/.config/git/ignore` is used instead.
pub fn global() -> (Gitignore, Option<Error>) { pub fn global() -> (Gitignore, Option<Error>) {
match gitconfig_excludes_path() { GitignoreBuilder::new("").build_global()
None => (Gitignore::empty(), None),
Some(path) => {
if !path.is_file() {
(Gitignore::empty(), None)
} else {
Gitignore::new(path)
}
}
}
} }
/// Creates a new empty gitignore matcher that never matches anything. /// Creates a new empty gitignore matcher that never matches anything.
@@ -359,6 +350,36 @@ impl GitignoreBuilder {
}) })
} }
/// Build a global gitignore matcher using the configuration in this
/// builder.
///
/// This consumes ownership of the builder unlike `build` because it
/// must mutate the builder to add the global gitignore globs.
///
/// Note that this ignores the path given to this builder's constructor
/// and instead derives the path automatically from git's global
/// configuration.
pub fn build_global(mut self) -> (Gitignore, Option<Error>) {
match gitconfig_excludes_path() {
None => (Gitignore::empty(), None),
Some(path) => {
if !path.is_file() {
(Gitignore::empty(), None)
} else {
let mut errs = PartialErrorBuilder::default();
errs.maybe_push_ignore_io(self.add(path));
match self.build() {
Ok(gi) => (gi, errs.into_error_option()),
Err(err) => {
errs.push(err);
(Gitignore::empty(), errs.into_error_option())
}
}
}
}
}
}
/// Add each glob from the file path given. /// Add each glob from the file path given.
/// ///
/// The file given should be formatted as a `gitignore` file. /// The file given should be formatted as a `gitignore` file.
@@ -505,12 +526,16 @@ impl GitignoreBuilder {
/// Toggle whether the globs should be matched case insensitively or not. /// Toggle whether the globs should be matched case insensitively or not.
/// ///
/// When this option is changed, only globs added after the change will be affected. /// When this option is changed, only globs added after the change will be
/// affected.
/// ///
/// This is disabled by default. /// This is disabled by default.
pub fn case_insensitive( pub fn case_insensitive(
&mut self, yes: bool &mut self,
yes: bool,
) -> Result<&mut GitignoreBuilder, Error> { ) -> Result<&mut GitignoreBuilder, Error> {
// TODO: This should not return a `Result`. Fix this in the next semver
// release.
self.case_insensitive = yes; self.case_insensitive = yes;
Ok(self) Ok(self)
} }

View File

@@ -144,8 +144,11 @@ impl OverrideBuilder {
/// ///
/// This is disabled by default. /// This is disabled by default.
pub fn case_insensitive( pub fn case_insensitive(
&mut self, yes: bool &mut self,
yes: bool,
) -> Result<&mut OverrideBuilder, Error> { ) -> Result<&mut OverrideBuilder, Error> {
// TODO: This should not return a `Result`. Fix this in the next semver
// release.
self.builder.case_insensitive(yes)?; self.builder.case_insensitive(yes)?;
Ok(self) Ok(self)
} }

View File

@@ -764,6 +764,14 @@ impl WalkBuilder {
self self
} }
/// Process ignore files case insensitively
///
/// This is disabled by default.
pub fn ignore_case_insensitive(&mut self, yes: bool) -> &mut WalkBuilder {
self.ig_builder.ignore_case_insensitive(yes);
self
}
/// Set a function for sorting directory entries by their path. /// Set a function for sorting directory entries by their path.
/// ///
/// If a compare function is set, the resulting iterator will return all /// If a compare function is set, the resulting iterator will return all

View File

@@ -571,6 +571,7 @@ pub fn all_args_and_flags() -> Vec<RGArg> {
flag_iglob(&mut args); flag_iglob(&mut args);
flag_ignore_case(&mut args); flag_ignore_case(&mut args);
flag_ignore_file(&mut args); flag_ignore_file(&mut args);
flag_ignore_file_case_insensitive(&mut args);
flag_invert_match(&mut args); flag_invert_match(&mut args);
flag_json(&mut args); flag_json(&mut args);
flag_line_buffered(&mut args); flag_line_buffered(&mut args);
@@ -1209,6 +1210,27 @@ directly on the command line, then used -g instead.
args.push(arg); args.push(arg);
} }
fn flag_ignore_file_case_insensitive(args: &mut Vec<RGArg>) {
const SHORT: &str =
"Process ignore files (.gitignore, .ignore, etc.) case insensitively.";
const LONG: &str = long!("\
Process ignore files (.gitignore, .ignore, etc.) case insensitively. Note that
this comes with a performance penalty and is most useful on case insensitive
file systems (such as Windows).
This flag can be disabled with the --no-ignore-file-case-insensitive flag.
");
let arg = RGArg::switch("ignore-file-case-insensitive")
.help(SHORT).long_help(LONG)
.overrides("no-ignore-file-case-insensitive");
args.push(arg);
let arg = RGArg::switch("no-ignore-file-case-insensitive")
.hidden()
.overrides("ignore-file-case-insensitive");
args.push(arg);
}
fn flag_invert_match(args: &mut Vec<RGArg>) { fn flag_invert_match(args: &mut Vec<RGArg>) {
const SHORT: &str = "Invert matching."; const SHORT: &str = "Invert matching.";
const LONG: &str = long!("\ const LONG: &str = long!("\

View File

@@ -797,7 +797,8 @@ impl ArgMatches {
&& !self.no_ignore_vcs() && !self.no_ignore_vcs()
&& !self.no_ignore_global()) && !self.no_ignore_global())
.git_ignore(!self.no_ignore() && !self.no_ignore_vcs()) .git_ignore(!self.no_ignore() && !self.no_ignore_vcs())
.git_exclude(!self.no_ignore() && !self.no_ignore_vcs()); .git_exclude(!self.no_ignore() && !self.no_ignore_vcs())
.ignore_case_insensitive(self.ignore_file_case_insensitive());
if !self.no_ignore() { if !self.no_ignore() {
builder.add_custom_ignore_filename(".rgignore"); builder.add_custom_ignore_filename(".rgignore");
} }
@@ -1003,6 +1004,11 @@ impl ArgMatches {
self.is_present("hidden") || self.unrestricted_count() >= 2 self.is_present("hidden") || self.unrestricted_count() >= 2
} }
/// Returns true if ignore files should be processed case insensitively.
fn ignore_file_case_insensitive(&self) -> bool {
self.is_present("ignore-file-case-insensitive")
}
/// Return all of the ignore file paths given on the command line. /// Return all of the ignore file paths given on the command line.
fn ignore_paths(&self) -> Vec<PathBuf> { fn ignore_paths(&self) -> Vec<PathBuf> {
let paths = match self.values_of_os("ignore-file") { let paths = match self.values_of_os("ignore-file") {
@@ -1143,7 +1149,7 @@ impl ArgMatches {
builder.add(&glob)?; builder.add(&glob)?;
} }
// This only enables case insensitivity for subsequent globs. // This only enables case insensitivity for subsequent globs.
builder.case_insensitive(true)?; builder.case_insensitive(true).unwrap();
for glob in self.values_of_lossy_vec("iglob") { for glob in self.values_of_lossy_vec("iglob") {
builder.add(&glob)?; builder.add(&glob)?;
} }

View File

@@ -568,3 +568,16 @@ rgtest!(r1064, |dir: Dir, mut cmd: TestCommand| {
dir.create("input", "abc"); dir.create("input", "abc");
eqnice!("input:abc\n", cmd.arg("a(.*c)").stdout()); eqnice!("input:abc\n", cmd.arg("a(.*c)").stdout());
}); });
// See: https://github.com/BurntSushi/ripgrep/issues/1164
rgtest!(r1164, |dir: Dir, mut cmd: TestCommand| {
dir.create_dir(".git");
dir.create(".gitignore", "myfile");
dir.create("MYFILE", "test");
cmd.arg("--ignore-file-case-insensitive").arg("test").assert_err();
eqnice!(
"MYFILE:test\n",
cmd.arg("--no-ignore-file-case-insensitive").stdout()
);
});