TechEarl

How to Exclude Matches with grep -v (Invert Match)

grep -v 'pattern' file prints every line that does NOT match. The flag reference, how to exclude multiple patterns, the strip-comments-and-blank-lines pipeline, the double-negative trap where -v of an OR becomes an AND of negations, and the macOS BSD vs GNU differences.

Ishan KarunaratneIshan Karunaratne⏱️ 12 min readUpdated
Use grep -v 'pattern' file to print every line that does not match. Exclude multiple patterns with -e or -vE, strip comments and blank lines, count with -vc, and avoid the OR-becomes-AND double-negative trap.

grep -v 'pattern' file prints every line that does not match the pattern. The -v flag inverts the match: instead of "show me the lines that contain this", you get "show me the lines that don't". It is the flag I reach for whenever the easiest way to describe what I want is to describe what I want gone.

The classic use is trimming noise. A log file is full of DEBUG lines and you want everything except those. A config file is half comments and blank lines and you want only the directives. A ps listing includes the grep process itself and you want it filtered out. All of those are grep -v jobs.

The one trap, covered in detail below, is that inverting an OR pattern does not give you "not A or not B". It gives you "not A and not B". The logic flips when you negate it, and that flip is the single most common grep -v bug.

Set your values

Try it with your own values

Set your OS, search path, and the pattern to exclude. Every grep example below updates with your values.

The one-liner

bash· Linux (GNU)
grep -v ':pattern' app.log

That returns every line of app.log that does not contain :pattern. On PowerShell, the equivalent of -v is the -NotMatch switch on Select-String.

The mental model: grep decides match or no-match per line, then prints the matching lines. -v keeps the per-line decision but prints the no-match lines instead. Everything else, regex flavor, recursion, line numbers, exit status, works the same as a normal grep.

Exclude multiple patterns

To drop lines matching any of several patterns, you have two clean options. The first uses -e once per pattern:

bash· Linux (GNU)
grep -v -e ':pattern' -e 'TRACE' app.log

The second collapses the patterns into a single extended-regex alternation with -E:

bash· Linux (GNU)
grep -vE ':pattern|TRACE|INFO' app.log

Both forms are equivalent here: a line survives only if it matches none of the listed patterns. Use -e when the patterns are literal strings (it avoids regex escaping); use -vE when you are already thinking in regex. The grep multiple patterns article covers the OR mechanics in full.

Strip comments and blank lines

The single most useful grep -v recipe: read a heavily commented config file without the noise. Comments start with #, blank lines are empty. Chain two inverts:

bash· Linux (GNU)
grep -v '^#' nginx.conf | grep -v '^$'

Here ^# anchors # to the start of the line, so a # mid-line in a value is not stripped. ^$ is an empty line: start anchor immediately followed by end anchor, nothing in between.

You can collapse the two greps into one with an alternation:

bash· Linux (GNU)
grep -vE '^(#|$)' nginx.conf

^(#|$) reads as "a line that starts with either # or end-of-line". Inverting it keeps only lines that are neither comments nor blank. This is the fastest way to see what a config file actually does. One refinement worth knowing: some files use leading-whitespace comments, so ^\s*# is the more robust comment pattern if indentation is in play.

Count non-matching lines

-c counts matching lines. Combine it with -v and you count the lines that do not match:

bash· Linux (GNU)
grep -vc ':pattern' app.log

grep -vc 'DEBUG' app.log answers "how many non-debug lines are in this log". It is the count of the inverted set, not the inverse of the count, so it sums with grep -c 'DEBUG' app.log to the total line count of the file.

Invert plus recursive

-v composes with -r. Search a tree and report only the lines that do not match:

bash· Linux (GNU)
grep -rnv ':pattern' :search_path

-n prefixes line numbers, -r walks the tree. Be aware this is rarely what you want at scale: across a whole repository, "every line that does not contain DEBUG" is almost the entire codebase. Recursive -v is most useful scoped to a small directory or paired with --include to limit the file set.

Invert plus case-insensitive

-i makes the pattern match case-insensitively before -v inverts the result:

bash· Linux (GNU)
grep -iv ':pattern' app.log

With -iv the pattern debug excludes lines containing debug, DEBUG, Debug, and every other casing. Note that Select-String is case-insensitive by default, so the Windows variant needs no extra switch.

Invert plus whole-word

-w restricts matches to whole words, so the pattern only matches when bounded by non-word characters. Inverting that drops lines that contain the word as a standalone token:

bash· Linux (GNU)
grep -vw ':pattern' app.log

The distinction matters. grep -v 'log' drops lines containing login, catalog, and dialog because log is a substring of all three. grep -vw 'log' only drops lines where log appears as a whole word, leaving login and friends untouched. PowerShell has no -w equivalent, so the Windows variant uses explicit \b word boundaries in the pattern.

The double-negative trap

This is the bug that catches everyone. You want lines that contain neither error nor warn. The instinct is to write the OR pattern you would use to find them, then add -v:

bash
grep -vE 'error|warn' app.log

That is correct, but it is easy to misread why. The pattern error|warn matches a line that contains error or warn. Inverting it with -v keeps lines that do not match, meaning lines that contain neither. The OR became an AND of negations:

NOT (error OR warn) = (NOT error) AND (NOT warn)

This is De Morgan's law, and grep -v applies it for you whether you noticed or not. Where people go wrong is the opposite case: you want lines that lack error or lack warn (a much weaker filter that keeps almost everything). There is no single inverted regex for that, because grep's pattern is one expression and -v negates the whole thing.

The rule to remember: grep -v of an alternation is always an AND. If you genuinely need an OR of negations, you cannot express it with one -v. You chain greps with a pipe, where each stage is its own independent filter, and even then a pipeline of grep -v is also an AND (each stage removes more). To get an OR of "lacks A" / "lacks B", you need set logic outside grep, for example comm or awk. In practice you almost never want that, so the trap is mostly about reading grep -vE 'A|B' correctly: it excludes A and excludes B, both.

macOS BSD grep vs GNU grep

The -v flag itself is identical on both, but the flags you commonly pair with it diverge.

FeatureGNU grep (Linux)BSD grep (macOS default)
-v invert matchSupportedSupported
-c, -i, -w, -r, -n with -vSupportedSupported
-e PATTERN (repeatable)SupportedSupported
-E extended regexSupportedSupported
--include / --exclude-dir with -rvSupportedNot supported
-P (PCRE) with -vSupportedNot supported
--invert-match long formSupportedSupported

The short answer: plain grep -v and its common combinations are portable. The moment you add --include, --exclude-dir, or -P to a recursive invert, you are on GNU-only ground. On macOS, brew install grep gives you GNU grep as ggrep. See the grep cheat sheet for the full BSD-versus-GNU divergence table.

Common grep -v mistakes

1. Misreading -vE 'A|B' as an OR of negations. It is an AND: lines that contain neither A nor B. Covered in full in the double-negative section above. This is the number one grep -v bug.

2. Combining -v with -l and expecting "files that do not match". grep -vl 'pattern' * lists files that have at least one line not matching the pattern, which is almost every file. The flag you actually want is -L (capital), which lists files with no matching line. -vl and -L are different operations and the difference is easy to miss.

3. Pairing -v with -o. -o prints only the matched portion of a line. With -v there is no matched portion (the line did not match), so grep -vo produces nothing useful. The combination is logically empty; if you see it in a script, it is a mistake.

4. Forgetting grep's exit status. grep -v exits 0 if any line was printed, 1 if none were. Under set -e, a grep -v that filters out everything aborts the script. Wrap with || true if an empty result is acceptable.

5. Anchoring the wrong end. To strip comment lines you want ^#, not bare #. Bare # matches a # anywhere on the line, so grep -v '#' also drops valid lines that merely contain a # in a value or URL.

6. Unescaped regex metacharacters. grep -v '.' does not exclude lines containing a literal dot; . matches any character, so it excludes every non-empty line. Use grep -vF '.' for a literal-string invert, or escape it as \..

7. The grep -v grep idiom is fragile. ps aux | grep ssh | grep -v grep filters out grep's own process line, but it is brittle. The robust trick is grep '[s]sh': the bracket expression matches ssh but the literal text [s]sh does not match itself, so the grep process never appears in its own results.

When NOT to use grep -v

-v is the right tool when the exclusion set is small and the keep set is large or hard to describe. It is the wrong tool when:

  • A positive pattern is clearer. If you can name what you want directly, grep 'pattern' is more readable than grep -v of everything else. "Show me errors" beats "show me everything that is not info, not debug, not trace". Reach for -v only when the negative is genuinely the simpler description.
  • You need an OR of negations. As covered above, grep -v of an alternation is an AND. If the logic you want is "lacks A or lacks B", grep cannot express it in one pass; use awk with an explicit boolean condition instead.
  • You are filtering structured data. For JSON, YAML, or CSV, line-oriented exclusion is fragile. A jq selection or a yq query understands the structure; grep -v just sees lines.
  • The exclusion list is long and changing. If you find yourself with ten -e patterns, put them in a file and use grep -vf excludes.txt, or rethink whether a positive --include filter would be cleaner.

See also

FAQ

TagsgrepCLIRegexLinuxmacOSBSDShell Scripting
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

The grep -o | sort | uniq -c | sort -rn pipeline counts unique matches and ranks them. Why sort comes before uniq, worked log-analysis examples, sort -u, uniq -d, and the awk one-pass alternative.

How to Count Unique Matches with grep, sort, and uniq

The grep -o 'pattern' file | sort | uniq -c | sort -rn pipeline is the classic log-analysis one-liner. Why sort must come before uniq, how each stage works, worked examples for top IPs and status codes, the awk one-pass alternative for huge files, and the BSD vs GNU notes.

grep -c counts matching lines, not occurrences. Use grep -o piped into wc -l for the true count, grep -rc for per-file counts, grep -vc to count non-matching lines, plus the macOS BSD vs GNU differences.

How to Count Matches with grep -c (and the Line-vs-Occurrence Trap)

grep -c counts matching LINES, not occurrences. A line with three hits still counts as 1. The fix is grep -o piped into wc -l, which puts every match on its own line first. Per-file counts, filtering out the :0 noise, counting non-matching lines, and the BSD vs GNU differences.

Wire ElasticPress to WP_Query so WordPress queries hit Elasticsearch or OpenSearch instead of MySQL. Install, indexable post types, ep_integrate, wp-cli index, faceted aggregations, and when ES actually beats MySQL FULLTEXT.

How to Use ElasticPress with WP_Query

Wire ElasticPress to WP_Query so WordPress queries hit Elasticsearch (or OpenSearch) instead of MySQL. Covers installation, indexable post types, ep_integrate, the wp-cli index command, faceted search with aggregations, and when ES actually beats MySQL FULLTEXT.