Literals, Metacharacters and Escaping
How a pattern is read: ordinary characters that match themselves, the metacharacters with special meaning, the dot, escaping with a backslash, and raw strings in code.
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.
\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