# Regular expressions

> Write patterns that match what you mean across grep, PCRE, Go, Python and JavaScript, avoid catastrophic backtracking and test them from the shell.

Canonical: https://www.wiki.jodisand.me/regex/
Reviewed: 2026-09-24
Related: [Bash](https://www.wiki.jodisand.me/bash/index.md), [jq](https://www.wiki.jodisand.me/jq/index.md), [Python](https://www.wiki.jodisand.me/python/index.md), [Go](https://www.wiki.jodisand.me/go/index.md), [TypeScript](https://www.wiki.jodisand.me/typescript/index.md)


## Cheatsheet

| Task | Pattern or command |
| --- | --- |
| Start and end of line | `^...$` |
| Any character except newline | `.` |
| One of, none of | `[abc]`, `[^abc]` |
| Digit, word char, whitespace | `\d`, `\w`, `\s` (not in POSIX BRE/ERE; use `[0-9]`, `[[:alnum:]_]`, `[[:space:]]`) |
| Zero or more, one or more, optional | `*`, `+`, `?` |
| Exactly n, n to m, at least n | `{n}`, `{n,m}`, `{n,}` |
| Shortest match | `.*?` (PCRE, RE2, Python, JS; not POSIX) |
| Capture, non-capturing group | `(...)`, `(?:...)` |
| Named group | `(?P<name>...)` Python and Go; `(?<name>...)` PCRE, JS, Python 3.x, Go 1.22+ |
| Alternation | `cat\|dog` |
| Word boundary | `\b` |
| Literal dot, bracket, backslash | `\.`, `\[`, `\\` |
| Case-insensitive inline | `(?i)` at the start |
| Lookahead, lookbehind | `(?=...)`, `(?<=...)` (PCRE, Python, JS; not RE2) |
| Backreference | `\1` (PCRE, Python; JS); `$1` in JS replacement strings |
| Extended regex in grep | `grep -E 'a\|b'` |
| PCRE in grep | `grep -P '\d+(?=ms)'` |
| Only the matched part | `grep -oE '[0-9]+'` |
| Regex in Bash | `[[ $s =~ ^[0-9]+$ ]]` then `${BASH_REMATCH[1]}` |
| Regex in sed | `sed -E 's/(a+)b/\1/'` |
| Explain a match in Python | `python3 -c 'import re; print(re.search(r"...", s).groupdict())'` |

Syntax below follows PCRE2 10.4x, Go 1.26 `regexp`, Python 3.12+ `re`, ECMAScript 2024 and GNU grep 3.12 unless stated. References: [PCRE2 pattern syntax](https://www.pcre.org/current/doc/html/pcre2pattern.html), [RE2 syntax](https://github.com/google/re2/wiki/Syntax), [Python `re`](https://docs.python.org/3/library/re.html).

## Flavours

The same text means different things to different engines. Know which one is reading your pattern before writing it.

| Flavour | Where | Lookaround | Backreferences | Lazy quantifiers | `\d \w \s` | Guaranteed linear time |
| --- | --- | --- | --- | --- | --- | --- |
| POSIX BRE | `grep`, `sed`, `ed`, `expr` | No | `\1` | No | GNU extension only | Yes (GNU implementation) |
| POSIX ERE | `grep -E`, `sed -E`, `awk`, Bash `=~` | No | Undefined in POSIX; GNU allows `\1` | No | GNU extension only | Yes (GNU); `awk` no backrefs |
| PCRE2 | `grep -P`, `ripgrep -P`, PHP, Nginx, HAProxy | Yes | Yes | Yes | Yes | No |
| RE2 | Go `regexp`, `ripgrep` default (Rust `regex`), Envoy, Prometheus, Google products | No | No | Yes | Yes | Yes |
| Python `re` | Python | Yes | Yes | Yes | Yes | No |
| ECMAScript | JavaScript, TypeScript, Deno | Yes | Yes | Yes | Yes | No |

BRE treats `+`, `?`, `|`, `{`, `(` as literals and needs a backslash to give them their special meaning: `\(a\|b\)\{2,\}`. ERE flips that. Both lack `\d`, non-capturing groups, lazy quantifiers and lookaround, so a pattern with `(?:` fails outright under `grep -E` and `sed -E`. GNU grep, sed and awk accept `\w`, `\s`, `\b`, `\<` and `\>` as extensions; portable POSIX code uses `[[:alnum:]_]`, `[[:space:]]` and word-boundary tricks.

RE2 (Go, Rust `regex`, ripgrep by default) refuses backreferences and lookaround at compile time in exchange for a guarantee that matching is linear in input length. That is the right trade for anything that matches untrusted input, such as log parsers, ingress rules and Prometheus relabelling.

```sh
grep -E '(?:a|b)'  x        # grep: warning: ? at start of expression; then matches nothing useful
grep -P '(?:a|b)'  x        # fine
go run - <<'EOF'
package main
import "regexp"
func main() { regexp.MustCompile(`(?=x)`) }   // panics: invalid or unsupported Perl syntax: `(?=`
EOF
```

Python `re` since 3.11 supports atomic groups `(?>...)` and possessive quantifiers `a*+`; earlier versions raise `re.error`. Go 1.22 accepts the `(?<name>...)` group syntax alongside `(?P<name>...)`. JavaScript added named groups and lookbehind in ES2018, the `s` flag in ES2018, `d` (match indices) in ES2022 and the `v` flag (set operations, string properties) in ES2024.

## Anchors and boundaries

`^` and `$` match at the start and end of the subject, and additionally at line breaks only in multiline mode. `\A` and `\z` (`\Z` in Python, which also matches before a final newline in Perl and PCRE) mean the real start and end regardless of mode. Go and RE2 accept `\A` and `\z`; JavaScript has neither and relies on the absence of the `m` flag.

`\b` matches between a `\w` character and a non-`\w` character or edge of string. It is ASCII-only in Go and in JavaScript without the `u` flag, and Unicode-aware in Python 3 `str` patterns and PCRE2 with `(*UCP)`. `\B` is its negation. GNU tools add `\<` and `\>` for start and end of word.

`$` in PCRE and Python matches before a trailing newline as well as at the very end, so `^abc$` matches `"abc\n"`. Use `\z` when the trailing newline must be rejected.

```sh
printf 'abc\n' | grep -cP '^abc$'      # 1: grep strips the newline before matching
python3 -c 'import re; print(bool(re.search(r"^abc$", "abc\n")))'    # True
python3 -c 'import re; print(bool(re.search(r"^abc\Z", "abc\n")))'   # False
```

## Character classes

| Class | Meaning |
| --- | --- |
| `[a-z]`, `[^0-9]` | Range; negated range. `-` is literal first or last: `[-a-z]` |
| `[]abc]` | Literal `]` must come first in POSIX and RE2; escape it as `\]` in PCRE, Python and JS |
| `[[:alpha:]]`, `[[:digit:]]`, `[[:space:]]`, `[[:punct:]]` | POSIX classes; valid inside brackets only, in grep, sed, awk, PCRE, Go, Rust. Not in JS or Python |
| `\d`, `\w`, `\s` | ASCII in Go and JS; Unicode in Python `str` patterns (`re.ASCII` restricts) and PCRE2 with `(*UCP)` |
| `\D`, `\W`, `\S` | Negations |
| `\p{L}`, `\p{Lu}`, `\p{Greek}` | Unicode property; `\P{...}` negates. Go, PCRE, Python 3rd-party `regex`, JS with `u` |
| `\h`, `\v`, `\R` | PCRE2 horizontal space, vertical space, any line break |
| `.` | Anything except `\n`; with `s` (dotall) also `\n`. In Go, `(?s)`. Never matches `\r\n` as one unit |

Inside a bracket expression most metacharacters lose their meaning: `[.*+?]` is four literal characters. The exceptions are `]`, `\`, `^` at the start and `-` between two characters. In POSIX bracket expressions the backslash is a literal, so `[\d]` matches a backslash or a `d` under `grep -E`, and `[:` opens a class name, which is why `[^[:]` is an error and `[^ :[]` is not.

## Quantifiers and greediness

Quantifiers apply to the element immediately before them. Greedy ones take as much as they can and back off one character at a time until the rest of the pattern matches.

```text
Subject:  <a href="x">link</a><p>text</p>
<.*>      matches the whole line: .* runs to the end, then backs off to the last >
<.*?>     matches <a href="x">, then </a>, then <p>, then </p>: lazy, shortest first
<[^>]*>   same result, no backtracking: preferred
```

| Form | Greedy | Lazy | Possessive (PCRE2, Python 3.11+, Java) |
| --- | --- | --- | --- |
| Zero or more | `*` | `*?` | `*+` |
| One or more | `+` | `+?` | `++` |
| Optional | `?` | `??` | `?+` |
| Range | `{2,5}` | `{2,5}?` | `{2,5}+` |

Lazy does not mean "shortest overall match"; the engine still starts at the leftmost position and takes the first match it finds there. `a.*?c` on `abcabc` gives `abc`, not the shorter middle `c`. A negated class such as `[^>]*` is almost always clearer and faster than `.*?`, and it is the only option in POSIX tools, which have no lazy quantifiers at all.

Possessive quantifiers and atomic groups `(?>...)` never give back what they consumed. `\d++x` fails immediately on `123` without trying `12x`, `1x`. They exist to prevent backtracking blow-ups, not to change what matches in the ordinary case.

In Go `(?U)` swaps the meaning of greedy and lazy for the whole pattern. RE2 chooses the leftmost match and, among those, the one a backtracking engine would return (leftmost-first), except that POSIX mode via `regexp.CompilePOSIX` returns the leftmost-longest.

## Groups and backreferences

Parentheses group and capture. Captures are numbered by the position of the opening bracket, left to right, starting at 1. Group 0 is the whole match. Use `(?:...)` when the group exists only for grouping, so capture numbers stay stable and matching is cheaper.

```python
import re
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "released 2026-09-24")
m.group(0)         # '2026-09-24'
m.group("year")    # '2026'
m.groupdict()      # {'year': '2026', 'month': '09', 'day': '24'}
m.span("month")    # (14, 16)
re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})", r"\g<m>/\g<y>", "2026-09")   # '09/2026'
```

```go
re := regexp.MustCompile(`(?P<user>[^@]+)@(?P<host>.+)`)
m := re.FindStringSubmatch("ops@example.com")            // []string{"ops@example.com", "ops", "example.com"}
m[re.SubexpIndex("host")]                                // "example.com"
re.ReplaceAllString("ops@example.com", "${host}!${user}")  // ${name} or $1; $1x means group named "1x", so write ${1}x
```

A repeated group keeps only its last iteration: `(\d,)+` on `1,2,3,` captures `3,`. Capture the whole run with `((?:\d,)+)` and split afterwards.

A backreference `\1` matches the same text the group matched, not the same pattern. `(["'])(.*?)\1` matches a quoted string with either quote and rejects `"abc'`. Backreferences make matching NP-hard in the general case, which is why RE2 omits them. In replacement strings the reference syntax differs: `\1` in sed and Python, `$1` in JavaScript and Go, `${1}` when a digit follows.

```sh
sed -E 's/([a-z]+)=([^ ]+)/\2=\1/'          # swap key and value
sed -E 's/(.)\1/<\1\1>/g'                   # mark doubled characters; GNU sed accepts \1 in ERE
grep -E '\b([a-z]+) \1\b' file              # doubled words such as "the the"; GNU extension
```

## Lookaround

Lookaround asserts without consuming. `(?=...)` positive lookahead, `(?!...)` negative lookahead, `(?<=...)` positive lookbehind, `(?<!...)` negative lookbehind.

```sh
grep -oP '\d+(?= ms)' timings.log            # numbers followed by " ms", without the unit
grep -oP '(?<=user=)\S+' auth.log            # value after user=, without the key
grep -P '^(?!#)' config                      # lines not starting with #; same as grep -v '^#'
grep -P '^(?=.*[A-Z])(?=.*\d).{12,}$' pw     # at least one upper, one digit, 12+ chars: three assertions on the same span
```

PCRE2 and Python require lookbehind to have a bounded length, so `(?<=a+)` is an error while `(?<=a{1,5})` and `(?<=ab|cde)` are fine. JavaScript since ES2018 allows unbounded lookbehind. Go, Rust `regex` and ripgrep's default engine reject all lookaround; ripgrep switches to PCRE2 with `-P`.

`\K` (PCRE2) resets the reported start of the match, a cheaper alternative to lookbehind for stripping a prefix: `grep -oP 'user=\K\S+'`.

## Flags and modes

| Flag | PCRE / `grep -P` | Go | Python | JavaScript | Effect |
| --- | --- | --- | --- | --- | --- |
| Case-insensitive | `(?i)` | `(?i)` | `re.I` or `(?i)` | `i` | ASCII in Go; Unicode simple folding elsewhere |
| Multiline | `(?m)` | `(?m)` | `re.M` | `m` | `^` and `$` at line boundaries |
| Dot matches newline | `(?s)` | `(?s)` | `re.S` | `s` | `.` also matches `\n` |
| Extended (ignore whitespace, allow `#` comments) | `(?x)` | not supported | `re.X` | not supported | Layout for long patterns |
| Unicode | `(*UCP)` | always | default for `str`; `re.A` for ASCII | `u` or `v` | Meaning of `\w`, `\b`, `\d` and case folding |
| Ungreedy | `(?U)` | `(?U)` | none | none | Swap greedy and lazy |
| Global | n/a | `All` functions | `findall`, `finditer`, `sub` | `g` | Match more than once |
| Sticky | `\G` | none | `match` at `pos` | `y` | Match only at the current position |

Inline flags at the start of the pattern are the portable form: `(?i)error` works in every flavour above except JavaScript, which requires the `i` flag on the literal. A global flag in the middle of a pattern (`a(?i)b`) works in PCRE and Go; Python 3.11+ rejects it with `global flags not at the start of the expression`. Scoped flags `(?i:b)` work in PCRE, Go, Python 3.6+ and JavaScript from ES2025.

```sh
grep -iE 'warn|error' app.log                     # -i is the flag; the pattern stays plain
python3 -c 'import re; print(re.findall(r"(?im)^error: (.+)$", open("app.log").read()))'
```

## Unicode

A pattern either sees bytes or code points, and the engine decides which. Go patterns match UTF-8 code points: `.` is one rune, `\w` is ASCII only, `\pL` is any letter. Python 3 `str` patterns are Unicode-aware, so `\d` matches Arabic-Indic digits and `[a-z]` with `re.I` matches `K` (Kelvin sign) and `ſ`. Python `bytes` patterns are ASCII. JavaScript without `u` operates on UTF-16 code units, so `.` matches half of an emoji and `[😀-😂]` is a range error; use `u` or `v`. PCRE2 needs `(*UTF)` or the caller's `PCRE2_UTF` flag, and `(*UCP)` for Unicode-aware `\w` and `\b`; `grep -P` enables both in a UTF-8 locale.

```sh
echo 'naïve café' | grep -oP '\w+'                  # naïve, café in a UTF-8 locale
echo 'naïve café' | LC_ALL=C grep -oP '\w+'         # na, ve, caf: bytes, ASCII \w
echo 'Straße' | grep -iP 'STRASSE'                  # no match: ß does not case-fold to SS in PCRE2
python3 -c 'import re; print(re.findall(r"\p{L}+", "x"))'   # error: Python re has no \p; use the regex package or explicit ranges
```

Normalise input before matching when accented characters can arrive as either a precomposed code point or a base letter plus combining mark; `unicodedata.normalize("NFC", s)` in Python. `[[:alpha:]]` and `\p{L}` match letters in any script; `[a-zA-Z]` matches 52 ASCII letters and nothing else, which is usually a bug in anything user-facing and the right choice for machine identifiers.

## Catastrophic backtracking

A backtracking engine tries every way a pattern could match before it gives up. Nested quantifiers where the inner and outer can split the same text in many ways make the number of attempts exponential in input length, so a failing match can take seconds, minutes or longer.

```text
(a+)+$      on  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaab"   # 2^30 ways to split the a's before every one fails
(\w+\s?)*$  on  a long line with a trailing "!"     # classic: word followed by optional space, repeated
(.*,)*x     on  "1,2,3,4,5,6,7,8,9,10,11,12,13,"   # .* and , overlap
```

Recognise the shape: a group containing a quantifier, itself quantified, where the inner element can match text the outer repetition could also have split differently, and something after it that can fail. Fix by making the pieces unambiguous so there is only one way to match:

```text
(a+)+$        →  a+$
(\w+\s?)*$    →  (\w+\s)*\w*$        # each iteration must consume a separator, or use possessive (\w++\s?+)*+
(.*,)*x       →  ([^,]*,)*x          # negated class cannot overlap the delimiter
"(.*?)"       →  "([^"]*)"           # lazy still backtracks on failure; negated class does not
```

Measure rather than guess. Python raises no timeout, so a hostile input stalls the thread; the same holds for PCRE unless the caller sets a match limit (`pcre2_set_match_limit`, or `pcre.backtrack_limit` in PHP). Go and Rust cannot blow up, at the cost of the missing features. If a service matches user-supplied input with PCRE or Python, either move the pattern to RE2 syntax or run it under a deadline in a separate worker.

```sh
# Time a pattern under Python; 26 a's take about six seconds and every extra "a" doubles it
time python3 -c 'import re; re.match(r"(a+)+$", "a"*26 + "b")'
# The same pattern under RE2 semantics returns instantly
time grep -E '(a+)+$' <<< "$(printf 'a%.0s' {1..28})b"
```

## Tested patterns

Each pattern below matched its intended inputs and rejected the counter-examples under `grep -E`, `grep -P`, Python `re`, Go `regexp` and JavaScript. They use capturing groups, not `(?:`, so that ERE tools accept them; convert to non-capturing where the flavour allows if capture numbers matter.

| Need | Pattern | Notes |
| --- | --- | --- |
| IPv4 address | `^((25[0-5]\|2[0-4][0-9]\|1[0-9]{2}\|[1-9]?[0-9])\.){3}(25[0-5]\|2[0-4][0-9]\|1[0-9]{2}\|[1-9]?[0-9])$` | Rejects `256.1.1.1`, leading zeros and five octets |
| IPv4 CIDR | `^((25[0-5]\|2[0-4][0-9]\|1[0-9]{2}\|[1-9]?[0-9])\.){3}(25[0-5]\|2[0-4][0-9]\|1[0-9]{2}\|[1-9]?[0-9])/(3[0-2]\|[12]?[0-9])$` | Prefix 0 to 32; does not check host bits are zero |
| Email, practical | `^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$` | Accepts what real mail systems use; RFC 5322 allows more and nobody wants it. Verify by sending mail, not regex |
| Hostname (RFC 1123 labels) | `^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$` | Lowercase; add `(?i)` or `-i`. Total 253-char limit needs `(?=.{1,253}$)`, which excludes ERE and RE2 |
| ISO 8601 date | `^[0-9]{4}-(0[1-9]\|1[0-2])-(0[1-9]\|[12][0-9]\|3[01])$` | Structural only; accepts `2026-02-31`. Parse with a date library for calendar validity |
| ISO 8601 date-time | `^[0-9]{4}-(0[1-9]\|1[0-2])-(0[1-9]\|[12][0-9]\|3[01])T([01][0-9]\|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z\|[+-]([01][0-9]\|2[0-3]):[0-5][0-9])$` | Requires `T` and a zone designator, matching RFC 3339 |
| Semantic version | `^(0\|[1-9][0-9]*)\.(0\|[1-9][0-9]*)\.(0\|[1-9][0-9]*)(-((0\|[1-9][0-9]*\|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0\|[1-9][0-9]*\|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$` | The semver.org grammar; rejects `01.0.0` and `v1.0.0`. Strip a leading `v` first |
| UUID (RFC 9562) | `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$` | Versions 1 to 8 and RFC variant; `[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}` for any 8-4-4-4-12 shape |
| MAC address | `^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$` | Colon form; replace `:` with `[:-]` to accept dashes |
| SHA-256 hex digest | `^[0-9a-f]{64}$` | Lowercase as produced by `sha256sum` |
| Hex colour | `^#([0-9a-fA-F]{3}){1,2}$` | `#fff` or `#a1b2c3` |
| Kubernetes name (DNS label) | `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` | Add `{0,63}` semantics by checking length separately |
| Log level token | `\b(TRACE\|DEBUG\|INFO\|WARN(ING)?\|ERROR\|FATAL)\b` | Word boundaries stop `errors` matching |
| syslog line (RFC 3164) | `^([A-Z][a-z]{2} [ 0-9][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}) (\S+) ([^ :[]+)(\[([0-9]+)\])?: (.*)$` | Groups: timestamp, host, program, pid, message. `[^ :[]` is ordered so `[:` does not open a POSIX class |
| Nginx/Apache combined log | `^(\S+) \S+ \S+ \[([^]]+)\] "([A-Z]+) ([^ "]+) HTTP/[0-9.]+" ([0-9]{3}) ([0-9]+\|-)` | Groups: client, time, method, path, status, bytes. JavaScript needs `[^\]]` instead of `[^]]` |

Every pattern above anchored with `^...$` validates a whole field. Drop the anchors, or use `\b`, to find the same thing inside a line.

## Testing from the shell

```sh
grep -E  'pattern' file          # ERE; -o prints only matches, -n line numbers, -c count, -v invert
grep -P  'pattern' file          # PCRE2; -o with \K or lookaround extracts values
grep -oE 'pattern' file | sort | uniq -c | sort -rn   # what a pattern actually pulls out of real data
rg -o 'pattern' file             # ripgrep: RE2-style engine, Unicode by default; -P switches to PCRE2
sed -nE 's/^host=(.*)$/\1/p' file               # print only rewritten lines
awk '/^ERROR/ && $3 ~ /timeout/' file           # per-field regex
[[ $line =~ ^([a-z]+)=(.*)$ ]] && echo "${BASH_REMATCH[1]} -> ${BASH_REMATCH[2]}"   # Bash: ERE, unquoted pattern
```

Bash `=~` uses the system ERE, so `\d` and `(?:` fail. Put the pattern in a variable to keep it readable and avoid quoting fights: `re='^([0-9]+)\.([0-9]+)$'; [[ $v =~ $re ]]`.

```sh
# Python: show every group of every match
python3 - <<'EOF'
import re, sys
pat = re.compile(r'(?P<ts>\S+) (?P<level>ERROR|WARN) (?P<msg>.*)')
for line in sys.stdin:
    if m := pat.search(line):
        print(m.groupdict())
EOF

# Python: verbose mode for a documented pattern
python3 -c '
import re
pat = re.compile(r"""
    ^(?P<major>0|[1-9]\d*)     # no leading zeros
    \.(?P<minor>0|[1-9]\d*)
    \.(?P<patch>0|[1-9]\d*)$
""", re.X)
print(pat.match("1.2.3").groupdict())'

# Go: check a pattern compiles under RE2 before shipping it in a config
go run - <<'EOF'
package main
import ("fmt"; "os"; "regexp")
func main() {
    if _, err := regexp.Compile(os.Args[1]); err != nil { fmt.Println(err); os.Exit(1) }
}
EOF

# Node: named groups and match indices
node -e 'console.log("2026-09-24".match(/(?<y>\d{4})-(?<m>\d{2})/d).groups, "\n", "a1b2".matchAll(/\d/g))'
```

`pcre2test` (package `pcre2-tools` on Fedora) reads a pattern and subjects from stdin and prints what matched, including partial matches and backtrack counts, which is the fastest way to see why PCRE disagrees with your expectation.

## Oneliners

```sh
# Extract every IPv4 address in a file, deduplicated
grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' file | sort -u

# Status codes from an access log, counted
grep -oE '" [0-9]{3} ' access.log | sort | uniq -c | sort -rn

# Requests slower than one second when the last field is milliseconds
awk '$NF ~ /^[0-9]+$/ && $NF > 1000' access.log

# Lines that are not comments or blank
grep -Ev '^\s*(#|$)' config.ini

# Value of a key in a key=value file, stripping quotes
sed -nE 's/^listen_port\s*=\s*"?([^"]*)"?\s*$/\1/p' app.conf

# Rename *.jpeg to *.jpg (GNU sed for the transform; prints the mv commands, pipe to sh to run them)
for f in *.jpeg; do printf 'mv -- %q %q\n' "$f" "${f%.jpeg}.jpg"; done

# Replace in place across a tree, only in files that contain the pattern
grep -rlE 'old\.example\.com' src | xargs -r sed -i -E 's/old\.example\.com/new.example.com/g'

# Same with ripgrep, previewing first
rg -l 'old\.example\.com' src; rg 'old\.example\.com' -r 'new.example.com' src   # -r prints replacements; it does not edit

# Pull the version out of a tool's --version output
kubectl version --client 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1

# Validate that a variable is a semver before using it
[[ $ver =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || { echo "bad version: $ver" >&2; exit 2; }

# Split a line on a regex delimiter in Python
python3 -c 'import re,sys; print(re.split(r"[,;]\s*", sys.stdin.read().strip()))' <<< 'a, b;c'

# Find files whose names match a regex (find uses Emacs syntax by default; -regextype changes it)
find . -regextype posix-extended -regex '.*/[0-9]{8}-.*\.log' -print

# Match across lines with grep -z (NUL-separated records, so the whole file is one record)
grep -Pzo '(?s)BEGIN.*?END' file | tr '\0' '\n'

# Case-insensitive match on a JSON field with jq
jq -r 'select(.msg | test("timeout"; "i")) | .ts' app.jsonl

# Email-like tokens from a mail spool, lowercased
grep -oiE '[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}' mbox | tr 'A-Z' 'a-z' | sort -u

# Words repeated twice in a row, with line numbers
grep -nE '\b([a-z]+) \1\b' README.md

# Strip ANSI colour codes from captured output
sed -E 's/\x1b\[[0-9;]*[A-Za-z]//g' run.log

# Escape a literal string for use inside a PCRE
python3 -c 'import re,sys; print(re.escape(sys.argv[1]))' 'price is $5 (approx)'

# Lines longer than 120 characters
grep -nE '^.{121,}' file
```

## Scripts

Validate every value in a CSV column against a pattern and report the offending rows.

```sh
#!/usr/bin/env bash
# usage: check-column.sh <file.csv> <column-number> <ERE>
set -euo pipefail
file=${1:?file} col=${2:?column} re=${3:?regex}
awk -F, -v col="$col" -v re="$re" '
  NR == 1 { next }                       # skip the header
  $col !~ re { bad++; printf "line %d: %s\n", NR, $col }
  END { printf "%d bad of %d rows\n", bad, NR - 1 > "/dev/stderr"; exit bad > 0 }
' "$file"
```

Compile a set of patterns under Go's RE2 syntax so a config file rejected by Prometheus, Envoy or a Go service is caught in CI rather than at reload.

```go
// go run check_re2.go patterns.txt
package main

import (
	"bufio"
	"fmt"
	"os"
	"regexp"
)

func main() {
	f, err := os.Open(os.Args[1])
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(2)
	}
	defer f.Close()
	bad := 0
	sc := bufio.NewScanner(f)
	for n := 1; sc.Scan(); n++ {
		if _, err := regexp.Compile(sc.Text()); err != nil {
			bad++
			fmt.Printf("line %d: %v\n", n, err)
		}
	}
	if bad > 0 {
		os.Exit(1)
	}
}
```

Benchmark a pattern against a corpus to catch backtracking before it reaches production. Prints the slowest lines.

```python
#!/usr/bin/env python3
"""usage: regex-bench.py PATTERN FILE   -- times each line; lists the ten slowest."""
import re
import sys
import time

pat = re.compile(sys.argv[1])
timings = []
with open(sys.argv[2], encoding="utf-8", errors="replace") as fh:
    for n, line in enumerate(fh, 1):
        t0 = time.perf_counter()
        pat.search(line)
        timings.append((time.perf_counter() - t0, n, line.rstrip()[:80]))
timings.sort(reverse=True)
total = sum(t for t, _, _ in timings)
print(f"{len(timings)} lines, {total*1000:.1f} ms total, {timings[0][0]*1e6:.0f} us worst")
for t, n, line in timings[:10]:
    print(f"{t*1e6:8.0f} us  line {n}: {line}")
```

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `grep: warning: ? at start of expression`, or `Unmatched ( or \(`, or silently no match | PCRE syntax (`(?:`, `\d`, lookaround) given to `grep -E` or `sed -E` | Use `grep -P`, or rewrite with capturing groups and `[0-9]` |
| `(a\|b)` matches literally under `grep` | BRE: `(`, `\|`, `+`, `?` are literals | `grep -E`, or escape: `\(a\|b\)` |
| Go: `invalid or unsupported Perl syntax` | Backreference or lookaround under RE2 | Rewrite without them, or validate in a second step in code |
| Python: `re.error: look-behind requires fixed-width pattern` | Variable-length lookbehind | Bound it (`{1,10}`), use alternation of fixed widths, or match the prefix and take a group |
| Match hangs or CPU pins at 100% | Nested quantifiers backtracking | Rewrite with negated classes or possessive quantifiers; test with the benchmark script; prefer RE2 for untrusted input |
| `.` does not match across lines | Dot excludes newline by default | `(?s)` or `re.S`; in grep use `-z` so the input is one record |
| `^` matches only the first line of a multi-line string | Multiline mode off | `(?m)` or `re.M`; `grep` is already per line |
| `\b` behaves oddly around accented letters | ASCII word definition | Python `str` patterns are Unicode by default; PCRE needs `(*UCP)`; Go has ASCII `\b` only |
| `[[:digit:]]` fails in Python or JavaScript | POSIX classes unsupported there | `\d` or `[0-9]` |
| Bash `[[ $s =~ "$re" ]]` never matches | Quoting the right side makes it a literal string | Store the pattern in a variable and expand it unquoted: `[[ $s =~ $re ]]` |
| `sed` prints `\1` literally | Backreference in a BRE replacement with an unescaped group | `sed -E 's/(x)/\1/'` or `sed 's/\(x\)/\1/'` |
| Capture group returns only the last item | Quantified group keeps the final iteration | Capture the whole repetition, then split |
| `$` accepts a value with a trailing newline | `$` matches before a final `\n` in PCRE and Python | Use `\z` (PCRE, Go) or `\Z` (Python) |
| JavaScript: `Invalid regular expression: Lone quantifier brackets` | `[^]]` under the `u` or `v` flag | Escape it: `[^\]]` |
| Case-insensitive match misses `ß`, `İ`, `K` | Simple case folding only | Normalise input first, or compare with a locale-aware collation in code |

## Further reading

- [PCRE2 pattern syntax](https://www.pcre.org/current/doc/html/pcre2pattern.html)
- [RE2 syntax](https://github.com/google/re2/wiki/Syntax), the grammar for Go `regexp` and ripgrep
- [Go `regexp` package](https://pkg.go.dev/regexp) and [`regexp/syntax`](https://pkg.go.dev/regexp/syntax)
- [Python `re` module](https://docs.python.org/3/library/re.html)
- [MDN regular expressions reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions)
- [GNU grep manual, regular expressions](https://www.gnu.org/software/grep/manual/html_node/Regular-Expressions.html)


