diff --git a/CHANGELOG.md b/CHANGELOG.md index c8c88cd1..4f9a0992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Feature enhancements: Cargo will now produce static executables on Windows when using MSVC. * [FEATURE #1680](https://github.com/BurntSushi/ripgrep/pull/1680): Add `-.` as a short flag alias for `--hidden`. +* [FEATURE #1842](https://github.com/BurntSushi/ripgrep/issues/1842): + Add `--field-{context,match}-separator` for customizing field delimiters. * [FEATURE #1856](https://github.com/BurntSushi/ripgrep/pull/1856): The README now links to a [Spanish translation](https://github.com/UltiRequiem/traducciones/tree/master/ripgrep). diff --git a/complete/_rg b/complete/_rg index 73ec6c6d..31bc697c 100644 --- a/complete/_rg +++ b/complete/_rg @@ -303,6 +303,8 @@ _rg() { '--context-separator=[specify string used to separate non-continuous context lines in output]:separator' $no"--no-context-separator[don't print context separators]" '--debug[show debug messages]' + '--field-context-separator[set string to delimit fields in context lines]' + '--field-match-separator[set string to delimit fields in matching lines]' '--trace[show more verbose debug messages]' '--dfa-size-limit=[specify upper size limit of generated DFA]:DFA size (bytes)' "(1 stats)--files[show each file that would be searched (but don't search)]" diff --git a/crates/core/app.rs b/crates/core/app.rs index dd9ae2cd..5058c0ea 100644 --- a/crates/core/app.rs +++ b/crates/core/app.rs @@ -568,6 +568,8 @@ pub fn all_args_and_flags() -> Vec { flag_dfa_size_limit(&mut args); flag_encoding(&mut args); flag_engine(&mut args); + flag_field_context_separator(&mut args); + flag_field_match_separator(&mut args); flag_file(&mut args); flag_files(&mut args); flag_files_with_matches(&mut args); @@ -1231,6 +1233,38 @@ This overrides previous uses of --pcre2 and --auto-hybrid-regex flags. args.push(arg); } +fn flag_field_context_separator(args: &mut Vec) { + const SHORT: &str = "Set the field context separator."; + const LONG: &str = long!( + "\ +Set the field context separator, which is used to delimit file paths, line +numbers, columns and the context itself, when printing contextual lines. The +separator may be any number of bytes, including zero. Escape sequences like +\\x7F or \\t may be used. The default value is -. +" + ); + let arg = RGArg::flag("field-context-separator", "SEPARATOR") + .help(SHORT) + .long_help(LONG); + args.push(arg); +} + +fn flag_field_match_separator(args: &mut Vec) { + const SHORT: &str = "Set the match separator."; + const LONG: &str = long!( + "\ +Set the field match separator, which is used to delimit file paths, line +numbers, columns and the match itself. The separator may be any number of +bytes, including zero. Escape sequences like \\x7F or \\t may be used. The +default value is -. +" + ); + let arg = RGArg::flag("field-match-separator", "SEPARATOR") + .help(SHORT) + .long_help(LONG); + args.push(arg); +} + fn flag_file(args: &mut Vec) { const SHORT: &str = "Search for patterns from the given file."; const LONG: &str = long!( diff --git a/crates/core/args.rs b/crates/core/args.rs index 7a448362..caa378bd 100644 --- a/crates/core/args.rs +++ b/crates/core/args.rs @@ -786,8 +786,8 @@ impl ArgMatches { .trim_ascii(self.is_present("trim")) .separator_search(None) .separator_context(self.context_separator()) - .separator_field_match(b":".to_vec()) - .separator_field_context(b"-".to_vec()) + .separator_field_match(self.field_match_separator()) + .separator_field_context(self.field_context_separator()) .separator_path(self.path_separator()?) .path_terminator(self.path_terminator()); if separator_search { @@ -1377,6 +1377,24 @@ impl ArgMatches { } } + /// Returns the unescaped field context separator. If one wasn't specified, + /// then '-' is used as the default. + fn field_context_separator(&self) -> Vec { + match self.value_of_os("field-context-separator") { + None => b"-".to_vec(), + Some(sep) => cli::unescape_os(&sep), + } + } + + /// Returns the unescaped field match separator. If one wasn't specified, + /// then ':' is used as the default. + fn field_match_separator(&self) -> Vec { + match self.value_of_os("field-match-separator") { + None => b":".to_vec(), + Some(sep) => cli::unescape_os(&sep), + } + } + /// Get a sequence of all available patterns from the command line. /// This includes reading the -e/--regexp and -f/--file flags. /// diff --git a/tests/feature.rs b/tests/feature.rs index 3cae7075..36cbad73 100644 --- a/tests/feature.rs +++ b/tests/feature.rs @@ -828,6 +828,99 @@ rgtest!(f1404_nothing_searched_ignored, |dir: Dir, mut cmd: TestCommand| { eqnice!(expected, stderr); }); +// See: https://github.com/BurntSushi/ripgrep/issues/1842 +rgtest!(f1842_field_context_separator, |dir: Dir, _: TestCommand| { + dir.create("sherlock", SHERLOCK); + + // Test the default. + let base = &["-n", "-A1", "Doctor Watsons", "sherlock"]; + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2-Holmeses, success in the province of detective work must always +"; + eqnice!(expected, dir.command().args(base).stdout()); + + // Test that it can be overridden. + let mut args = vec!["--field-context-separator", "!"]; + args.extend(base); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2!Holmeses, success in the province of detective work must always +"; + eqnice!(expected, dir.command().args(&args).stdout()); + + // Test that it can use multiple bytes. + let mut args = vec!["--field-context-separator", "!!"]; + args.extend(base); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2!!Holmeses, success in the province of detective work must always +"; + eqnice!(expected, dir.command().args(&args).stdout()); + + // Test that unescaping works. + let mut args = vec!["--field-context-separator", r"\x7F"]; + args.extend(base); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2\x7FHolmeses, success in the province of detective work must always +"; + eqnice!(expected, dir.command().args(&args).stdout()); + + // Test that an empty separator is OK. + let mut args = vec!["--field-context-separator", r""]; + args.extend(base); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2Holmeses, success in the province of detective work must always +"; + eqnice!(expected, dir.command().args(&args).stdout()); +}); + +// See: https://github.com/BurntSushi/ripgrep/issues/1842 +rgtest!(f1842_field_match_separator, |dir: Dir, _: TestCommand| { + dir.create("sherlock", SHERLOCK); + + // Test the default. + let base = &["-n", "Doctor Watsons", "sherlock"]; + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +"; + eqnice!(expected, dir.command().args(base).stdout()); + + // Test that it can be overridden. + let mut args = vec!["--field-match-separator", "!"]; + args.extend(base); + let expected = "\ +1!For the Doctor Watsons of this world, as opposed to the Sherlock +"; + eqnice!(expected, dir.command().args(&args).stdout()); + + // Test that it can use multiple bytes. + let mut args = vec!["--field-match-separator", "!!"]; + args.extend(base); + let expected = "\ +1!!For the Doctor Watsons of this world, as opposed to the Sherlock +"; + eqnice!(expected, dir.command().args(&args).stdout()); + + // Test that unescaping works. + let mut args = vec!["--field-match-separator", r"\x7F"]; + args.extend(base); + let expected = "\ +1\x7FFor the Doctor Watsons of this world, as opposed to the Sherlock +"; + eqnice!(expected, dir.command().args(&args).stdout()); + + // Test that an empty separator is OK. + let mut args = vec!["--field-match-separator", r""]; + args.extend(base); + let expected = "\ +1For the Doctor Watsons of this world, as opposed to the Sherlock +"; + eqnice!(expected, dir.command().args(&args).stdout()); +}); + rgtest!(no_context_sep, |dir: Dir, mut cmd: TestCommand| { dir.create("test", "foo\nctx\nbar\nctx\nfoo\nctx"); cmd.args(&["-A1", "--no-context-separator", "foo", "test"]);