Skip to content
AldeaCode Logo
Regex Tester / grep Developer 100% local

Test regex for grep: BRE vs ERE vs PCRE, and the macOS gotcha

grep on a Linux box and grep on macOS are not the same program, and even one of them speaks three regex dialects depending on the flag you pass. Picking the right dialect saves an afternoon of escape hunting.

BRE, ERE, PCRE: three flavors, one binary

Plain grep defaults to BRE (Basic Regular Expression). In BRE, the metacharacters ?, +, {}, (, ), and | are literals. To get the regex meaning you escape them: \?, \+, \{n\}, \(, \), \|. That is the inverse of every other regex flavor on earth.

grep -E (or egrep) switches to ERE (Extended Regular Expression), where those characters mean what you expect. grep -E 'foo|bar' finds either word; grep 'foo\|bar' does the same in BRE.

grep -P (GNU only) opts into PCRE (Perl-Compatible). That gives you lookahead, lookbehind, lazy quantifiers, named groups, and the rest of the modern regex feature set.

BSD grep on macOS does not have -P

If you wrote a regex on Linux and your colleague on macOS gets an "invalid option" error on -P, this is why. The macOS grep is the BSD implementation, which does not link against PCRE.

Workarounds:

```bash brew install grep # installs as ggrep ggrep -P 'pattern' file.txt

# Or use ripgrep, which speaks Rust regex (PCRE-like) brew install ripgrep rg 'pattern' file.txt ```

Most teams that run cross-platform shell scripts settle on ripgrep precisely because the dialect is consistent everywhere and the performance is far better than grep on large trees. If you are picking a tool for a 2026 codebase, pick rg.

The escape rules that actually trip people up

Even within the right flavor, two specific escapes catch people daily:

1. The literal dot. . matches any character in every flavor. To match a literal dot you write \. In ERE and PCRE that is one backslash; in BRE it is also one backslash; in your shell, the backslash needs to survive shell quoting. Single-quote the whole pattern: grep -E '\.com$' urls.txt.

2. The literal pipe. | is alternation in ERE and PCRE, and a literal in BRE. To match a literal pipe in ERE: grep -E '\|' file.txt. To do the same in BRE: grep '|' file.txt. They are inverted; copy-pasting between scripts will burn you.

When in doubt, use grep -E (ERE) as your default. It is the closest to JavaScript and Python regex flavors and the rules are predictable.

Test the regex in a tester that speaks JavaScript first

If you build the pattern in a regex tester that speaks JavaScript flavor, then port to grep, two adjustments cover most cases:

- Replace lookarounds ((?=...), (?!...)) with anchored alternatives or a grep -P invocation. They do not exist in BRE or ERE. - Replace shorthand classes like \d with [0-9] for portable BRE/ERE; \d is a Perl-ism that works in PCRE only.

For complex patterns, prototype in a visual tester to see what each group is capturing, then translate to the grep dialect you need. Iterating in a CLI alone is slower because grep does not show you the captures, only the matched lines.

Working example

bash
#!/usr/bin/env bash
# BRE: escape ?, +, |, {}, (), to get regex meaning
grep '^[A-Z][a-z]\{2,\}$' /usr/share/dict/words

# ERE: same intent, no escaping
grep -E '^[A-Z][a-z]{2,}$' /usr/share/dict/words

# PCRE (GNU only): lookahead for "word followed by digit"
grep -P '\w+(?=\d)' app.log

# Cross-platform: ripgrep speaks one dialect everywhere
rg '^[A-Z][a-z]{2,}$' /usr/share/dict/words

Just need the result?

When you want to iterate on the pattern visually, see what each group is capturing, and only port to grep once it works, the browser-based regex tester gives you that loop with live highlighting. JavaScript flavor by default, port the result to grep -E with one or two escape adjustments.

Open Regex Tester (JavaScript Flavor) →

Frequently asked questions

Is there a way to make grep behave like Perl on macOS without Homebrew?

Not really. macOS ships BSD grep without PCRE support. The pragmatic options are ggrep via Homebrew, or ripgrep (also Homebrew), or pipe through perl -ne. Trying to downgrade your pattern to BRE works but adds escapes everywhere.

Why does my pattern work in egrep but not in grep?

egrep is grep -E. The pattern probably uses ?, +, |, {}, or () unescaped, which BRE treats as literals. Either keep using -E or escape every metacharacter with a backslash. The first option is almost always the right one.

Should I use grep or ripgrep for new scripts?

Ripgrep for any tree larger than a single file. Same regex dialect on every platform, parallel by default, gitignore aware. Stick with grep when portability to a minimal Alpine container or BusyBox is a hard requirement.