Text Processing for Network Engineers Part 1: Regex, the Pattern Language Everything Else Depends On
Every network engineer has typed grep something logfile a thousand times without ever learning what “something” actually is as a language. That’s fine, right up until the pattern needs to match an IPv6 address, or exclude a specific VLAN, or you copy a working grep pattern into sed and it silently stops working. This series is about the text tools a network engineer actually reaches for: grep, sed, awk, vim, nano, diff, and the smaller utilities around them. All of them speak some dialect of the same underlying language, so it’s worth learning that language once, properly, before touching any of the tools.
That language is regular expressions. This part is regex on its own, with networking examples instead of the usual colou?r and [0-9]{3}-[0-9]{4}.
The building blocks
A regex is a pattern that either matches a piece of text or doesn’t. The vocabulary is small:
| Token | Meaning |
|---|---|
. | any single character |
^ | start of line |
$ | end of line |
* | zero or more of the previous token |
+ | one or more of the previous token (ERE/PCRE) |
? | zero or one of the previous token (ERE/PCRE) |
{n,m} | between n and m of the previous token |
[...] | a character class, e.g. [0-9] or [a-fA-F] |
[^...] | negated class: anything except these |
(...) | a group, for alternation or backreference |
| | alternation: “this or that” (ERE/PCRE) |
\1, \2 | backreference to a captured group |
That’s genuinely most of it. The complexity in practice comes from combining these correctly, and from a detail almost nobody explains clearly: there isn’t one regex language, there are three, and they don’t all support the same syntax.
BRE, ERE, and PCRE: the dialect trap
- BRE (Basic Regular Expressions) is the default for
grepandsedwith no flags. In BRE,+,?,|, and()are literal characters unless you escape them:\+,\?,\|,\(...\). - ERE (Extended Regular Expressions) is what you get with
grep -Eoregrep, andsed -E(orsed -ron GNU sed). Here+,?,|, and()work unescaped, the way most people expect. - PCRE (Perl-Compatible Regular Expressions) is what you get with
grep -P. It adds lookaheads, lookbehinds, non-greedy quantifiers (*?), and\d/\w/\sshorthand classes.awkandseddon’t support-Pat all.
This is the single most common source of “I tested this pattern and it worked, then it didn’t” across grep, sed, and awk. A pattern written for grep -P with \d{1,3} will not work in plain sed. A pattern written in BRE with escaped parens will look wrong once you add -E. Decide your dialect before you write the pattern, not after it fails.
grep 'a\+' # BRE: literal backslash-plus means one-or-more
grep -E 'a+' # ERE: plus means one-or-more, no escaping
grep -P '\d+' # PCRE: \d is a digit class, ERE doesn't have this
sed 's/a\+/X/' # BRE, same escaping rule as grep
sed -E 's/a+/X/' # ERE
awk '/a+/' # awk's regex is ERE-like by default (gawk, at least)
IPv4: naive vs strict
The pattern everyone reaches for first is [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}. It works for finding IPv4-shaped strings in a log, and it will also happily match 999.999.999.999, because it has no concept of “each octet is 0-255.” For grepping a syslog file to find every source address mentioned, the naive pattern is genuinely fine, you don’t need octet validation to skim a log.
For anything that actually validates an address (an input field, a config generator, a pre-push sanity check), you want the octet-aware version:
^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$
That block repeated four times with dots between is the price of correctness: 250-255, 200-249, 100-199, and 0-99 (with no leading zero forced by the optional [1-9]? in front of the last digit). Nobody types this from memory. Know that it exists, keep a copy somewhere, and reach for the naive version everywhere else.
IPv6, MAC addresses, and CIDR
IPv6’s shorthand :: compression makes a fully general regex nasty (there are versions of this pattern that run past 500 characters to handle every legal compression). For log-grepping purposes, a looser pattern that matches “something that looks like an IPv6 address” is almost always what you want:
grep -E '([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}'
MAC addresses are much friendlier, because the format is rigid:
grep -E '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' # colon-separated
grep -E '([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}' # Cisco dot-separated
Note the two conventions: colon-separated pairs (aa:bb:cc:dd:ee:ff, common on Linux and most vendors) versus Cisco’s dot-separated quads (aabb.ccdd.eeff). Grep the wrong pattern against the wrong vendor’s show mac address-table output and you’ll get zero matches and no error, which is its own lesson in regex debugging: no match is not an error, so a silently-wrong pattern looks identical to “there’s nothing here.”
CIDR is just an IPv4 (or IPv6) pattern with /\d{1,2} tacked on:
grep -E '[0-9]{1,3}(\.[0-9]{1,3}){3}/([0-9]|[12][0-9]|3[0-2])'
The prefix-length group ([0-9]|[12][0-9]|3[0-2]) is doing the same job as the octet group above: it restricts the match to 0-32 instead of accepting /99.
Interface names and VLAN tags
This is where regex earns its keep for a network engineer specifically, because interface naming has no single format:
GigabitEthernet0/1
Gi0/1
TenGigE0/0/0/1
eth0
ens192
Port-channel10
Vlan100
A pattern that matches “any Cisco-style GigabitEthernet interface, long or short form” needs alternation:
grep -E '(GigabitEthernet|Gi)[0-9]+/[0-9]+'
And a VLAN tag inside an switchport trunk allowed vlan line, or a .1Q frame dump, usually just needs a bounded numeric range check the same way IPv4 octets did, since VLAN IDs run 1-4094:
grep -E '\b(40[0-9]{2}|[1-3][0-9]{3}|[1-9][0-9]{0,2})\b'
\b here is a word boundary, PCRE and GNU-extended-BRE both support it (plain POSIX BRE/ERE technically doesn’t, though GNU grep accepts it anyway). It stops the pattern from matching the 100 inside 4100 when you only meant a bare 100.
Anchors are the difference between “contains” and “is”
^ and $ matter more in network config text than almost anywhere else, because config lines are often indented and structured. Grepping a Cisco or Fortinet config for a bare 10 will match 10, interface Gi0/10, 192.168.1.10, and MTU 1500 (no, that last one wouldn’t match, but you get the idea, everything with a 10 in it lights up). Anchoring to line boundaries, or wrapping in word boundaries, is the fix:
grep '^ *interface' # lines that, after leading whitespace, start with "interface"
grep -E '\bVlan10\b' # exactly Vlan10, not Vlan100 or Vlan1
A worked example: pulling every /30 point-to-point link from a config
Put it together. Given a Cisco-style running-config with a mix of subnet sizes, find every interface configured with a /30 (a classic point-to-point WAN link mask):
grep -B1 -E '255\.255\.255\.252' running-config.txt
255.255.255.252 is the only way a /30 subnet mask is written in dotted-decimal in an IOS config, so this doesn’t even need the full IPv4 pattern, just the one string that means “/30.” -B1 pulls the line before the match too, which is almost always the ip address line’s own interface header once you account for how show running-config formats blocks. This is the pattern that matters more than the fancy validated-IPv4 one: regex is at its best when you exploit the fact that config output has a fixed, boring, predictable format. You rarely need the general-purpose pattern. You need the specific pattern for the specific format sitting in front of you.
That’s the whole toolkit: anchors, classes, quantifiers, alternation, groups, and the BRE/ERE/PCRE dialect awareness to know which syntax a given tool expects. Part 2 puts this to work in grep itself, searching configs and logs at scale rather than one file at a time.