Sign in
← Linux: tools
12 min · medium · Lesson 3 of 4

Regular expressions: grep -E and sed

Basic regex syntax: anchors, classes, quantifiers, groups. Searching with grep -E and replacing with sed.

Why regex

In the lesson on pipes, we filtered logs through grep by a plain string. But you often need to find not an exact word but a pattern: all IP addresses, all lines with a 5xx response code, all emails. That's what regular expressions (regex) are for. A regular expression describes a text pattern. The syntax is almost identical in grep, sed, awk, and most programming languages, with differences in the details.

The pattern in the commands below is always in single quotes. That way the shell doesn't touch $, \, and * inside it, and passes the pattern to the command as-is.

Anchors and character classes

The basic building blocks:

grep -E '^ERROR' app.log        # lines that start with ERROR
grep -E '[0-9]{3}$' app.log     # lines that end with three digits

The short notation \d for a digit doesn't exist in grep -E, it's PCRE syntax (Perl, Python, JavaScript). There won't be an error though. GNU grep 3.11 silently reads \d as the letter d:

printf 'code 503\nid 7d\nabc\n' | grep -E '\d'
code 503
id 7d

Lines with the letter d were found, not with a digit. PCRE in GNU grep is turned on with the -P flag, but macOS doesn't have it. It's more reliable to write [0-9].

ERE and BRE: why the -E flag

grep and sed have two dialects. Without the flag, the basic one (BRE) works; with -E, the extended one (ERE). In ERE, the characters +, ?, |, (), and {} work as special characters. In BRE, the same characters without \ mean themselves:

echo aaa | grep 'a+'        # looks for "a" followed by the character "+": won't match
echo aaa | grep 'a\+'       # GNU BRE: one or more "a", matches
echo aaa | grep -E 'a+'     # ERE: the same thing without backslashes

That's why grep -E and sed -E are used by default: the pattern is shorter and reads the same way in both commands.

Quantifiers

Quantifiers say how many times the preceding element repeats:

grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' access.log

This is a rough IP address pattern: four groups of 1-3 digits separated by dots. The dot is escaped as \., because without escaping, . means "any character". Without \, the pattern would also match the string 10x0y0z1.

The pattern really is rough. It will let 999.999.999.999 through and will cut 1.2.3.4 out of the version number 1.2.3.4.5:

echo '10.0.0.1 999.999.999.999 1.2.3.4.5' | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
10.0.0.1
999.999.999.999
1.2.3.4

The -o flag prints not the whole line but just the matched piece, one per output line. For logs, this level of precision is usually enough. Checking that an address is actually valid is better done with something other than a regex.

Groups and alternation

() groups, | means "or":

grep -E '(GET|POST) /api' access.log    # lines with GET or POST to /api

The group (GET|POST) matches either option. Without the parentheses, GET|POST /api reads as "GET or POST /api" and would match any GET, even to /home. Groups are also used in replacement, to refer back to what was matched.

A couple of grep flags that often go along with a pattern: -i case-insensitive, -v lines that don't match the pattern, -c the count of matching lines.

Replacing with sed

grep finds, sed replaces by pattern. The syntax is s/pattern/replacement/:

echo 'user=alice' | sed -E 's/user=(.+)/login: \1/'
login: alice

The group (.+) captures the match, and \1 substitutes it into the replacement. There can be several groups; they're numbered left to right by their opening parenthesis:

echo '2026-10-09' | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3.\2.\1/'
09.10.2026

Without -E, parentheses in sed mean themselves, and \1 has nothing to refer to. GNU sed will say exactly that: invalid reference \1 on 's' command's RHS.

The g flag at the end (s/.../.../g) replaces all occurrences in the line, not just the first one: echo a-b-c | sed 's/-/+/' gives a+b-c, with g you get a+b+c. sed -i can edit a file in place; it was covered in the lesson on sed.

Greediness

* and + grab as much text as they can. If the line has two quoted values, "(.*)" will capture everything from the first quote to the last:

echo 'key="a" other="b"' | sed -E 's/"(.*)"/<\1>/'
key=<a" other="b>

Instead of "any character" you need "any character except a quote":

echo 'key="a" other="b"' | sed -E 's/"([^"]*)"/<\1>/g'
key=<a> other=<b>

Common mistakes

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson linux-tools-03-text-regex