To negate a regular expression, first decide what you want to exclude. [^,] matches one character other than a comma. (?!...) checks that text ahead does not match. (?<!...) checks that text behind does not match. To exclude whole lines in a shell, grep -v is often clearer than putting every condition into one regex. These techniques are different: a negated character class cannot exclude a complete word, and lookarounds are not available in every regex engine.
| Need | Example | Scope |
|---|---|---|
| Anything except one character | [^,]+ |
A run of characters that contains no comma |
A line with ERROR but not the word DEBUG |
^(?!.*\bDEBUG\b).*ERROR.*$ |
One line, in an engine with lookahead |
ERROR unless immediately preceded by IGNORE_ |
(?<!IGNORE_)ERROR |
The position before ERROR, in an engine with lookbehind |
Lines with ERROR except those containing healthcheck |
Two fixed-string grep stages |
Complete input lines; no lookaround needed |
The examples below are case-sensitive. They also assume one record per line unless stated otherwise. MDN's regular-expression reference documents character classes, anchors, and lookaround assertions in JavaScript.
Exclude a character with [^...]
The caret immediately after [ negates a character class. [^,] matches exactly one character that is not a comma; [^,]+ matches one or more such characters. On api,500, a search for [^,]+ first returns api. It does not mean “match everything except the string , wherever it occurs”; it consumes a run until a comma is reached.
[^0-9] matches one character other than an ASCII digit. [^/] can match one character within a slash-delimited path segment. A common mistake is [^ERROR]: it does not mean “anything except the word ERROR.” It excludes the individual characters E, R, and O; the repeated R adds no new meaning. Use a lookahead or a second filtering step for whole words. MDN's character-class documentation gives the single-character rule.
The same caret means something else outside brackets: ^ERROR anchors ERROR to the start of the input, or the start of a line when the engine is in multiline mode. Character classes also have their own newline behavior. In JavaScript and Python, a negated class such as [^,] can match a newline because newline is not a comma. If your input is a multi-line string, limit the class to the actual record boundary instead of assuming it stops at the visible end of a line.
Exclude a word or a whole line with negative lookahead
A negative lookahead, (?!pattern), succeeds only if pattern does not match at the current position. It checks without consuming characters. To keep lines containing ERROR but reject lines containing the whole word DEBUG, use this pattern in JavaScript or Python on individual lines:
^(?!.*\bDEBUG\b).*ERROR.*$
It matches ERROR db timeout. It does not match DEBUG ERROR probe. The ^ makes the exclusion run at the beginning of the line; the .*\bDEBUG\b inside the assertion searches forward for the word; the rest requires ERROR somewhere on that line. Both MDN and Python's re documentation describe negative lookahead.
Anchoring matters. Without ^, a search engine may start matching after the excluded text and find a later substring that passes the assertion. The lookahead rejects a line only when it is applied from the position that represents the whole line's start. If you are searching a complete multi-line string, decide whether to process it line by line or enable the engine's multiline mode deliberately. The . in this example does not normally cross a newline, while the meaning of ^ and $ changes with flags.
The \b markers are word boundaries, not literal spaces. They keep DEBUG from matching the middle of NODEBUG. But they are not a universal definition of a natural-language word: underscores, Unicode characters, and engine options can change the boundary behavior. For example, an underscore adjacent to DEBUG is treated as part of a word in common JavaScript and Python modes. If you mean a literal substring anywhere, omit \b; if you mean a field value, parse or compare that field instead of guessing its boundaries from free text.
A simpler variant rejects any line containing healthcheck, whether or not it is a separate word:
^(?!.*healthcheck).*ERROR.*$
That matches ERROR database unavailable and rejects ERROR healthcheck failed. It also rejects ERROR healthcheck_v2 failed because the exclusion is a substring. Decide which behavior you need before changing the pattern.
Exclude immediate preceding context with negative lookbehind
Negative lookbehind, (?<!pattern), checks text immediately before the current position. For example:
(?<!IGNORE_)ERROR
This finds ERROR in ERROR database unavailable, but not the ERROR in IGNORE_ERROR. It does not reject every line that happens to contain IGNORE_ elsewhere; only the exact prefix immediately before the matched word matters. MDN's lookbehind guide explains this positional behavior.
Lookbehind is useful when the preceding text has a known shape. Python's standard re module requires the expression inside lookbehind to match a fixed length, so (?<!IGNORE_) is valid but a variable-length idea such as (?<!IGNORE_.*) is not. See the current Python re syntax reference. If the excluded context can appear anywhere earlier in a line, a start-anchored negative lookahead or a second filter is usually the clearer tool.
Use grep -v when the task is line filtering
If the input is a line-oriented file, a two-stage command can express the rule plainly:
grep -F 'ERROR' app.log | grep -Fv 'healthcheck'
The first grep keeps lines containing the literal, case-sensitive text ERROR. The second keeps only lines without the literal substring healthcheck; -v inverts the selection, and -F treats the text as fixed strings instead of regex. Given ERROR db timeout, ERROR healthcheck failed, and INFO healthcheck ok, only ERROR db timeout is printed. The GNU grep manual documents these matching options.
Do not assume grep -E enables lookahead: it selects extended POSIX-style regular expressions, which do not have (?!...) or (?<!...). GNU grep's -P selects Perl-compatible syntax when that support is available in the installed build. For this simple exclusion, the two fixed-string filters above avoid that dependency and are easier to review. If the unwanted term is a regex rather than literal text, use grep -Ev 'pattern' for the second stage and test that pattern against sample lines.
grep -v removes entire lines, not just the unwanted substring. If a single log record spans several physical lines, filtering one physical line at a time may leave the rest of that record behind. Assemble or parse the records first if the decision must apply to a whole event.
Check the regex engine before copying a pattern
Regex syntax belongs to an engine and a host tool, not merely to the word “regex.” A pattern that works in a JavaScript tester may fail in a shell command or a search field. The official references show these boundaries:
- JavaScript: supports negated classes, negative lookahead, and negative lookbehind in current engines. Use the appropriate flags when testing a whole multi-line string. MDN documents the syntax.
- Python
re: supports both negative lookaround forms, with a fixed-length requirement inside lookbehind. Use raw strings such asr'(?<!IGNORE_)ERROR'so Python string escaping does not obscure the regex. Python documentation lists the details. - GNU grep:
-Edoes not add lookaround;-Puses PCRE syntax if compiled with the needed support.-vworks independently of lookaround. GNU grep documentation distinguishes the modes. - RE2: supports
[^...]but deliberately omits lookahead and lookbehind. Use separate matching steps or application logic for whole-word and context exclusions. RE2's syntax list marks both lookaround forms unsupported.
Do not infer that a log tool uses RE2, PCRE2, or JavaScript regex from the shape of its search box. Check that tool's documentation or test its actual runtime. No Fluxtail-specific regex engine is assumed here.
Test what should match and what should not
A negated pattern needs both positive and negative examples. Before using one in an application or filter, test at least these cases: the excluded word at the start, middle, and end; a word that merely contains the excluded letters; different letter case; an empty line; and a multi-line record if that is possible. Confirm whether the tool searches for a substring or requires the entire input to match.
Keep the rule readable. If an assertion grows into several nested exclusions, separate filters or structured-field comparisons may be easier to maintain. If the source is JSON, compare its level or path field after parsing rather than applying a text regex to serialized JSON whose field order and escaping may change.
The reliable approach is to choose the smallest operation that expresses the exclusion: [^...] for a character, a lookaround for supported positional checks, and grep -v or ordinary code for whole-line rejection. Then run the exact expression in the engine that will execute it.