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
Set your OS, search path, and the pattern to exclude. Every grep example below updates with your values.
The one-liner
grep -v ':pattern' app.logThat 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:
grep -v -e ':pattern' -e 'TRACE' app.logThe second collapses the patterns into a single extended-regex alternation with -E:
grep -vE ':pattern|TRACE|INFO' app.logBoth 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:
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:
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:
grep -vc ':pattern' app.loggrep -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:
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:
grep -iv ':pattern' app.logWith -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:
grep -vw ':pattern' app.logThe 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:
grep -vE 'error|warn' app.logThat 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.
| Feature | GNU grep (Linux) | BSD grep (macOS default) |
|---|---|---|
-v invert match | Supported | Supported |
-c, -i, -w, -r, -n with -v | Supported | Supported |
-e PATTERN (repeatable) | Supported | Supported |
-E extended regex | Supported | Supported |
--include / --exclude-dir with -rv | Supported | Not supported |
-P (PCRE) with -v | Supported | Not supported |
--invert-match long form | Supported | Supported |
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 thangrep -vof everything else. "Show me errors" beats "show me everything that is not info, not debug, not trace". Reach for-vonly when the negative is genuinely the simpler description. - You need an OR of negations. As covered above,
grep -vof an alternation is an AND. If the logic you want is "lacks A or lacks B", grep cannot express it in one pass; useawkwith an explicit boolean condition instead. - You are filtering structured data. For JSON, YAML, or CSV, line-oriented exclusion is fragile. A
jqselection or ayqquery understands the structure;grep -vjust sees lines. - The exclusion list is long and changing. If you find yourself with ten
-epatterns, put them in a file and usegrep -vf excludes.txt, or rethink whether a positive--includefilter would be cleaner.
See also
- grep cheat sheet: the full grep reference covering regex flavors, context lines, recursion, and BSD vs GNU differences.
- grep multiple patterns: the OR mechanics,
-e,-f, and alternation in depth. - grep count matches:
-c, counting unique matches, and frequency tables. - External: GNU grep manual, FreeBSD grep(1) man page.





