Repository: BurntSushi/ripgrep
Pull request #2649 (2023-11-22 01:20:00)

rollup issue triage
This is basically me trying to go through all of the issues knocking off relatively low hanging fruit.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 795d26b..b323fb4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,8 @@ owns this process itself by writing `roff` directly.
 
 Performance improvements:
 
+* [PERF #1746](https://github.com/BurntSushi/ripgrep/issues/1746):
+  Make some cases with inner literals faster.
 * [PERF #1760](https://github.com/BurntSushi/ripgrep/issues/1760):
   Make most searches with `\b` look-arounds (among others) much faster.
 * [PERF #2591](https://github.com/BurntSushi/ripgrep/pull/2591):
@@ -30,16 +32,30 @@ Feature enhancements:
 
 * Added or improved file type filtering for Ada, DITA, Elixir, Fuchsia, Gentoo,
   Gradle, GraphQL, Markdown, Prolog, Raku, TypeScript, USD, V
+* [FEATURE #1709](https://github.com/BurntSushi/ripgrep/issues/1709):
+  Improve documentation of ripgrep's behavior when stdout is a tty.
+* [FEATURE #1737](https://github.com/BurntSushi/ripgrep/issues/1737):
+  Provide binaries for Apple silicon.
 * [FEATURE #1790](https://github.com/BurntSushi/ripgrep/issues/1790):
   Add new `--stop-on-nonmatch` flag.
 * [FEATURE #1814](https://github.com/BurntSushi/ripgrep/issues/1814):
   Flags are now categorized in `-h/--help` output and ripgrep's man page.
+* [FEATURE #1838](https://github.com/BurntSushi/ripgrep/issues/1838):
+  An error is shown when searching for NUL bytes with binary detection enabled.
 * [FEATURE #2195](https://github.com/BurntSushi/ripgrep/issues/2195):
   When `extra-verbose` mode is enabled in zsh, show extra file type info.
+* [FEATURE #2298](https://github.com/BurntSushi/ripgrep/issues/2298):
+  Add instructions for installing ripgrep using `cargo binstall`.
 * [FEATURE #2409](https://github.com/BurntSushi/ripgrep/pull/2409):
   Added installation instructions for `winget`.
+* [FEATURE #2425](https://github.com/BurntSushi/ripgrep/pull/2425):
+  Shell completions (and man page) can be created via `rg --generate`.
+* [FEATURE #2524](https://github.com/BurntSushi/ripgrep/issues/2524):
+  The `--debug` flag now indicates whether stdin or `./` is being searched.
 * [FEATURE #2643](https://github.com/BurntSushi/ripgrep/issues/2643):
   Make `-d` a short flag for `--max-depth`.
+* [FEATURE #2645](https://github.com/BurntSushi/ripgrep/issues/2645):
+  The `--version` output will now also contain PCRE2 availability information.
 
 Bug fixes:
 
@@ -47,6 +63,10 @@ Bug fixes:
   Don't error when `-v/--invert-match` is used multiple times.
 * [BUG #1275](https://github.com/BurntSushi/ripgrep/issues/1275):
   Fix bug with `\b` assertion in the regex engine.
+* [BUG #1376](https://github.com/BurntSushi/ripgrep/issues/1376):
+  Using `--no-ignore --ignore-vcs` now works as one would expect.
+* [BUG #1622](https://github.com/BurntSushi/ripgrep/issues/1622):
+  Add note about error messages to `-z/--search-zip` documentation.
 * [BUG #1648](https://github.com/BurntSushi/ripgrep/issues/1648):
   Fix bug where sometimes short flags with values, e.g., `-M 900`, would fail.
 * [BUG #1701](https://github.com/BurntSushi/ripgrep/issues/1701):
@@ -59,22 +79,40 @@ Bug fixes:
   Disable mmap searching in all non-64-bit environments.
 * [BUG #1966](https://github.com/BurntSushi/ripgrep/issues/1966):
   Fix bug where ripgrep can panic when printing to stderr.
+* [BUG #2046](https://github.com/BurntSushi/ripgrep/issues/2046):
+  Clarify that `--pre` can accept any kind of path in the documentation.
 * [BUG #2108](https://github.com/BurntSushi/ripgrep/issues/2108):
   Improve docs for `-r/--replace` syntax.
 * [BUG #2198](https://github.com/BurntSushi/ripgrep/issues/2198):
   Fix bug where `--no-ignore-dot` would not ignore `.rgignore`.
+* [BUG #2201](https://github.com/BurntSushi/ripgrep/issues/2201):
+  Improve docs for `-r/--replace` flag.
 * [BUG #2288](https://github.com/BurntSushi/ripgrep/issues/2288):
   `-A` and `-B` now only each partially override `-C`.
 * [BUG #2236](https://github.com/BurntSushi/ripgrep/issues/2236):
   Fix gitignore parsing bug where a trailing `\/` resulted in an error.
 * [BUG #2243](https://github.com/BurntSushi/ripgrep/issues/2243):
   Fix `--sort` flag for values other than `path`.
+* [BUG #2246](https://github.com/BurntSushi/ripgrep/issues/2246):
+  Add note in `--debug` logs when binary files are ignored.
+* [BUG #2337](https://github.com/BurntSushi/ripgrep/issues/2337):
+  Improve docs to mention that `--stats` is always implied by `--json`.
 * [BUG #2381](https://github.com/BurntSushi/ripgrep/issues/2381):
   Make `-p/--pretty` override flags like `--no-line-number`.
 * [BUG #2392](https://github.com/BurntSushi/ripgrep/issues/2392):
   Improve global git config parsing of the `excludesFile` field.
+* [BUG #2418](https://github.com/BurntSushi/ripgrep/pull/2418):
+  Clarify sorting semantics of `--sort=path`.
+* [BUG #2458](https://github.com/BurntSushi/ripgrep/pull/2458):
+  Make `--trim` run before `-M/--max-columns` takes effect.
+* [BUG #2479](https://github.com/BurntSushi/ripgrep/issues/2479):
+  Add documentation about `.ignore`/`.rgignore` files in parent directories.
 * [BUG #2480](https://github.com/BurntSushi/ripgrep/issues/2480):
   Fix bug when using inline regex flags with `-e/--regexp`.
+* [BUG #2505](https://github.com/BurntSushi/ripgrep/issues/2505):
+  Improve docs for `--vimgrep` by mentioning footguns and some work-arounds.
+* [BUG #2519](https://github.com/BurntSushi/ripgrep/issues/2519):
+  Fix incorrect default value in documentation for `--field-match-separator`.
 * [BUG #2523](https://github.com/BurntSushi/ripgrep/issues/2523):
   Make executable searching take `.com` into account on Windows.
 * [BUG #2574](https://github.com/BurntSushi/ripgrep/issues/2574):
diff --git a/GUIDE.md b/GUIDE.md
index d81db04..6d51e46 100644
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -178,11 +178,15 @@ search. By default, when you search a directory, ripgrep will ignore all of
 the following:
 
 1. Files and directories that match glob patterns in these three categories:
-      1. gitignore globs (including global and repo-specific globs).
+      1. `.gitignore` globs (including global and repo-specific globs). This
+         includes `.gitignore` files in parent directories that are part of the
+         same `git` repository. (Unless the `--no-require-git` flag is given.)
       2. `.ignore` globs, which take precedence over all gitignore globs
-         when there's a conflict.
+         when there's a conflict. This includes `.ignore` files in parent
+         directories.
       3. `.rgignore` globs, which take precedence over all `.ignore` globs
-         when there's a conflict.
+         when there's a conflict. This includes `.rgignore` files in parent
+         directories.
 2. Hidden files and directories.
 3. Binary files. (ripgrep considers any file with a `NUL` byte to be binary.)
 4. Symbolic links aren't followed.
@@ -190,7 +194,7 @@ the following:
 All of these things can be toggled using various flags provided by ripgrep:
 
 1. You can disable all ignore-related filtering with the `--no-ignore` flag.
-2. Hidden files and directories can be searched with the `--hidden` (`-.` for 
+2. Hidden files and directories can be searched with the `--hidden` (`-.` for
 short) flag.
 3. Binary files can be searched via the `--text` (`-a` for short) flag.
    Be careful with this flag! Binary files may emit control characters to your
@@ -997,7 +1001,7 @@ used options that will likely impact how you use ripgrep on a regular basis.
   if the pattern contains any uppercase letters. Usually this flag is put into
   alias or a config file.
 * `-F/--fixed-strings`: Disable regular expression matching and treat the pattern
-   as a literal string. 
+   as a literal string.
 * `-w/--word-regexp`: Require that all matches of the pattern be surrounded
   by word boundaries. That is, given `pattern`, the `--word-regexp` flag will
   cause ripgrep to behave as if `pattern` were actually `\b(?:pattern)\b`.
diff --git a/README.md b/README.md
index 8402eec..63c0725 100644
--- a/README.md
+++ b/README.md
@@ -370,6 +370,14 @@ If you're a **Rust programmer**, ripgrep can be installed with `cargo`.
 $ cargo install ripgrep
 ```
 
+Alternatively, one can use [`cargo
+binstall`](https://github.com/cargo-bins/cargo-binstall) to install a ripgrep
+binary directly from GitHub:
+
+```
+$ cargo binstall ripgrep
+```
+
 
 ### Building
 
diff --git a/crates/core/flags/defs.rs b/crates/core/flags/defs.rs
index ed78c28..9200c14 100644
--- a/crates/core/flags/defs.rs
+++ b/crates/core/flags/defs.rs
@@ -537,9 +537,8 @@ When enabled, ripgrep will use block buffering. That is, whenever a matching
 line is found, it will be written to an in-memory buffer and will not be
 written to stdout until the buffer reaches a certain size. This is the default
 when ripgrep's stdout is redirected to a pipeline or a file. When ripgrep's
-stdout is connected to a terminal, line buffering will be used by default.
-Forcing block buffering can be useful when dumping a large amount of contents
-to a terminal.
+stdout is connected to a tty, line buffering will be used by default. Forcing
+block buffering can be useful when dumping a large amount of contents to a tty.
 .sp
 This overrides the \flag{line-buffered} flag.
 "
@@ -709,8 +708,8 @@ impl Flag for Color {
         r"
 This flag controls when to use colors. The default setting is \fBauto\fP, which
 means ripgrep will try to guess when to use colors. For example, if ripgrep is
-printing to a terminal, then it will use colors, but if it is redirected to a
-file or a pipe, then it will suppress color output.
+printing to a tty, then it will use colors, but if it is redirected to a file
+or a pipe, then it will suppress color output.
 .sp
 ripgrep will suppress color output by default in some other circumstances as
 well. These include, but are not limited to:
@@ -851,8 +850,8 @@ the background color for line numbers to yellow:
     rg \-\-colors 'match:fg:magenta' \-\-colors 'line:bg:yellow'
 .EE
 .sp
-Extended colors can be used for \fIvalue\fP when the terminal supports
-ANSI color sequences. These are specified as either \fIx\fP (256-color) or
+Extended colors can be used for \fIvalue\fP when the tty supports ANSI color
+sequences. These are specified as either \fIx\fP (256-color) or
 .IB x , x , x
 (24-bit truecolor) where \fIx\fP is a number between \fB0\fP and \fB255\fP
 inclusive. \fIx\fP may be given as a normal decimal number or a hexadecimal
@@ -2615,10 +2614,11 @@ impl Flag for Heading {
     fn doc_long(&self) -> &'static str {
         r"
 This flag prints the file path above clusters of matches from each file instead
-of printing the file path as a prefix for each matched line. This is the
-default mode when printing to a terminal.
+of printing the file path as a prefix for each matched line.
 .sp
-When \fBstdout\fP is not a terminal, then ripgrep will default to the standard
+This is the default mode when printing to a tty.
+.sp
+When \fBstdout\fP is not a tty, then ripgrep will default to the standard
 grep-like format. Once can force this format in Unix-like environments by
 piping the output of ripgrep to \fBcat\fP. For example, \fBrg\fP \fIfoo\fP \fB|
 cat\fP.
@@ -3392,7 +3392,8 @@ flags are used in concert with \flag{json}.
 .sp
 Other flags that control aspects of the standard output such as
 \flag{only-matching}, \flag{heading}, \flag{replace}, \flag{max-columns}, etc.,
-have no effect when \flag{json} is set.
+have no effect when \flag{json} is set. However, enabling JSON output will
+always implicitly and unconditionally enable \flag{stats}.
 .sp
 A more complete description of the JSON format used can be found here:
 \fIhttps://docs.rs/grep-printer/*/grep_printer/struct.JSON.html\fP.
@@ -3453,11 +3454,11 @@ impl Flag for LineBuffered {
     fn doc_long(&self) -> &'static str {
         r"
 When enabled, ripgrep will always use line buffering. That is, whenever a
-matching line is found, it will be flushed to stdout immediately. This is
-the default when ripgrep's stdout is connected to a terminal, but otherwise,
-ripgrep will use block buffering, which is typically faster. This flag forces
-ripgrep to use line buffering even if it would otherwise use block buffering.
-This is typically useful in shell pipelines, for example:
+matching line is found, it will be flushed to stdout immediately. This is the
+default when ripgrep's stdout is connected to a tty, but otherwise, ripgrep
+will use block buffering, which is typically faster. This flag forces ripgrep
+to use line buffering even if it would otherwise use block buffering. This is
+typically useful in shell pipelines, for example:
 .sp
 .EX
     tail -f something.log | rg foo --line-buffered | rg bar
@@ -3516,8 +3517,9 @@ impl Flag for LineNumber {
     }
     fn doc_long(&self) -> &'static str {
         r"
-Show line numbers (1-based). This is enabled by default stdout is connected to
-a tty.
+Show line numbers (1-based).
+.sp
+This is enabled by default when stdout is connected to a tty.
 .sp
 This flag can be disabled by \flag{no-line-number}.
 "
@@ -3568,8 +3570,9 @@ impl Flag for LineNumberNo {
     }
     fn doc_long(&self) -> &'static str {
         r"
-Suppress line numbers. Line numbers are off by default when stdout is not
-connected to a tty.
+Suppress line numbers.
+.sp
+Line numbers are off by default when stdout is not connected to a tty.
 .sp
 Line numbers can be forcefully turned on by \flag{line-number}.
 "
@@ -4734,7 +4737,7 @@ impl Flag for NoRequireGit {
     fn doc_long(&self) -> &'static str {
         r"
 When this flag is given, source control ignore files such as \fB.gitignore\fP
-are respect even if no \fBgit\fP repository is present.
+are respected even if no \fBgit\fP repository is present.
 .sp
 By default, ripgrep will only respect filter rules from source control ignore
 files when ripgrep detects that the search is executed inside a source control
@@ -5358,9 +5361,9 @@ impl Flag for Pre {
     fn doc_long(&self) -> &'static str {
         r#"
 For each input \fIPATH\fP, this flag causes ripgrep to search the standard
-output of \fICOMMAND\fP \fIPATH\fP instead of the contents of \fIPATH\fP. This
-option expects the \fICOMMAND\fP program to either be an absolute path or to
-be available in your \fBPATH\fP. Either an empty string \fICOMMAND\fP or the
+output of \fICOMMAND\fP \fIPATH\fP instead of the contents of \fIPATH\fP.
+This option expects the \fICOMMAND\fP program to either be a path or to be
+available in your \fBPATH\fP. Either an empty string \fICOMMAND\fP or the
 \fB\-\-no\-pre\fP flag will disable this behavior.
 .sp
 .TP 12
@@ -5966,7 +5969,9 @@ impl Flag for SearchZip {
 This flag instructs ripgrep to search in compressed files. Currently gzip,
 bzip2, xz, LZ4, LZMA, Brotli and Zstd files are supported. This option expects
 the decompression binaries (such as \fBgzip\fP) to be available in your
-\fBPATH\fP.
+\fBPATH\fP. If the required binaries are not found, then ripgrep will not
+emit an error messages by default. Use the \flag{debug} flag to see more
+information.
 .sp
 Note that this flag does not make ripgrep search archive formats as directory
 trees. It only makes ripgrep detect compressed files and then decompress them
@@ -6205,7 +6210,10 @@ for this flag are:
 (Default) Do not sort results. Fastest. Can be multi-threaded.
 .TP 12
 \fBpath\fP
-Sort by file path. Always single-threaded.
+Sort by file path. Always single-threaded. The order is determined by sorting
+files in each directory entry during traversal. This means that given the files
+\fBa/b\fP and \fBa+\fP, the latter will sort after the former even though
+\fB+\fP would normally sort before \fB/\fP.
 .TP 12
 \fBmodified\fP
 Sort by the last modified time on a file. Always single-threaded.
@@ -6220,8 +6228,8 @@ If the chosen (manually or by-default) sorting criteria isn't available on your
 system (for example, creation time is not available on ext4 file systems), then
 ripgrep will attempt to detect this, print an error and exit without searching.
 .sp
-To sort results in reverse or descending order, use the \flag{sortr} flag. Also,
-this flag overrides \flag{sortr}.
+To sort results in reverse or descending order, use the \flag{sortr} flag.
+Also, this flag overrides \flag{sortr}.
 .sp
 Note that sorting results currently always forces ripgrep to abandon
 parallelism and run in a single thread.
@@ -6303,7 +6311,11 @@ for this flag are:
 (Default) Do not sort results. Fastest. Can be multi-threaded.
 .TP 12
 \fBpath\fP
-Sort by file path. Always single-threaded.
+Sort by file path. Always single-threaded. The order is determined by sorting
+files in each directory entry during traversal. This means that given the files
+\fBa/b\fP and \fBa+\fP, the latter will sort before the former even though
+\fB+\fP would normally sort after \fB/\fP when doing a reverse lexicographic
+sort.
 .TP 12
 \fBmodified\fP
 Sort by the last modified time on a file. Always single-threaded.
@@ -6413,6 +6425,8 @@ complete.
 .sp
 This set of aggregate statistics may expand over time.
 .sp
+This flag is always and implicitly enabled when \flag{json} is used.
+.sp
 Note that this flag has no effect if \flag{files}, \flag{files-with-matches} or
 \flag{files-without-match} is passed.
 "
@@ -6761,6 +6775,9 @@ any custom file types). The end result is that \fB\-\-type=all\fP causes
 ripgrep to search in "whitelist" mode, where it will only search files it
 recognizes via its type definitions.
 .sp
+Note that this flag has lower precedence than both the \flag{glob} flag and
+any rules found in ignore files.
+.sp
 To see the list of available file types, use the \flag{type-list} flag.
 "#
     }
@@ -7235,6 +7252,10 @@ if the pattern matches every byte in an input file, then each line will be
 repeated for every byte matched. For this reason, users should only use this
 flag when there is no other choice. Editor integrations should prefer some
 other way of reading results from ripgrep, such as via the \flag{json} flag.
+One alternative to avoiding exorbitant memory usage is to force ripgrep into
+single threaded mode with the \flag{threads} flag. Note though that this will
+not impact the total size of the output, just the heap memory that ripgrep will
+use.
 "
     }
     fn doc_choices(&self) -> &'static [&'static str] {
@@ -7282,9 +7303,9 @@ impl Flag for WithFilename {
         r"
 This flag instructs ripgrep to print the file path for each matching line.
 This is the default when more than one file is searched. If \flag{heading} is
-enabled (the default when printing to a terminal), the file path will be shown
-above clusters of matches from each file; otherwise, the file name will be
-shown as a prefix for each matched line.
+enabled (the default when printing to a tty), the file path will be shown above
+clusters of matches from each file; otherwise, the file name will be shown as a
+prefix for each matched line.
 .sp
 This flag overrides \flag{no-filename}.
 "
diff --git a/crates/core/flags/doc/template.long.help b/crates/core/flags/doc/template.long.help
index 3a32594..83c20c1 100644
--- a/crates/core/flags/doc/template.long.help
+++ b/crates/core/flags/doc/template.long.help
@@ -1,9 +1,9 @@
 ripgrep !!VERSION!!
 Andrew Gallant <jamslam@gmail.com>
 
-ripgrep (rg) recursively searches the current directory for a regex pattern.
-By default, ripgrep will respect gitignore rules and automatically skip hidden
-files/directories and binary files.
+ripgrep (rg) recursively searches the current directory for lines matching
+a regex pattern. By default, ripgrep will respect gitignore rules and
+automatically skip hidden files/directories and binary files.
 
 Use -h for short descriptions and --help for more details.
 
diff --git a/crates/core/flags/doc/template.rg.1 b/crates/core/flags/doc/template.rg.1
index 5c7fbdb..fcc9008 100644
--- a/crates/core/flags/doc/template.rg.1
+++ b/crates/core/flags/doc/template.rg.1
@@ -48,6 +48,11 @@ pattern, e.g. \fBls | rg foo\fP. In some environments, stdin may exist when
 it shouldn't. To turn off stdin detection, one can explicitly specify the
 directory to search, e.g. \fBrg foo ./\fP.
 .sp
+Like other tools such as \fBls\fP, ripgrep will alter its output depending on
+whether stdout is connected to a tty. By default, when printing a tty, ripgrep
+will enable colors, line numbers and a heading format that lists each matching
+file path once instead of once per matching line.
+.sp
 Tip: to disable all smart filtering and make ripgrep behave a bit more like
 classical grep, use \fBrg -uuu\fP.
 .
@@ -165,7 +170,9 @@ this includes the following:
 Any global rules, e.g., in \fB$HOME/.config/git/ignore\fP.
 .
 .IP \(bu 3n
-Any rules in relevant \fB.gitignore\fP files.
+Any rules in relevant \fB.gitignore\fP files. This includes \fB.gitignore\fP
+files in parent directories that are part of the same \fBgit\fP repository.
+(Unless \fB\-\-no\-require\-git\fP is given.)
 .
 .IP \(bu 3n
 Any local rules, e.g., in \fB.git/info/exclude\fP.
@@ -180,10 +187,12 @@ precisely, or otherwise use \fBgit grep\fP for search.
 Additional ignore rules can be provided outside of a \fBgit\fP context:
 .
 .IP \(bu 3n
-Any rules in \fB.ignore\fP.
+Any rules in \fB.ignore\fP. ripgrep will also respect \fB.ignore\fP files in
+parent directories.
 .
 .IP \(bu 3n
-Any rules in \fB.rgignore\fP.
+Any rules in \fB.rgignore\fP. ripgrep will also respect \fB.rgignore\fP files
+in parent directories.
 .
 .IP \(bu 3n
 Any rules in files specified with the \fB\-\-ignore\-file\fP flag.
diff --git a/crates/core/flags/doc/template.short.help b/crates/core/flags/doc/template.short.help
index ad26bf1..0a28566 100644
--- a/crates/core/flags/doc/template.short.help
+++ b/crates/core/flags/doc/template.short.help
@@ -1,9 +1,9 @@
 ripgrep !!VERSION!!
 Andrew Gallant <jamslam@gmail.com>
 
-ripgrep (rg) recursively searches the current directory for a regex pattern.
-By default, ripgrep will respect gitignore rules and automatically skip hidden
-files/directories and binary files.
+ripgrep (rg) recursively searches the current directory for lines matching
+a regex pattern. By default, ripgrep will respect gitignore rules and
+automatically skip hidden files/directories and binary files.
 
 Use -h for short descriptions and --help for more details.
 
diff --git a/crates/core/flags/doc/version.rs b/crates/core/flags/doc/version.rs
index ce46283..8e52861 100644
--- a/crates/core/flags/doc/version.rs
+++ b/crates/core/flags/doc/version.rs
@@ -33,6 +33,7 @@ pub(crate) fn generate_long() -> String {
 
     let mut out = String::new();
     writeln!(out, "{}", generate_short()).unwrap();
+    writeln!(out).unwrap();
     writeln!(out, "features:{}", features().join(",")).unwrap();
     if !compile.is_empty() {
         writeln!(out, "simd(compile):{}", compile.join(",")).unwrap();
@@ -40,9 +41,40 @@ pub(crate) fn generate_long() -> String {
     if !runtime.is_empty() {
         writeln!(out, "simd(runtime):{}", runtime.join(",")).unwrap();
     }
+    let (pcre2_version, _) = generate_pcre2();
+    writeln!(out, "\n{pcre2_version}").unwrap();
     out
 }
 
+/// Generates multi-line version string with PCRE2 information.
+///
+/// This also returns whether PCRE2 is actually available in this build of
+/// ripgrep.
+pub(crate) fn generate_pcre2() -> (String, bool) {
+    let mut out = String::new();
+
+    #[cfg(feature = "pcre2")]
+    {
+        use grep::pcre2;
+
+        let (major, minor) = pcre2::version();
+        write!(out, "PCRE2 {}.{} is available", major, minor).unwrap();
+        if cfg!(target_pointer_width = "64") && pcre2::is_jit_available() {
+            writeln!(out, " (JIT is available)").unwrap();
+        } else {
+            writeln!(out, " (JIT is unavailable)").unwrap();
+        }
+        (out, true)
+    }
+
+    #[cfg(not(feature = "pcre2"))]
+    {
+        writeln!(out, "PCRE2 is not available in this build of ripgrep.")
+            .unwrap();
+        (out, false)
+    }
+}
+
 /// Returns the relevant SIMD features supported by the CPU at runtime.
 ///
 /// This is kind of a dirty violation of abstraction, since it assumes
diff --git a/crates/core/flags/hiargs.rs b/crates/core/flags/hiargs.rs
index b4e0c09..59a178e 100644
--- a/crates/core/flags/hiargs.rs
+++ b/crates/core/flags/hiargs.rs
@@ -171,6 +171,7 @@ impl HiArgs {
         } else {
             std::thread::available_parallelism().map_or(1, |n| n.get()).min(12)
         };
+        log::debug!("using {threads} thread(s)");
         let with_filename = low
             .with_filename
             .unwrap_or_else(|| low.vimgrep || !paths.is_one_file);
@@ -498,9 +499,14 @@ impl HiArgs {
         if let Some(limit) = self.dfa_size_limit {
             builder.dfa_size_limit(limit);
         }
+        if !self.binary.is_none() {
+            builder.ban_byte(Some(b'\x00'));
+        }
         let m = match builder.build_many(&self.patterns.patterns) {
             Ok(m) => m,
-            Err(err) => anyhow::bail!(suggest_multiline(err.to_string())),
+            Err(err) => {
+                anyhow::bail!(suggest_text(suggest_multiline(err.to_string())))
+            }
         };
         Ok(PatternMatcher::RustRegex(m))
     }
@@ -1080,12 +1086,24 @@ impl Paths {
         // mode, but there really is no good way to mitigate it. It's just a
         // consequence of letting the user type 'rg foo' and "guessing" that
         // they meant to search the CWD.
-        let use_cwd = !grep::cli::is_readable_stdin()
+        let is_readable_stdin = grep::cli::is_readable_stdin();
+        let use_cwd = !is_readable_stdin
             || state.stdin_consumed
             || !matches!(low.mode, Mode::Search(_));
+        log::debug!(
+            "using heuristics to determine whether to read from \
+             stdin or search ./ (\
+             is_readable_stdin={is_readable_stdin}, \
+             stdin_consumed={stdin_consumed}, \
+             mode={mode:?})",
+            stdin_consumed = state.stdin_consumed,
+            mode = low.mode,
+        );
         let (path, is_one_file) = if use_cwd {
+            log::debug!("heuristic chose to search ./");
             (PathBuf::from("./"), false)
         } else {
+            log::debug!("heuristic chose to search stdin");
             (PathBuf::from("-"), true)
         };
         Ok(Paths { paths: vec![path], has_implicit_path: true, is_one_file })
@@ -1131,6 +1149,13 @@ impl BinaryDetection {
         };
         BinaryDetection { explicit, implicit }
     }
+
+    /// Returns true when both implicit and explicit binary detection is
+    /// disabled.
+    pub(crate) fn is_none(&self) -> bool {
+        let none = grep::searcher::BinaryDetection::none();
+        self.explicit == none && self.implicit == none
+    }
 }
 
 /// Builds the file type matcher from low level arguments.
@@ -1415,3 +1440,17 @@ When multiline mode is enabled, new line characters can be matched.",
         msg
     }
 }
+
+/// Possibly suggest the `-a/--text` flag.
+fn suggest_text(msg: String) -> String {
+    if msg.contains("pattern contains \"\\0\"") {
+        format!(
+            "{msg}
+
+Consider enabling text mode with the --text flag (or -a for short). Otherwise,
+binary detection is enabled and matching a NUL byte is impossible.",
+        )
+    } else {
+        msg
+    }
+}
diff --git a/crates/core/flags/mod.rs b/crates/core/flags/mod.rs
index aefe163..e2b0ccb 100644
--- a/crates/core/flags/mod.rs
+++ b/crates/core/flags/mod.rs
@@ -30,6 +30,7 @@ pub(crate) use crate::flags::{
         man::generate as generate_man_page,
         version::{
             generate_long as generate_version_long,
+            generate_pcre2 as generate_version_pcre2,
             generate_short as generate_version_short,
         },
     },
diff --git a/crates/core/main.rs b/crates/core/main.rs
index ab2230d..64f35ce 100644
--- a/crates/core/main.rs
+++ b/crates/core/main.rs
@@ -383,6 +383,7 @@ fn generate(mode: crate::flags::GenerateMode) -> anyhow::Result<ExitCode> {
 fn special(mode: crate::flags::SpecialMode) -> anyhow::Result<ExitCode> {
     use crate::flags::SpecialMode;
 
+    let mut exit = ExitCode::from(0);
     let output = match mode {
         SpecialMode::HelpShort => flags::generate_help_short(),
         SpecialMode::HelpLong => flags::generate_help_long(),
@@ -390,33 +391,16 @@ fn special(mode: crate::flags::SpecialMode) -> anyhow::Result<ExitCode> {
         SpecialMode::VersionLong => flags::generate_version_long(),
         // --pcre2-version is a little special because it emits an error
         // exit code if this build of ripgrep doesn't support PCRE2.
-        SpecialMode::VersionPCRE2 => return version_pcre2(),
+        SpecialMode::VersionPCRE2 => {
+            let (output, available) = flags::generate_version_pcre2();
+            if !available {
+                exit = ExitCode::from(1);
+            }
+            output
+        }
     };
     writeln!(std::io::stdout(), "{}", output.trim_end())?;
-    Ok(ExitCode::from(0))
-}
-
-/// The top-level entry point for `--pcre2-version`.
-fn version_pcre2() -> anyhow::Result<ExitCode> {
-    let mut stdout = std::io::stdout().lock();
-
-    #[cfg(feature = "pcre2")]
-    {
-        use grep::pcre2;
-
-        let (major, minor) = pcre2::version();
-        writeln!(stdout, "PCRE2 {}.{} is available", major, minor)?;
-        if cfg!(target_pointer_width = "64") && pcre2::is_jit_available() {
-            writeln!(stdout, "JIT is available")?;
-        }
-        Ok(ExitCode::from(0))
-    }
-
-    #[cfg(not(feature = "pcre2"))]
-    {
-        writeln!(stdout, "PCRE2 is not available in this build of ripgrep.")?;
-        Ok(ExitCode::from(1))
-    }
+    Ok(exit)
 }
 
 /// Prints a heuristic error messages when nothing is searched.
diff --git a/crates/printer/src/json.rs b/crates/printer/src/json.rs
index de4da9c..82ad94d 100644
--- a/crates/printer/src/json.rs
+++ b/crates/printer/src/json.rs
@@ -755,6 +755,23 @@ impl<'p, 's, M: Matcher, W: io::Write> Sink for JSONSink<'p, 's, M, W> {
         Ok(!self.should_quit())
     }
 
+    fn binary_data(
+        &mut self,
+        searcher: &Searcher,
+        binary_byte_offset: u64,
+    ) -> Result<bool, io::Error> {
+        if searcher.binary_detection().quit_byte().is_some() {
+            if let Some(ref path) = self.path {
+                log::debug!(
+                    "ignoring {path}: found binary data at \
+                     offset {binary_byte_offset}",
+                    path = path.display(),
+                );
+            }
+        }
+        Ok(true)
+    }
+
     fn begin(&mut self, _searcher: &Searcher) -> Result<bool, io::Error> {
         self.json.wtr.reset_count();
         self.start_time = Instant::now();
diff --git a/crates/printer/src/standard.rs b/crates/printer/src/standard.rs
index 2287b5d..6cc5374 100644
--- a/crates/printer/src/standard.rs
+++ b/crates/printer/src/standard.rs
@@ -884,9 +884,18 @@ impl<'p, 's, M: Matcher, W: WriteColor> Sink for StandardSink<'p, 's, M, W> {
 
     fn binary_data(
         &mut self,
-        _searcher: &Searcher,
+        searcher: &Searcher,
         binary_byte_offset: u64,
     ) -> Result<bool, io::Error> {
+        if searcher.binary_detection().quit_byte().is_some() {
+            if let Some(ref path) = self.path {
+                log::debug!(
+                    "ignoring {path}: found binary data at \
+                     offset {binary_byte_offset}",
+                    path = path.as_path().display(),
+                );
+            }
+        }
         self.binary_byte_offset = Some(binary_byte_offset);
         Ok(true)
     }
@@ -1100,13 +1109,14 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
         let mut count = 0;
         let mut stepper = LineStep::new(line_term, 0, bytes.len());
         while let Some((start, end)) = stepper.next(bytes) {
-            let line = Match::new(start, end);
+            let mut line = Match::new(start, end);
             self.write_prelude(
                 self.sunk.absolute_byte_offset() + line.start() as u64,
                 self.sunk.line_number().map(|n| n + count),
                 Some(matches[0].start() as u64 + 1),
             )?;
             count += 1;
+            self.trim_ascii_prefix(bytes, &mut line);
             if self.exceeds_max_columns(&bytes[line]) {
                 self.write_exceeded_line(bytes, line, matches, &mut midx)?;
             } else {
@@ -1189,12 +1199,12 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
                     Some(m.start().saturating_sub(line.start()) as u64 + 1),
                 )?;
                 count += 1;
+                self.trim_line_terminator(bytes, &mut line);
+                self.trim_ascii_prefix(bytes, &mut line);
                 if self.exceeds_max_columns(&bytes[line]) {
                     self.write_exceeded_line(bytes, line, &[m], &mut 0)?;
                     continue;
                 }
-                self.trim_line_terminator(bytes, &mut line);
-                self.trim_ascii_prefix(bytes, &mut line);
 
                 while !line.is_empty() {
                     if m.end() <= line.start() {
@@ -1246,6 +1256,14 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
 
     #[inline(always)]
     fn write_line(&self, line: &[u8]) -> io::Result<()> {
+        let line = if !self.config().trim_ascii {
+            line
+        } else {
+            let lineterm = self.searcher.line_terminator();
+            let full_range = Match::new(0, line.len());
+            let range = trim_ascii_prefix(lineterm, line, full_range);
+            &line[range]
+        };
         if self.exceeds_max_columns(line) {
             let range = Match::new(0, line.len());
             self.write_exceeded_line(
@@ -1255,7 +1273,8 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
                 &mut 0,
             )?;
         } else {
-            self.write_trim(line)?;
+            // self.write_trim(line)?;
+            self.write(line)?;
             if !self.has_line_terminator(line) {
                 self.write_line_term()?;
             }
@@ -1274,7 +1293,8 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
             return self.write_line(bytes);
         }
 
-        let line = Match::new(0, bytes.len());
+        let mut line = Match::new(0, bytes.len());
+        self.trim_ascii_prefix(bytes, &mut line);
         if self.exceeds_max_columns(bytes) {
             self.write_exceeded_line(bytes, line, matches, &mut 0)
         } else {
@@ -1298,7 +1318,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
         match_index: &mut usize,
     ) -> io::Result<()> {
         self.trim_line_terminator(bytes, &mut line);
-        self.trim_ascii_prefix(bytes, &mut line);
         if matches.is_empty() {
             self.write(&bytes[line])?;
             return Ok(());
@@ -1535,15 +1554,6 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
         Ok(())
     }
 
-    fn write_trim(&self, buf: &[u8]) -> io::Result<()> {
-        if !self.config().trim_ascii {
-            return self.write(buf);
-        }
-        let mut range = Match::new(0, buf.len());
-        self.trim_ascii_prefix(buf, &mut range);
-        self.write(&buf[range])
-    }
-
     fn write(&self, buf: &[u8]) -> io::Result<()> {
         self.wtr().borrow_mut().write_all(buf)
     }
diff --git a/crates/printer/src/summary.rs b/crates/printer/src/summary.rs
index e69703f..275419d 100644
--- a/crates/printer/src/summary.rs
+++ b/crates/printer/src/summary.rs
@@ -688,6 +688,23 @@ impl<'p, 's, M: Matcher, W: WriteColor> Sink for SummarySink<'p, 's, M, W> {
         Ok(!self.should_quit())
     }
 
+    fn binary_data(
+        &mut self,
+        searcher: &Searcher,
+        binary_byte_offset: u64,
+    ) -> Result<bool, io::Error> {
+        if searcher.binary_detection().quit_byte().is_some() {
+            if let Some(ref path) = self.path {
+                log::debug!(
+                    "ignoring {path}: found binary data at \
+                     offset {binary_byte_offset}",
+                    path = path.as_path().display(),
+                );
+            }
+        }
+        Ok(true)
+    }
+
     fn begin(&mut self, _searcher: &Searcher) -> Result<bool, io::Error> {
         if self.path.is_none() && self.summary.config.kind.requires_path() {
             return Err(io::Error::error_message(format!(
diff --git a/crates/printer/src/util.rs b/crates/printer/src/util.rs
index 04f9e12..825f81c 100644
--- a/crates/printer/src/util.rs
+++ b/crates/printer/src/util.rs
@@ -344,7 +344,7 @@ impl<'a> PrinterPath<'a> {
     }
 
     /// Return this path as an actual `Path` type.
-    fn as_path(&self) -> &Path {
+    pub(crate) fn as_path(&self) -> &Path {
         #[cfg(unix)]
         fn imp<'p>(p: &'p PrinterPath<'_>) -> &'p Path {
             use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
diff --git a/crates/regex/src/ban.rs b/crates/regex/src/ban.rs
new file mode 100644
index 0000000..1bd62e2
--- /dev/null
+++ b/crates/regex/src/ban.rs
@@ -0,0 +1,83 @@
+use regex_syntax::hir::{
+    self, ClassBytesRange, ClassUnicodeRange, Hir, HirKind,
+};
+
+use crate::error::{Error, ErrorKind};
+
+/// Returns an error when a sub-expression in `expr` must match `byte`.
+pub(crate) fn check(expr: &Hir, byte: u8) -> Result<(), Error> {
+    assert!(byte.is_ascii(), "ban byte must be ASCII");
+    let ch = char::from(byte);
+    let invalid = || Err(Error::new(ErrorKind::Banned(byte)));
+    match expr.kind() {
+        HirKind::Empty => {}
+        HirKind::Literal(hir::Literal(ref lit)) => {
+            if lit.iter().find(|&&b| b == byte).is_some() {
+                return invalid();
+            }
+        }
+        HirKind::Class(hir::Class::Unicode(ref cls)) => {
+            if cls.ranges().iter().map(|r| r.len()).sum::<usize>() == 1 {
+                let contains =
+                    |r: &&ClassUnicodeRange| r.start() <= ch && ch <= r.end();
+                if cls.ranges().iter().find(contains).is_some() {
+                    return invalid();
+                }
+            }
+        }
+        HirKind::Class(hir::Class::Bytes(ref cls)) => {
+            if cls.ranges().iter().map(|r| r.len()).sum::<usize>() == 1 {
+                let contains = |r: &&ClassBytesRange| {
+                    r.start() <= byte && byte <= r.end()
+                };
+                if cls.ranges().iter().find(contains).is_some() {
+                    return invalid();
+                }
+            }
+        }
+        HirKind::Look(_) => {}
+        HirKind::Repetition(ref x) => check(&x.sub, byte)?,
+        HirKind::Capture(ref x) => check(&x.sub, byte)?,
+        HirKind::Concat(ref xs) => {
+            for x in xs.iter() {
+                check(x, byte)?;
+            }
+        }
+        HirKind::Alternation(ref xs) => {
+            for x in xs.iter() {
+                check(x, byte)?;
+            }
+        }
+    };
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use regex_syntax::Parser;
+
+    /// Returns true when the given pattern is detected to contain the given
+    /// banned byte.
+    fn check(pattern: &str, byte: u8) -> bool {
+        let hir = Parser::new().parse(pattern).unwrap();
+        super::check(&hir, byte).is_err()
+    }
+
+    #[test]
+    fn various() {
+        assert!(check(r"\x00", 0));
+        assert!(check(r"a\x00", 0));
+        assert!(check(r"\x00b", 0));
+        assert!(check(r"a\x00b", 0));
+        assert!(check(r"\x00|ab", 0));
+        assert!(check(r"ab|\x00", 0));
+        assert!(check(r"\x00?", 0));
+        assert!(check(r"(\x00)", 0));
+
+        assert!(check(r"[\x00]", 0));
+        assert!(check(r"[^[^\x00]]", 0));
+
+        assert!(!check(r"[^\x00]", 0));
+        assert!(!check(r"[\x00a]", 0));
+    }
+}
diff --git a/crates/regex/src/config.rs b/crates/regex/src/config.rs
index 8c69ef5..7ebe035 100644
--- a/crates/regex/src/config.rs
+++ b/crates/regex/src/config.rs
@@ -8,7 +8,7 @@ use {
 };
 
 use crate::{
-    ast::AstAnalysis, error::Error, non_matching::non_matching_bytes,
+    ast::AstAnalysis, ban, error::Error, non_matching::non_matching_bytes,
     strip::strip_from_match,
 };
 
@@ -35,6 +35,7 @@ pub(crate) struct Config {
     pub(crate) dfa_size_limit: usize,
     pub(crate) nest_limit: u32,
     pub(crate) line_terminator: Option<LineTerminator>,
+    pub(crate) ban: Option<u8>,
     pub(crate) crlf: bool,
     pub(crate) word: bool,
     pub(crate) fixed_strings: bool,
@@ -58,6 +59,7 @@ impl Default for Config {
             dfa_size_limit: 1000 * (1 << 20),
             nest_limit: 250,
             line_terminator: None,
+            ban: None,
             crlf: false,
             word: false,
             fixed_strings: false,
@@ -205,6 +207,9 @@ impl ConfiguredHIR {
                 .build()
                 .translate(&pattern, &ast)
                 .map_err(Error::generic)?;
+            if let Some(byte) = config.ban {
+                ban::check(&hir, byte)?;
+            }
             // We don't need to do this for the fixed-strings case above
             // because is_fixed_strings will return false if any pattern
             // contains a line terminator. Therefore, we don't need to strip
diff --git a/crates/regex/src/error.rs b/crates/regex/src/error.rs
index 88a8adb..a75f1f5 100644
--- a/crates/regex/src/error.rs
+++ b/crates/regex/src/error.rs
@@ -60,6 +60,8 @@ pub enum ErrorKind {
     ///
     /// The invalid byte is included in this error.
     InvalidLineTerminator(u8),
+    /// Occurs when a banned byte was found in a pattern.
+    Banned(u8),
 }
 
 impl std::error::Error for Error {}
@@ -76,8 +78,15 @@ impl std::fmt::Display for Error {
             ErrorKind::InvalidLineTerminator(byte) => {
                 write!(
                     f,
-                    "line terminators must be ASCII, but {} is not",
-                    [byte].as_bstr()
+                    "line terminators must be ASCII, but {byte:?} is not",
+                    byte = [byte].as_bstr(),
+                )
+            }
+            ErrorKind::Banned(byte) => {
+                write!(
+                    f,
+                    "pattern contains {byte:?} but it is impossible to match",
+                    byte = [byte].as_bstr(),
                 )
             }
         }
diff --git a/crates/regex/src/lib.rs b/crates/regex/src/lib.rs
index 4693bff..93e477b 100644
--- a/crates/regex/src/lib.rs
+++ b/crates/regex/src/lib.rs
@@ -9,6 +9,7 @@ pub use crate::{
 };
 
 mod ast;
+mod ban;
 mod config;
 mod error;
 mod literal;
diff --git a/crates/regex/src/matcher.rs b/crates/regex/src/matcher.rs
index f3f673f..003d415 100644
--- a/crates/regex/src/matcher.rs
+++ b/crates/regex/src/matcher.rs
@@ -286,6 +286,22 @@ impl RegexMatcherBuilder {
         self
     }
 
+    /// Ban a byte from occurring in a regular expression pattern.
+    ///
+    /// If this byte is found in the regex pattern, then an error will be
+    /// returned at construction time.
+    ///
+    /// This is useful when binary detection is enabled. Callers will likely
+    /// want to ban the same byte that is used to detect binary data, i.e.,
+    /// the NUL byte. The reason for this is that when binary detection is
+    /// enabled, it's impossible to match a NUL byte because binary detection
+    /// will either quit when one is found, or will convert NUL bytes to line
+    /// terminators to avoid exorbitant heap usage.
+    pub fn ban_byte(&mut self, byte: Option<u8>) -> &mut RegexMatcherBuilder {
+        self.config.ban = byte;
+        self
+    }
+
     /// Set the line terminator to `\r\n` and enable CRLF matching for `$` in
     /// regex patterns.
     ///
diff --git a/crates/searcher/src/line_buffer.rs b/crates/searcher/src/line_buffer.rs
index badcc43..822bdee 100644
--- a/crates/searcher/src/line_buffer.rs
+++ b/crates/searcher/src/line_buffer.rs
@@ -47,7 +47,7 @@ pub(crate) fn alloc_error(limit: usize) -> io::Error {
 /// is that binary data often indicates data that is undesirable to search
 /// using textual patterns. Of course, there are many cases in which this isn't
 /// true, which is why binary detection is disabled by default.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
 pub(crate) enum BinaryDetection {
     /// No binary detection is performed. Data reported by the line buffer may
     /// contain arbitrary bytes.
diff --git a/crates/searcher/src/searcher/mod.rs b/crates/searcher/src/searcher/mod.rs
index ff1bea5..d4bd156 100644
--- a/crates/searcher/src/searcher/mod.rs
+++ b/crates/searcher/src/searcher/mod.rs
@@ -52,7 +52,7 @@ type Range = Match;
 ///    heap, then binary detection is only guaranteed to be applied to the
 ///    parts corresponding to a match. When `Quit` is enabled, then the first
 ///    few KB of the data are searched for binary data.
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
 pub struct BinaryDetection(line_buffer::BinaryDetection);
 
 impl BinaryDetection {


Test patch:
diff --git a/tests/feature.rs b/tests/feature.rs
index 5321d11..e278685 100644
--- a/tests/feature.rs
+++ b/tests/feature.rs
@@ -425,7 +425,6 @@ rgtest!(f411_parallel_search_stats, |dir: Dir, mut cmd: TestCommand| {
     dir.create("sherlock_2", SHERLOCK);
 
     let lines = cmd.arg("-j2").arg("--stats").arg("Sherlock").stdout();
-    dbg!(&lines);
     assert!(lines.contains("4 matched lines"));
     assert!(lines.contains("2 files contained matches"));
     assert!(lines.contains("2 files searched"));
@@ -657,6 +656,110 @@ but Doctor Watson has to have it taken out for him and dusted,
     eqnice!(expected, cmd.stdout());
 });
 
+rgtest!(f917_trim_multi_standard, |dir: Dir, mut cmd: TestCommand| {
+    const HAYSTACK: &str = "     0123456789abcdefghijklmnopqrstuvwxyz";
+    dir.create("haystack", HAYSTACK);
+    cmd.args(&["--multiline", "--trim", "-r$0", "--no-filename", r"a\n?bc"]);
+
+    let expected = "0123456789abcdefghijklmnopqrstuvwxyz\n";
+    eqnice!(expected, cmd.stdout());
+});
+
+rgtest!(f917_trim_max_columns_normal, |dir: Dir, mut cmd: TestCommand| {
+    const HAYSTACK: &str = "     0123456789abcdefghijklmnopqrstuvwxyz";
+    dir.create("haystack", HAYSTACK);
+    cmd.args(&[
+        "--trim",
+        "--max-columns-preview",
+        "-M8",
+        "--no-filename",
+        "abc",
+    ]);
+
+    let expected = "01234567 [... omitted end of long line]\n";
+    eqnice!(expected, cmd.stdout());
+});
+
+rgtest!(f917_trim_max_columns_matches, |dir: Dir, mut cmd: TestCommand| {
+    const HAYSTACK: &str = "     0123456789abcdefghijklmnopqrstuvwxyz";
+    dir.create("haystack", HAYSTACK);
+    cmd.args(&[
+        "--trim",
+        "--max-columns-preview",
+        "-M8",
+        "--color=always",
+        "--colors=path:none",
+        "--no-filename",
+        "abc",
+    ]);
+
+    let expected = "01234567 [... 1 more match]\n";
+    eqnice!(expected, cmd.stdout());
+});
+
+rgtest!(
+    f917_trim_max_columns_multi_standard,
+    |dir: Dir, mut cmd: TestCommand| {
+        const HAYSTACK: &str = "     0123456789abcdefghijklmnopqrstuvwxyz";
+        dir.create("haystack", HAYSTACK);
+        cmd.args(&[
+            "--multiline",
+            "--trim",
+            "--max-columns-preview",
+            "-M8",
+            // Force the "slow" printing path without actually
+            // putting colors in the output.
+            "--color=always",
+            "--colors=path:none",
+            "--no-filename",
+            r"a\n?bc",
+        ]);
+
+        let expected = "01234567 [... 1 more match]\n";
+        eqnice!(expected, cmd.stdout());
+    }
+);
+
+rgtest!(
+    f917_trim_max_columns_multi_only_matching,
+    |dir: Dir, mut cmd: TestCommand| {
+        const HAYSTACK: &str = "     0123456789abcdefghijklmnopqrstuvwxyz";
+        dir.create("haystack", HAYSTACK);
+        cmd.args(&[
+            "--multiline",
+            "--trim",
+            "--max-columns-preview",
+            "-M8",
+            "--only-matching",
+            "--no-filename",
+            r".*a\n?bc.*",
+        ]);
+
+        let expected = "01234567 [... 0 more matches]\n";
+        eqnice!(expected, cmd.stdout());
+    }
+);
+
+rgtest!(
+    f917_trim_max_columns_multi_per_match,
+    |dir: Dir, mut cmd: TestCommand| {
+        const HAYSTACK: &str = "     0123456789abcdefghijklmnopqrstuvwxyz";
+        dir.create("haystack", HAYSTACK);
+        cmd.args(&[
+            "--multiline",
+            "--trim",
+            "--max-columns-preview",
+            "-M8",
+            "--vimgrep",
+            "--no-filename",
+            r".*a\n?bc.*",
+        ]);
+
+        let expected = "1:1:01234567 [... 0 more matches]\n";
+        eqnice!(expected, cmd.stdout());
+    }
+);
+
 // See: https://github.com/BurntSushi/ripgrep/issues/993
 rgtest!(f993_null_data, |dir: Dir, mut cmd: TestCommand| {
     dir.create("test", "foo\x00bar\x00\x00\x00baz\x00");
diff --git a/tests/regression.rs b/tests/regression.rs
index 04138fd..54490b9 100644
--- a/tests/regression.rs
+++ b/tests/regression.rs
@@ -977,6 +977,20 @@ rgtest!(r1765, |dir: Dir, mut cmd: TestCommand| {
     assert!(!cmd.stdout().is_empty());
 });
 
+// See: https://github.com/BurntSushi/ripgrep/issues/1838
+rgtest!(r1838_nul_error_with_binary_detection, |dir: Dir, _: TestCommand| {
+    // We don't support this error reporting with PCRE2 since we can't parse
+    // the pattern (easily) to give a good error message.
+    if dir.is_pcre2() {
+        return;
+    }
+    dir.create("test", "foo\n");
+
+    dir.command().args(&[r"foo\x00?"]).assert_err();
+    eqnice!("test:foo\n", dir.command().args(&["-a", r"foo\x00?"]).stdout());
+});
+
+// See: https://github.com/BurntSushi/ripgrep/issues/1866
 rgtest!(r1866, |dir: Dir, mut cmd: TestCommand| {
     dir.create("test", "foobar\nfoobar\nfoo quux");
     cmd.args(&[