diff --git a/ci/test-complete b/ci/test-complete index 995775b70f..fd6d0d4854 100755 --- a/ci/test-complete +++ b/ci/test-complete @@ -43,10 +43,10 @@ main() { # where a long option starting with certain letters (see `_rg`) is found. # Occasionally we may have to handle some manually, however help_args=( ${(f)"$( - $rg --help | - $rg -i -- '^\s+--?[a-z0-9.]|--[a-z]' | - $rg -ior '$1' -- $'[\t /\"\'`.,](-[a-z0-9.]|--[a-z0-9-]+)(,|\\b)' | - $rg -vw -- --print0 # False positives + $rg --no-config --help | + $rg --no-config -i -- '^\s+--?[a-z0-9.]|--[a-z]' | + $rg --no-config -ior '$1' -- $'[\t /\"\'`.,](-[a-z0-9.]|--[a-z0-9-]+)(,|\\b)' | + $rg --no-config -vw -- --print0 # False positives )"} ) help_args=( ${(ou)help_args} ) diff --git a/crates/core/flags/complete/rg.zsh b/crates/core/flags/complete/rg.zsh index aacaddd92b..b4bf5f8779 100644 --- a/crates/core/flags/complete/rg.zsh +++ b/crates/core/flags/complete/rg.zsh @@ -175,6 +175,10 @@ _rg() { "--no-ignore-files[don't respect --ignore-file flags]" $no'--ignore-files[respect --ignore-file files]' + + input + '*--in=[specify text file containing paths to search]: :_files' + '*--in0=[specify binary file containing NUL-terminated paths to search]: :_files' + + '(invert-match)' {-v,--invert-match}'[invert matching]' $no"--no-invert-match[don't invert matching]" diff --git a/crates/core/flags/defs.rs b/crates/core/flags/defs.rs index 671a9290d3..95db2210b2 100644 --- a/crates/core/flags/defs.rs +++ b/crates/core/flags/defs.rs @@ -26,8 +26,8 @@ use crate::flags::{ lowargs::{ BinaryMode, BoundaryMode, BufferMode, CaseMode, ColorChoice, ContextMode, EncodingMode, EngineChoice, GenerateMode, IndexMode, - LoggingMode, LowArgs, MmapMode, Mode, PatternSource, SearchMode, - SortMode, SortModeKind, SpecialMode, TypeChange, + InputSource, LoggingMode, LowArgs, MmapMode, Mode, PatternSource, + SearchMode, SortMode, SortModeKind, SpecialMode, TypeChange, }, }; @@ -83,6 +83,8 @@ pub(super) const FLAGS: &[&dyn Flag] = &[ &IgnoreCase, &IgnoreFile, &IgnoreFileCaseInsensitive, + &In, + &In0, &IncludeZero, &Index, &IndexCrud, @@ -3402,6 +3404,186 @@ fn test_ignore_file_case_insensitive() { assert_eq!(true, args.ignore_file_case_insensitive); } +/// --in +#[derive(Debug)] +struct In; + +impl Flag for In { + fn is_switch(&self) -> bool { + false + } + fn name_long(&self) -> &'static str { + "in" + } + fn doc_variable(&self) -> Option<&'static str> { + Some("INPUTFILE") + } + fn doc_category(&self) -> Category { + Category::Input + } + fn doc_short(&self) -> &'static str { + r"Read paths to search from the given text file." + } + fn doc_long(&self) -> &'static str { + r" +Includes paths to search from the given file, with one path per line. +.sp +When this flag is used, it behaves as if the file paths in the file provided +were passed as positional arguments in the same order as they are +in the file and relative to other positional arguments. +.sp +Newlines (both LF and CRLF) are not counted as part of the path. +The rest of the path name is taken as-is on Unix and decoded as UTF-8 on +Windows. Use the \flag{in0} flag to use \fBNUL\fP as terminator. +.sp +When \fIINPUTFILE\fP is \fB-\fP, then the paths will be read from \fBstdin\fP. +" + } + fn completion_type(&self) -> CompletionType { + CompletionType::Filename + } + + fn update(&self, v: FlagValue, args: &mut LowArgs) -> anyhow::Result<()> { + let path = PathBuf::from(v.unwrap_value()); + args.inputs.push(InputSource::LineTerminated(path)); + Ok(()) + } +} + +/// --in0 +#[derive(Debug)] +struct In0; + +impl Flag for In0 { + fn is_switch(&self) -> bool { + false + } + fn name_long(&self) -> &'static str { + "in0" + } + fn doc_variable(&self) -> Option<&'static str> { + Some("INPUTFILE") + } + fn doc_category(&self) -> Category { + Category::Input + } + fn doc_short(&self) -> &'static str { + r"Read paths to search from the given binary file." + } + fn doc_long(&self) -> &'static str { + r" +Includes paths to search from the given file, terminated by \fBNUL\fP bytes. +.sp +When this flag is used, it behaves as if the file paths in the file provided +were passed as positional arguments in the same order as they are +in the file and relative to other positional arguments. +.sp +The path names are taken as-is on Unix, and decoded as UTF-8 on Windows. +.sp +When \fIINPUTFILE\fP is \fB-\fP, then the paths will be read from \fBstdin\fP. +" + } + fn completion_type(&self) -> CompletionType { + CompletionType::Filename + } + + fn update(&self, v: FlagValue, args: &mut LowArgs) -> anyhow::Result<()> { + let path = PathBuf::from(v.unwrap_value()); + args.inputs.push(InputSource::NulTerminated(path)); + Ok(()) + } +} + +#[cfg(test)] +#[test] +fn test_in_in0() { + let args = parse_low_raw(None::<&str>).unwrap(); + assert_eq!(Vec::::new(), args.inputs); + + let args = parse_low_raw(["--in", "foo"]).unwrap(); + assert_eq!( + vec![InputSource::LineTerminated(PathBuf::from("foo"))], + args.inputs + ); + + let args = parse_low_raw(["--in0", "foo"]).unwrap(); + assert_eq!( + vec![InputSource::NulTerminated(PathBuf::from("foo"))], + args.inputs + ); + + let args = parse_low_raw(["--in=foo"]).unwrap(); + assert_eq!( + vec![InputSource::LineTerminated(PathBuf::from("foo"))], + args.inputs + ); + + let args = parse_low_raw(["--in0=foo"]).unwrap(); + assert_eq!( + vec![InputSource::NulTerminated(PathBuf::from("foo"))], + args.inputs + ); + + let args = parse_low_raw(["--in", "-foo"]).unwrap(); + assert_eq!( + vec![InputSource::LineTerminated(PathBuf::from("-foo"))], + args.inputs + ); + + let args = parse_low_raw(["--in0", "-foo"]).unwrap(); + assert_eq!( + vec![InputSource::NulTerminated(PathBuf::from("-foo"))], + args.inputs + ); + + let args = parse_low_raw(["--in=-foo"]).unwrap(); + assert_eq!( + vec![InputSource::LineTerminated(PathBuf::from("-foo"))], + args.inputs + ); + + let args = parse_low_raw(["--in0=-foo"]).unwrap(); + assert_eq!( + vec![InputSource::NulTerminated(PathBuf::from("-foo"))], + args.inputs + ); + + let args = + parse_low_raw(["--in=foo", "--in0", "bar", "--in", "baz"]).unwrap(); + assert_eq!( + vec![ + InputSource::LineTerminated(PathBuf::from("foo")), + InputSource::NulTerminated(PathBuf::from("bar")), + InputSource::LineTerminated(PathBuf::from("baz")), + ], + args.inputs + ); + + let args = parse_low_raw(["--in=-"]).unwrap(); + assert_eq!( + vec![InputSource::LineTerminated(PathBuf::from("-")),], + args.inputs + ); + + let args = parse_low_raw(["--in", "-"]).unwrap(); + assert_eq!( + vec![InputSource::LineTerminated(PathBuf::from("-")),], + args.inputs + ); + + let args = parse_low_raw(["--in0=-"]).unwrap(); + assert_eq!( + vec![InputSource::NulTerminated(PathBuf::from("-")),], + args.inputs + ); + + let args = parse_low_raw(["--in0", "-"]).unwrap(); + assert_eq!( + vec![InputSource::NulTerminated(PathBuf::from("-")),], + args.inputs + ); +} + /// --include-zero #[derive(Debug)] struct IncludeZero; @@ -3640,7 +3822,10 @@ fn test_index_crud() { let args = parse_low_raw(["--files", "--x-crud", "foo"]).unwrap(); assert_eq!(Mode::Index(IndexMode::Crud), args.mode); - assert_eq!(vec![std::ffi::OsString::from("foo")], args.positional); + assert_eq!( + vec![InputSource::PositionalArgument(std::ffi::OsString::from("foo"))], + args.inputs + ); let args = parse_low_raw(["--x-crud", "--files"]).unwrap(); assert_eq!(Mode::Files, args.mode); diff --git a/crates/core/flags/hiargs.rs b/crates/core/flags/hiargs.rs index fb5669872d..ba470076db 100644 --- a/crates/core/flags/hiargs.rs +++ b/crates/core/flags/hiargs.rs @@ -8,7 +8,7 @@ use std::{ }; use { - bstr::BString, + bstr::{BString, ByteSlice, io::BufReadExt}, grep::printer::{ColorSpecs, SummaryKind}, }; @@ -16,8 +16,9 @@ use crate::{ flags::lowargs::{ BinaryMode, BoundaryMode, BufferMode, CaseMode, ColorChoice, ContextMode, ContextSeparator, EncodingMode, EngineChoice, - FieldContextSeparator, FieldMatchSeparator, LowArgs, MmapMode, Mode, - PatternSource, SearchMode, SortMode, SortModeKind, TypeChange, + FieldContextSeparator, FieldMatchSeparator, InputSource, LowArgs, + MmapMode, Mode, PatternSource, SearchMode, SortMode, SortModeKind, + TypeChange, }, haystack::{Haystack, HaystackBuilder}, search::{PatternMatcher, Printer, SearchWorker, SearchWorkerBuilder}, @@ -1030,11 +1031,19 @@ impl Patterns { // If we got nothing from -e/--regexp and -f/--file, then the first // positional is a pattern. if low.patterns.is_empty() { - anyhow::ensure!( - !low.positional.is_empty(), - "ripgrep requires at least one pattern to execute a search" - ); - let ospat = low.positional.remove(0); + let Some(ospat) = low + .inputs + .iter() + .position(|i| matches!(i, InputSource::PositionalArgument(_))) + .and_then(|i| match low.inputs.remove(i) { + InputSource::PositionalArgument(pattern) => Some(pattern), + _ => None, + }) + else { + anyhow::bail!( + "ripgrep requires at least one pattern to execute a search" + ); + }; let Ok(pat) = ospat.into_string() else { anyhow::bail!("pattern given is not valid UTF-8") }; @@ -1104,26 +1113,27 @@ struct Paths { impl Paths { /// Drain the search paths out of the given low arguments. + /// + /// This includes collecting files from `--in`/`--in0`. fn from_low_args( state: &mut State, _: &Patterns, low: &mut LowArgs, ) -> anyhow::Result { // We require a `&Patterns` even though we don't use it to ensure that - // patterns have already been read from LowArgs. This let's us safely + // patterns have already been read from LowArgs. This lets us safely // assume that all remaining positional arguments are intended to be // file paths. + if state.stdin_consumed && low.inputs.iter().any(|i| i.is_stdin()) { + anyhow::bail!( + "error: attempted to read patterns or input file paths \ + from stdin while also searching stdin", + ); + } - let mut paths = Vec::with_capacity(low.positional.len()); - for osarg in low.positional.drain(..) { - let path = PathBuf::from(osarg); - if state.stdin_consumed && path == Path::new("-") { - anyhow::bail!( - "error: attempted to read patterns from stdin \ - while also searching stdin", - ); - } - paths.push(path); + let mut paths = Vec::with_capacity(low.inputs.len()); + for input in low.inputs.drain(..) { + Self::add_paths_from_input(state, input, &mut paths)?; } log::debug!("number of paths given to search: {}", paths.len()); if !paths.is_empty() { @@ -1173,6 +1183,95 @@ impl Paths { fn is_only_stdin(&self) -> bool { self.paths.len() == 1 && self.paths[0] == Path::new("-") } + + /// Interprets `input` and adds its content to `output`. + /// + /// `input` may be a positional argument or a source specified through + /// an `--in` or `--in0` flag, whose goal is to reuse existing input file + /// sets and to enable usage of chained ripgrep calls, such as: + /// `rg foo -l0 | rg bar --in0=- -l0 | rg baz --in0=-` + fn add_paths_from_input( + state: &mut State, + input: InputSource, + output: &mut Vec, + ) -> anyhow::Result<()> { + // We need to handle a combination of the following: + // - Positional arguments or input files + // - In case of input files: with LF/CRLF or NUL terminators + // - Files or stdin: this needs to handle `stdin_consumed` + match input { + InputSource::PositionalArgument(_) => { + if state.stdin_consumed && input.is_stdin() { + anyhow::bail!( + "error: attempted to read from stdin: stdin \ + has already been consumed", + ); + } + Self::consume_input(std::io::empty(), input, output)?; + state.stdin_consumed = true; + } + InputSource::LineTerminated(ref path) + | InputSource::NulTerminated(ref path) => { + if input.is_stdin() { + anyhow::ensure!( + !state.stdin_consumed, + "error: attempted to read {} from stdin: stdin \ + has already been consumed", + input.flag().unwrap_or("input") + ); + let stdin = std::io::stdin(); + let locked = stdin.lock(); + Self::consume_input(locked, input, output)?; + state.stdin_consumed = true; + } else { + let file = std::fs::File::open(path).map_err(|err| { + std::io::Error::other(format!("{}: {}", input, err)) + })?; + Self::consume_input(file, input, output)?; + } + } + } + Ok(()) + } + + /// Consumes `input` by adding paths from the associated `read` to `output`. + /// + /// In the case of positional arguments, `read` is not relevant. + fn consume_input( + read: impl std::io::Read, + input: InputSource, + output: &mut Vec, + ) -> anyhow::Result<()> { + match input { + InputSource::PositionalArgument(osstr) => { + output.push(PathBuf::from(osstr)); + } + InputSource::LineTerminated(_) => std::io::BufReader::new(read) + .for_byte_line(|line| { + if line.contains(&b'\x00') { + return Err(std::io::Error::other(format!( + "{}: file contains a NUL byte, \ + did you intend to use --in0 instead of --in?", + input + ))); + } + let s = ByteSlice::to_path(line).map_err(|err| { + std::io::Error::other(format!("{}: {}", input, err)) + })?; + output.push(PathBuf::from(s)); + Ok(true) + })?, + InputSource::NulTerminated(_) => std::io::BufReader::new(read) + .for_byte_record(b'\x00', |record| { + let s = ByteSlice::to_path(record).map_err(|err| { + std::io::Error::other(format!("{}: {}", input, err)) + })?; + output.push(PathBuf::from(s)); + Ok(true) + })?, + }; + Ok(()) + } } /// The "binary detection" configuration that ripgrep should use. diff --git a/crates/core/flags/lowargs.rs b/crates/core/flags/lowargs.rs index 08775d0b6c..2f0f7a9354 100644 --- a/crates/core/flags/lowargs.rs +++ b/crates/core/flags/lowargs.rs @@ -4,7 +4,7 @@ Provides the definition of low level arguments from CLI flags. use std::{ ffi::{OsStr, OsString}, - path::PathBuf, + path::{Path, PathBuf}, }; use { @@ -34,7 +34,7 @@ pub(crate) struct LowArgs { // Essential arguments. pub(crate) special: Option, pub(crate) mode: Mode, - pub(crate) positional: Vec, + pub(crate) inputs: Vec, pub(crate) patterns: Vec, // Everything else, sorted lexicographically. pub(crate) binary: BinaryMode, @@ -649,6 +649,57 @@ pub(crate) enum MmapMode { Never, } +/// Represents a source of input paths to be searched. +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum InputSource { + /// A positional argument on the command line. Provides a single path. + PositionalArgument(OsString), + /// A text file with newline-terminated paths. Comes from the `--in` flag. + LineTerminated(PathBuf), + /// A binary file with NUL-terminated paths. Comes from the `--in0` flag. + NulTerminated(PathBuf), +} + +impl InputSource { + /// Returns true if this source reads from stdin. + pub(crate) fn is_stdin(&self) -> bool { + match self { + InputSource::PositionalArgument(osarg) => osarg == OsStr::new("-"), + InputSource::LineTerminated(path) => path == Path::new("-"), + InputSource::NulTerminated(path) => path == Path::new("-"), + } + } + + /// Returns the command-line flag used for this source. + pub(crate) fn flag(&self) -> Option<&str> { + match self { + InputSource::PositionalArgument(_) => None, + InputSource::LineTerminated(_) => Some("--in"), + InputSource::NulTerminated(_) => Some("--in0"), + } + } +} + +impl std::fmt::Display for InputSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_stdin() { + write!(f, "stdin") + } else { + match self { + InputSource::PositionalArgument(osarg) => { + write!(f, "{}", osarg.to_string_lossy()) + } + InputSource::LineTerminated(path) => { + write!(f, "{}", path.display()) + } + InputSource::NulTerminated(path) => { + write!(f, "{}", path.display()) + } + } + } + } +} + /// Represents a source of patterns that ripgrep should search for. /// /// The reason to unify these is so that we can retain the order of `-f/--flag` diff --git a/crates/core/flags/parse.rs b/crates/core/flags/parse.rs index edda40a40b..92e3e9f566 100644 --- a/crates/core/flags/parse.rs +++ b/crates/core/flags/parse.rs @@ -10,7 +10,7 @@ use crate::flags::{ Flag, FlagValue, defs::FLAGS, hiargs::HiArgs, - lowargs::{LoggingMode, LowArgs, SpecialMode}, + lowargs::{InputSource, LoggingMode, LowArgs, SpecialMode}, }; /// The result of parsing CLI arguments. @@ -227,7 +227,10 @@ impl Parser { while let Some(arg) = p.next().context("invalid CLI arguments")? { let lookup = match arg { lexopt::Arg::Value(value) => { - args.positional.push(value); + // Note: the first positional argument may be + // reinterpreted as a pattern later on + // if no -e/--regexp or -f/--file is given. + args.inputs.push(InputSource::PositionalArgument(value)); continue; } lexopt::Arg::Short(ch) if ch == 'h' => { diff --git a/tests/input.rs b/tests/input.rs new file mode 100644 index 0000000000..5872ca4bce --- /dev/null +++ b/tests/input.rs @@ -0,0 +1,212 @@ +use crate::util::{Dir, TestCommand, sort_lines}; + +// This file tests for ripgrep's handling of input sources +// and relations between them. +// See: https://github.com/BurntSushi/ripgrep/issues/3459 + +// Tests for correct usage + +rgtest!(positional_args, |dir: Dir, mut cmd: TestCommand| { + dir.create("foo", "match"); + dir.create("bar", "match"); + dir.create("baz", "match"); + dir.create("other", "match"); + cmd.arg("match").arg("foo").arg("bar").arg("baz").arg("-l"); + eqnice!(sort_lines("foo\nbar\nbaz\n"), sort_lines(&cmd.stdout())); +}); + +rgtest!(in_terminated, |dir: Dir, mut cmd: TestCommand| { + dir.create("input", "foo\nbar\r\nbaz\n"); // Tests both LF and CRLF + dir.create("foo", "match"); + dir.create("bar", "match"); + dir.create("baz", "match"); + dir.create("other", "match"); + cmd.arg("match").arg("--in").arg("input").arg("-l"); + eqnice!(sort_lines("foo\nbar\nbaz\n"), sort_lines(&cmd.stdout())); +}); + +rgtest!(in0_terminated, |dir: Dir, mut cmd: TestCommand| { + dir.create("input", "foo\x00bar\x00baz\x00"); + dir.create("foo", "match"); + dir.create("bar", "match"); + dir.create("baz", "match"); + dir.create("other", "match"); + cmd.arg("match").arg("--in0").arg("input").arg("-l"); + eqnice!(sort_lines("foo\nbar\nbaz\n"), sort_lines(&cmd.stdout())); +}); + +rgtest!(in_unterminated, |dir: Dir, mut cmd: TestCommand| { + dir.create("input", "foo\nbar\r\nbaz"); // Tests both LF and CRLF + dir.create("foo", "match"); + dir.create("bar", "match"); + dir.create("baz", "match"); + dir.create("other", "match"); + cmd.arg("match").arg("--in").arg("input").arg("-l"); + eqnice!(sort_lines("foo\nbar\nbaz\n"), sort_lines(&cmd.stdout())); +}); + +rgtest!(in0_unterminated, |dir: Dir, mut cmd: TestCommand| { + dir.create("input", "foo\x00bar\x00baz"); + dir.create("foo", "match"); + dir.create("bar", "match"); + dir.create("baz", "match"); + dir.create("other", "match"); + cmd.arg("match").arg("--in0").arg("input").arg("-l"); + eqnice!(sort_lines("foo\nbar\nbaz\n"), sort_lines(&cmd.stdout())); +}); + +// Tests specific to --in + +rgtest!(in_with_nul_byte, |dir: Dir, mut cmd: TestCommand| { + dir.create("input", "foo\x00bar"); + dir.create("foo", "match"); + dir.create("bar", "match"); + cmd.arg("match").arg("--in").arg("input"); + cmd.assert_exit_code(2); +}); + +// Tests for handling of non-existing files + +rgtest!(in_non_existing_file, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match").arg("--in").arg("does_not_exist"); + cmd.assert_exit_code(2); +}); + +rgtest!(in0_non_existing_file, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match").arg("--in0").arg("does_not_exist"); + cmd.assert_exit_code(2); +}); + +rgtest!(in_contains_non_existing_file, |dir: Dir, mut cmd: TestCommand| { + dir.create("input", "foo"); + cmd.arg("match").arg("--in").arg("input"); + cmd.assert_exit_code(2); +}); + +rgtest!(in0_contains_non_existing_file, |dir: Dir, mut cmd: TestCommand| { + dir.create("input", "foo"); + cmd.arg("match").arg("--in0").arg("input"); + cmd.assert_exit_code(2); +}); + +// Tests for stdin consumption + +rgtest!(arg_stdin_consumed_by_in, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--in").arg("-"); + cmd.arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(arg_stdin_consumed_by_in0, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--in0").arg("-"); + cmd.arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(arg_stdin_consumed_by_search, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("-"); + cmd.arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(arg_stdin_consumed_by_file, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--file").arg("-"); + cmd.arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in_stdin_consumed_by_in, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--in").arg("-"); + cmd.arg("--in").arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in_stdin_consumed_by_in0, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--in0").arg("-"); + cmd.arg("--in").arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in_stdin_consumed_by_search, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("-"); + cmd.arg("--in").arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in_stdin_consumed_by_file, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--file").arg("-"); + cmd.arg("--in").arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in0_stdin_consumed_by_in, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--in").arg("-"); + cmd.arg("--in0").arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in0_stdin_consumed_by_in0, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--in0").arg("-"); + cmd.arg("--in0").arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in0_stdin_consumed_by_search, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("-"); + cmd.arg("--in0").arg("-"); + cmd.assert_exit_code(2); +}); + +rgtest!(in0_stdin_consumed_by_file, |_dir: Dir, mut cmd: TestCommand| { + cmd.arg("match"); + cmd.arg("--file").arg("-"); + cmd.arg("--in0").arg("-"); + cmd.assert_exit_code(2); +}); + +// Test for input ordering + +rgtest!(input_order, |dir: Dir, mut cmd: TestCommand| { + // Add a few more files than what we want to actually search. + for i in 1..=12 { + dir.create(format!("file{}", i), "match"); + } + + dir.create("input-6-3", "file6\nfile3\n"); + dir.create("input-1-8", "file1\x00file8\x00"); + + cmd.arg("match").arg("--threads").arg("1").arg("-l"); + + // Mingle the file order to exclude any potential sorting effect. + cmd.arg("file7").arg("file5"); + cmd.arg("--in").arg("input-6-3"); + cmd.arg("file2").arg("file4"); + cmd.arg("--in0").arg("input-1-8"); + cmd.arg("file10").arg("file9"); + + let expected = "\ +file7 +file5 +file6 +file3 +file2 +file4 +file1 +file8 +file10 +file9 +"; + + eqnice!(expected, &cmd.stdout()); +}); diff --git a/tests/tests.rs b/tests/tests.rs index cf18731825..641e19bdf8 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -15,6 +15,8 @@ mod feature; // Tests ripgrep's indexing feature. #[cfg(feature = "unstable-index")] mod index; +// Tests for ripgrep's handling of input sources. +mod input; // Tests for ripgrep's JSON format. mod json; // Miscellaneous tests grouped in a haphazard manner. Try not to add more. diff --git a/tests/util.rs b/tests/util.rs index c0aa59cf91..2665a913ee 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -279,6 +279,7 @@ impl TestCommand { } /// Set an environment variable for this command. + #[allow(dead_code)] // unused on Windows pub fn env( &mut self, key: impl AsRef,