From b65a8c353b1d214f2b26be61b715059727ce94bb Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 6 Jan 2017 22:43:59 -0500 Subject: [PATCH] Add --sort-files flag. When used, parallelism is disabled but the results are sorted by file path. Closes #263 --- doc/rg.1 | 7 +++++++ doc/rg.1.md | 4 ++++ ignore/src/walk.rs | 39 +++++++++++++++++++++++++++++++++++++-- src/app.rs | 5 +++++ src/args.rs | 8 ++++++++ tests/tests.rs | 12 ++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) diff --git a/doc/rg.1 b/doc/rg.1 index 0dfd3f07b..6ce5bd65a 100644 --- a/doc/rg.1 +++ b/doc/rg.1 @@ -355,6 +355,13 @@ This is overridden by either \-\-case\-sensitive or \-\-ignore\-case. .RS .RE .TP +.B \-\-sort\-files +Sort results by file path. +Note that this currently disables all parallelism and runs search in a +single thread. +.RS +.RE +.TP .B \-j, \-\-threads \f[I]ARG\f[] The number of threads to use. 0 means use the number of logical CPUs (capped at 6). diff --git a/doc/rg.1.md b/doc/rg.1.md index b6ed88082..748e5aa56 100644 --- a/doc/rg.1.md +++ b/doc/rg.1.md @@ -239,6 +239,10 @@ Project home page: https://github.com/BurntSushi/ripgrep Search case sensitively otherwise. This is overridden by either --case-sensitive or --ignore-case. +--sort-files +: Sort results by file path. Note that this currently + disables all parallelism and runs search in a single thread. + -j, --threads *ARG* : The number of threads to use. 0 means use the number of logical CPUs (capped at 6). [default: 0] diff --git a/ignore/src/walk.rs b/ignore/src/walk.rs index f0c042c78..e1dd20526 100644 --- a/ignore/src/walk.rs +++ b/ignore/src/walk.rs @@ -1,4 +1,5 @@ -use std::ffi::OsStr; +use std::cmp; +use std::ffi::{OsStr, OsString}; use std::fmt; use std::fs::{self, FileType, Metadata}; use std::io; @@ -324,16 +325,30 @@ impl DirEntryRaw { /// path is skipped. /// * Sixth, if the path has made it this far then it is yielded in the /// iterator. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct WalkBuilder { paths: Vec, ig_builder: IgnoreBuilder, parents: bool, max_depth: Option, follow_links: bool, + sorter: Option cmp::Ordering + 'static>>, threads: usize, } +impl fmt::Debug for WalkBuilder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("WalkBuilder") + .field("paths", &self.paths) + .field("ig_builder", &self.ig_builder) + .field("parents", &self.parents) + .field("max_depth", &self.max_depth) + .field("follow_links", &self.follow_links) + .field("threads", &self.threads) + .finish() + } +} + impl WalkBuilder { /// Create a new builder for a recursive directory iterator for the /// directory given. @@ -348,6 +363,7 @@ impl WalkBuilder { parents: true, max_depth: None, follow_links: false, + sorter: None, threads: 0, } } @@ -356,6 +372,7 @@ impl WalkBuilder { pub fn build(&self) -> Walk { let follow_links = self.follow_links; let max_depth = self.max_depth; + let cmp = self.sorter.clone(); let its = self.paths.iter().map(move |p| { if p == Path::new("-") { (p.to_path_buf(), None) @@ -365,6 +382,10 @@ impl WalkBuilder { if let Some(max_depth) = max_depth { wd = wd.max_depth(max_depth); } + if let Some(ref cmp) = cmp { + let cmp = cmp.clone(); + wd = wd.sort_by(move |a, b| cmp(a, b)); + } (p.to_path_buf(), Some(WalkEventIter::from(wd))) } }).collect::>().into_iter(); @@ -533,6 +554,20 @@ impl WalkBuilder { self.ig_builder.git_exclude(yes); self } + + /// Set a function for sorting directory entries. + /// + /// If a compare function is set, the resulting iterator will return all + /// paths in sorted order. The compare function will be called to compare + /// names from entries from the same directory using only the name of the + /// entry. + /// + /// Note that this is not used in the parallel iterator. + pub fn sort_by(&mut self, cmp: F) -> &mut WalkBuilder + where F: Fn(&OsString, &OsString) -> cmp::Ordering + 'static { + self.sorter = Some(Arc::new(cmp)); + self + } } /// Walk is a recursive directory iterator over file paths in one or more diff --git a/src/app.rs b/src/app.rs index 8466b2ba8..8cbb257d7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -158,6 +158,7 @@ fn app(next_line_help: bool, doc: F) -> App<'static, 'static> .arg(flag("replace").short("r").value_name("ARG").takes_value(true)) .arg(flag("case-sensitive").short("s")) .arg(flag("smart-case").short("S")) + .arg(flag("sort-files")) .arg(flag("threads") .short("j").value_name("ARG").takes_value(true) .validator(validate_number)) @@ -426,6 +427,10 @@ lazy_static! { "Searches case insensitively if the pattern is all lowercase. \ Search case sensitively otherwise. This is overridden by \ either -s/--case-sensitive or -i/--ignore-case."); + doc!(h, "sort-files", + "Sort results by file path. Implies --threads=1.", + "Sort results by file path. Note that this currently \ + disables all parallelism and runs search in a single thread."); doc!(h, "threads", "The approximate number of threads to use.", "The approximate number of threads to use. A value of 0 (which \ diff --git a/src/args.rs b/src/args.rs index 18da6f66c..6aec396be 100644 --- a/src/args.rs +++ b/src/args.rs @@ -64,6 +64,7 @@ pub struct Args { quiet: bool, quiet_matched: QuietMatched, replace: Option>, + sort_files: bool, text: bool, threads: usize, type_list: bool, @@ -277,6 +278,9 @@ impl Args { wd.ignore(!self.no_ignore); wd.parents(!self.no_ignore_parent); wd.threads(self.threads()); + if self.sort_files { + wd.sort_by(|a, b| a.cmp(b)); + } wd } } @@ -333,6 +337,7 @@ impl<'a> ArgMatches<'a> { quiet: quiet, quiet_matched: QuietMatched::new(quiet), replace: self.replace(), + sort_files: self.is_present("sort-files"), text: self.text(), threads: try!(self.threads()), type_list: self.is_present("type-list"), @@ -654,6 +659,9 @@ impl<'a> ArgMatches<'a> { /// Returns the approximate number of threads that ripgrep should use. fn threads(&self) -> Result { + if self.is_present("sort-files") { + return Ok(1); + } let threads = try!(self.usize_of("threads")).unwrap_or(0); Ok(if threads == 0 { cmp::min(12, num_cpus::get()) diff --git a/tests/tests.rs b/tests/tests.rs index e00da84e4..1802e1862 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1203,6 +1203,18 @@ clean!(feature_159_zero_max, "test", ".", |wd: WorkDir, mut cmd: Command| { wd.assert_err(&mut cmd); }); +// See: https://github.com/BurntSushi/ripgrep/issues/263 +clean!(feature_263_sort_files, "test", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("foo", "test"); + wd.create("abc", "test"); + wd.create("zoo", "test"); + wd.create("bar", "test"); + cmd.arg("--sort-files"); + + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "abc:test\nbar:test\nfoo:test\nzoo:test\n"); +}); + #[test] fn binary_nosearch() { let wd = WorkDir::new("binary_nosearch");