Literal characters

Most characters in a pattern are literal: they match exactly themselves. The pattern kWh matches the three characters k, W and h in that order, wherever they occur. This is the regex counterpart of a literal in code: the value is written out as it is.

Two details catch people out. Matching is case-sensitive by default, so kWh does not match "KWH" (a flag changes that). And spaces are literal too: meter 42 with two spaces does not match "meter 42" with one.

import re

readings = "KE-0042: 318.4 kWh; KE-0043: 12 KWH; KE-0044: 77.0 kWh"
print(re.findall(r"kWh", readings))
print(re.findall(r"KE-0043", readings))
print(re.findall(r"KE-0043:  12", readings))  # two spaces: no match
['kWh', 'kWh']
['KE-0043']
[]

Metacharacters

A metacharacter is a character that has a special meaning in a pattern instead of matching itself. In the Perl-style flavors used by Python, JavaScript and PHP (and grep -P), these are the metacharacters outside square brackets:

Character Meaning Explained on
. Any one character (except a newline) this page
\ Escape: makes the next metacharacter literal, or starts a special sequence such as \d this page
[ ] A set of characters, one of which must match Character classes
* + ? { } Repeat the item before them Quantifiers
^ $ Start and end (of the text or line) Anchors
( ) Group parts of the pattern Groups
| Alternation: this or that Alternation

Everything else, including -, :, /, @, #, = and the space, is literal outside brackets. A closing ] or } on its own is usually literal as well (JavaScript with the u flag rejects it), so escaping it is the safe choice.

grep's basic syntax is different

The classic Unix tools use the POSIX (Portable Operating System Interface) syntaxes, explained on grep, find and regular expressions. In basic regular expressions (plain grep and sed), + ? { } ( ) | are literal and only become special with a backslash. In extended syntax (grep -E, sed -E) they behave as in the table above. So the backslash means the opposite in the two syntaxes. Phone numbers with a leading plus show it:

printf '%s\n' '+31 70 123 4567' '0031 70 123 4567' '31 meters' > contacts.txt
echo "--- basic syntax, '+31' (the plus is literal):"
grep '+31' contacts.txt
echo "--- extended syntax, '+31' (the plus is a quantifier with nothing before it):"
grep -E '+31' contacts.txt
echo "--- extended syntax, '\+31' (the plus escaped):"
grep -E '\+31' contacts.txt
echo "--- grep -F, fixed string (no metacharacters at all):"
grep -F '+31' contacts.txt
--- basic syntax, '+31' (the plus is literal):
+31 70 123 4567
--- extended syntax, '+31' (the plus is a quantifier with nothing before it):
grep: warning: + at start of expression
+31 70 123 4567
0031 70 123 4567
31 meters
--- extended syntax, '\+31' (the plus escaped):
+31 70 123 4567
--- grep -F, fixed string (no metacharacters at all):
+31 70 123 4567

In extended syntax, a + at the start has nothing to repeat. GNU grep warns and then ignores it, so the search becomes plain 31 and every line matches. Other engines refuse such a pattern outright (Python and JavaScript: "nothing to repeat"); POSIX leaves it undefined.

grep -F treats the whole pattern as a fixed string, so nothing in it is special. When you search for exact text, such as an error message copied from a log, it is the simplest choice.

The dot

The dot (.) is the metacharacter that matches any single character except a line break. KE-00.2 matches KE-0042, KE-0012 and KE-00x2: exactly one character, whatever it is, in that spot. It is useful for "something goes here", but it is also the most common source of patterns that match more than intended.

IP addresses are the classic case. In 192.0.2.1, each dot is a metacharacter, so the pattern matches 192x0y2z1. And without anything marking the end, it also matches the start of 192.0.2.15. Both mistakes show up in a simple log search:

cat <<'EOF' > firewall.log
deny  src=192.0.2.1   dst=198.51.100.7 port=22
allow src=192.0.2.15  dst=198.51.100.7 port=443
deny  src=203.0.113.9 dst=198.51.100.9 port=22 ref=192-0-2-1
EOF
echo "--- 192.0.2.1 (dots are wildcards):"
grep '192.0.2.1' firewall.log
echo "--- 192\.0\.2\.1 (literal dots):"
grep '192\.0\.2\.1' firewall.log
echo "--- literal dots, and a space must follow:"
grep '192\.0\.2\.1 ' firewall.log
--- 192.0.2.1 (dots are wildcards):
deny  src=192.0.2.1   dst=198.51.100.7 port=22
allow src=192.0.2.15  dst=198.51.100.7 port=443
deny  src=203.0.113.9 dst=198.51.100.9 port=22 ref=192-0-2-1
--- 192\.0\.2\.1 (literal dots):
deny  src=192.0.2.1   dst=198.51.100.7 port=22
allow src=192.0.2.15  dst=198.51.100.7 port=443
--- literal dots, and a space must follow:
deny  src=192.0.2.1   dst=198.51.100.7 port=22

Escaping the dots removes the third line; requiring the space after the address removes the second. The page on anchors and word boundaries shows cleaner ways to say "the number ends here".

The dot and line breaks

In Python, JavaScript and PHP the dot does not match a newline character (\n) unless you switch on "dotall" mode, explained on flags and modes. This only matters when the text has several lines; grep, sed and awk read one line at a time, so the question does not come up there.

import re

ticket = "Subject: meter offline\nBody: KE-0042 stopped reporting"
print(re.search(r"offline.Body", ticket))
print(re.search(r"offline.Body", ticket, re.DOTALL))
None
<re.Match object; span=(15, 27), match='offline\nBody'>

Escaping with a backslash

Escaping means putting a backslash in front of a metacharacter so the engine treats it as a literal character: \. is a real dot, \* a real asterisk, \( a real parenthesis, \\ one real backslash. It is the same idea as an escape sequence in a string, applied to the pattern language.

Pattern Matches Does not match
1.5 1.5, 1x5, 125 15
1\.5 1.5 1x5, 125
\$5\.00 $5.00 5.00, $5x00
\(urgent\) (urgent) urgent
C:\\temp C:\temp C:temp

Inside square brackets, most metacharacters are literal already: [.] is a dot and [$] a dollar sign. Some people prefer [.] over \. because it survives extra layers of quoting; the character classes page lists the few characters that are special inside brackets.

A backslash before a letter is not "nothing"

The backslash does two jobs. Before a metacharacter it removes the special meaning; before many letters it adds one: \d (a digit), \w, \s, \b (a word boundary), \n (a newline). So never escape a letter "to be safe". What happens with a backslash before a letter that has no meaning depends on the flavor:

Engine \q (no meaning) \- (outside brackets)
Python re Error: bad escape A hyphen
JavaScript, no flag The letter q A hyphen
JavaScript with u or v SyntaxError: invalid escape SyntaxError: invalid escape
PHP (PCRE2) Error: unrecognized character follows \ A hyphen
GNU grep, basic or extended The letter q (Debian: no warning) A hyphen

JavaScript is the odd one out: without the u flag it quietly ignores unknown escapes, and with it it refuses them. Run on Node.js 24:

console.log(/\q/.test('q'));        // unknown escape: just the letter q
console.log(/\-/.test('-'));
try {
  new RegExp('\\-', 'u');           // the same pattern in Unicode mode
} catch (error) {
  console.log(error.message);
}
true
true
Invalid regular expression: /\-/u: Invalid escape

Escaping text that comes from outside

Patterns are often built from data: a ticket number someone typed in a search box, a host name from a configuration file. If that text contains metacharacters and you paste it into a pattern unescaped, the pattern means something else, or it does not compile at all. A user searching for "(urgent" breaks a Python search:

import re

search_term = "(urgent"
notes = "INC-31877 (urgent) portal login fails"
print(re.search(search_term, notes))
re.error: missing ), unterminated subpattern at position 0

A crash is the friendly outcome. Worse is a term that compiles but means something else, such as 1.5*2, where the dot and star act as metacharacters, or a pattern that makes the engine work for a very long time (see catastrophic backtracking). Every language has a function that escapes all metacharacters in a piece of text:

import re

search_term = "1.5*2 (urgent)"
pattern = re.escape(search_term)
print(pattern)
print(re.search(pattern, "Formula 1.5*2 (urgent) applied").group())
print(re.search(pattern, "Formula 1x5552 urgent applied"))
1\.5\*2\ \(urgent\)
1.5*2 (urgent)
None

In PHP, preg_quote() does the same. Pass your pattern delimiter (usually /) as the second argument, because the delimiter is not a metacharacter and is otherwise left alone:

<?php
$searchTerm = '1.5*2 (kWh/day)';
$pattern = '/' . preg_quote($searchTerm, '/') . '/';
echo $pattern, "\n";
var_dump(preg_match($pattern, 'Usage 1.5*2 (kWh/day) estimated'));
/1\.5\*2 \(kWh\/day\)/
int(1)

JavaScript gained RegExp.escape() in ECMAScript 2025; it is available from Node.js 24 and in current browsers, but not in older runtimes such as Node.js 20. Until all your targets have it, a short function does the job. It puts a backslash before every character that is special in a pattern:

function escapeRegExp(text) {
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

const searchTerm = '1.5*2 (urgent)';
const pattern = new RegExp(escapeRegExp(searchTerm));
console.log(pattern.source);
console.log(pattern.test('Formula 1.5*2 (urgent) applied'));
console.log(pattern.test('Formula 1x5552 urgent applied'));
console.log(typeof RegExp.escape); // 'function' from Node.js 24
1\.5\*2 \(urgent\)
true
false
function

Two layers: the string, then the pattern

In code, a pattern is usually written as a string. That string is read twice: first by the programming language, which applies its own escape sequences, and only then by the regex engine. A backslash meant for the engine can be eaten by the first reader.

Three Python spellings of a pattern. r"\bcat\b" reaches the engine as backslash b cat backslash b, a word boundary. "\bcat\b" reaches it as backspace characters around cat. "\\bcat\\b" reaches it as backslash b again. in your code string the engine gets engine reads r"\bcat\b" \bcat\b word boundary, cat, … "\bcat\b" ⌫cat⌫ backspace, cat, backspace "\\bcat\\b" \bcat\b word boundary, cat, …
In a normal Python string, \b is the backspace character before the regex engine ever sees it. A raw string, or a doubled backslash, passes \b through.

Raw strings

A raw string is a string literal in which backslashes are kept exactly as typed instead of starting escape sequences. In Python you write it with an r prefix (see raw strings in the Python topic), and it is the normal way to write regular expressions there. The difference is invisible for \d, which Python leaves alone, and fatal for \b, which Python turns into a backspace:

import re

line = "replaced cat 5 cable at meter KE-0042"
print(len("\b"), len(r"\b"))                # backspace vs backslash + b
print(re.search("\bcat\b", line))           # looks for backspaces
print(re.search(r"\bcat\b", line))          # word boundaries
print(re.search("\\bcat\\b", line))         # same as the raw string
1 2
None
<re.Match object; span=(9, 12), match='cat'>
<re.Match object; span=(9, 12), match='cat'>

"\d" without the r happens to work, because Python keeps unknown escape sequences as they are. Python 3.12 and later print a SyntaxWarning for it, so use raw strings for every pattern and you never have to think about which sequences Python knows.

JavaScript and PHP

JavaScript has a regex literal, /\d+/, which needs no string escaping at all: the backslash goes straight to the engine. Only a forward slash inside it must be escaped, /\/api\/v1/. When you build a pattern from a string with new RegExp(), the string rules apply first, so every backslash must be doubled:

console.log(new RegExp('\d+').source);   // '\d' in a string is just 'd'
console.log(new RegExp('\\d+').source);  // backslash kept
console.log(/\d+/.source);               // a literal: no string layer
console.log(new RegExp(String.raw`\d+`).source);
d+
\d+
\d+
\d+

PHP patterns are strings with delimiters around them, usually '/…/'. In single quotes, PHP keeps backslashes (except before ' and \), so patterns arrive intact. In double quotes, PHP first processes \n, \$, $variables and more. A pattern for a price shows the trap: in double quotes, \$ becomes a bare $, which the engine reads as "end of text".

<?php
$line = 'Late fee $5.00 waived';
var_dump(preg_match('/\$5\.00/', $line));  // single quotes: \$ reaches PCRE
var_dump(preg_match("/\$5\.00/", $line));  // double quotes: PCRE gets $5\.00
int(1)
int(0)

The shell is a layer too

On the command line, the shell reads your command before grep does. In double quotes, Bash still expands $name, $5 and backquotes; without quotes it also expands * and ? as wildcards. Single quotes pass everything through unchanged, which is why patterns in grep, sed and awk are almost always written in single quotes. printf shows what the program would receive:

printf 'pattern: %s\n' "fee $5.00"     # Bash replaces $5 (empty)
printf 'pattern: %s\n' 'fee $5.00'     # single quotes: unchanged
printf '%s\n' 'Late fee $5.00 waived' 'fee 5100 due' > fees.txt
grep 'fee \$5\.00' fees.txt
pattern: fee .00
pattern: fee $5.00
Late fee $5.00 waived