Skip to content

Commit

Permalink
ripgrep: add --ignore-file-case-insensitive
Browse files Browse the repository at this point in the history
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
  • Loading branch information
davidtorosyan authored and BurntSushi committed Jan 23, 2019
1 parent 7cbc535 commit 718a00f
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 21 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
===================
This is a new minor version release of ripgrep that contains some major new
Expand Down
5 changes: 5 additions & 0 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`.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
are interpreted, please see `man gitignore`.

Expand Down
4 changes: 4 additions & 0 deletions complete/_rg
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ _rg() {
"(--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]'

+ '(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
"--no-ignore-global[don't respect global ignore files]"
$no'--ignore-global[respect global ignore files]'
Expand Down
49 changes: 44 additions & 5 deletions ignore/src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ struct IgnoreOptions {
git_ignore: bool,
/// Whether to read .git/info/exclude files.
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.
Expand Down Expand Up @@ -225,31 +227,50 @@ impl Ignore {
Gitignore::empty()
} else {
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);
m
};
let ig_matcher =
if !self.0.opts.ignore {
Gitignore::empty()
} 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);
m
};
let gi_matcher =
if !self.0.opts.git_ignore {
Gitignore::empty()
} 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);
m
};
let gi_exclude_matcher =
if !self.0.opts.git_exclude {
Gitignore::empty()
} 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);
m
};
Expand Down Expand Up @@ -483,6 +504,7 @@ impl IgnoreBuilder {
git_global: true,
git_ignore: true,
git_exclude: true,
ignore_case_insensitive: false,
},
}
}
Expand All @@ -496,7 +518,11 @@ impl IgnoreBuilder {
if !self.opts.git_global {
Gitignore::empty()
} 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 {
debug!("{}", err);
}
Expand Down Expand Up @@ -627,6 +653,17 @@ impl IgnoreBuilder {
self.opts.git_exclude = yes;
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.
Expand All @@ -638,9 +675,11 @@ impl IgnoreBuilder {
pub fn create_gitignore<T: AsRef<OsStr>>(
dir: &Path,
names: &[T],
case_insensitive: bool,
) -> (Gitignore, Option<Error>) {
let mut builder = GitignoreBuilder::new(dir);
let mut errs = PartialErrorBuilder::default();
builder.case_insensitive(case_insensitive).unwrap();
for name in names {
let gipath = dir.join(name.as_ref());
errs.maybe_push_ignore_io(builder.add(gipath));
Expand Down
49 changes: 37 additions & 12 deletions ignore/src/gitignore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,7 @@ impl Gitignore {
/// `$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.
pub fn global() -> (Gitignore, Option<Error>) {
match gitconfig_excludes_path() {
None => (Gitignore::empty(), None),
Some(path) => {
if !path.is_file() {
(Gitignore::empty(), None)
} else {
Gitignore::new(path)
}
}
}
GitignoreBuilder::new("").build_global()
}

/// Creates a new empty gitignore matcher that never matches anything.
Expand Down Expand Up @@ -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.
///
/// The file given should be formatted as a `gitignore` file.
Expand Down Expand Up @@ -505,12 +526,16 @@ impl GitignoreBuilder {

/// 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.
pub fn case_insensitive(
&mut self, yes: bool
&mut self,
yes: bool,
) -> Result<&mut GitignoreBuilder, Error> {
// TODO: This should not return a `Result`. Fix this in the next semver
// release.
self.case_insensitive = yes;
Ok(self)
}
Expand Down
7 changes: 5 additions & 2 deletions ignore/src/overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,16 @@ impl OverrideBuilder {
}

/// 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.
///
/// This is disabled by default.
pub fn case_insensitive(
&mut self, yes: bool
&mut self,
yes: bool,
) -> Result<&mut OverrideBuilder, Error> {
// TODO: This should not return a `Result`. Fix this in the next semver
// release.
self.builder.case_insensitive(yes)?;
Ok(self)
}
Expand Down
8 changes: 8 additions & 0 deletions ignore/src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,14 @@ impl WalkBuilder {
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.
///
/// If a compare function is set, the resulting iterator will return all
Expand Down
22 changes: 22 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ pub fn all_args_and_flags() -> Vec<RGArg> {
flag_iglob(&mut args);
flag_ignore_case(&mut args);
flag_ignore_file(&mut args);
flag_ignore_file_case_insensitive(&mut args);
flag_invert_match(&mut args);
flag_json(&mut args);
flag_line_buffered(&mut args);
Expand Down Expand Up @@ -1209,6 +1210,27 @@ directly on the command line, then used -g instead.
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>) {
const SHORT: &str = "Invert matching.";
const LONG: &str = long!("\
Expand Down
10 changes: 8 additions & 2 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,8 @@ impl ArgMatches {
&& !self.no_ignore_vcs()
&& !self.no_ignore_global())
.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() {
builder.add_custom_ignore_filename(".rgignore");
}
Expand Down Expand Up @@ -1003,6 +1004,11 @@ impl ArgMatches {
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.
fn ignore_paths(&self) -> Vec<PathBuf> {
let paths = match self.values_of_os("ignore-file") {
Expand Down Expand Up @@ -1143,7 +1149,7 @@ impl ArgMatches {
builder.add(&glob)?;
}
// 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") {
builder.add(&glob)?;
}
Expand Down
13 changes: 13 additions & 0 deletions tests/regression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,3 +568,16 @@ rgtest!(r1064, |dir: Dir, mut cmd: TestCommand| {
dir.create("input", "abc");
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()
);
});

0 comments on commit 718a00f

Please sign in to comment.