GNU grep and BSD grep: what travels and what does not¶
Level: 201 · you copy grep lines between a Linux book or server and a Mac
One line: The grep that ships with macOS, BSD grep 2.6.0, calls itself "GNU compatible", and most of a Linux book's grep lines do run on it unchanged: \w, \s, \b, \<, \|, \+, \?, --include, --null, egrep. What does not travel fails in one of three ways. Loudly: -P is an invalid option. Silently: "\d" is a digit to BSD grep and the letter d to GNU grep, and neither warns. Or in the output: -r with no directory, -Z, and symlinks under -r and -R give a different list.
Measured¶
What travels¶
Every line of this script printed the same bytes with GNU grep 3.11 on Ubuntu and BSD grep 2.6.0-FreeBSD on macOS:
Verified output of gnubsd_same_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ grep -E "[[:digit:]]+" hosts.txt
alma 9 build
ubuntu 24 box
$ grep "[0-9]\{2\}" hosts.txt
ubuntu 24 box
$ grep -E "[0-9]{2}" hosts.txt
ubuntu 24 box
$ grep -o "\w\+ box" hosts.txt
fedora box
24 box
$ grep -c "\s" hosts.txt
4
$ grep -o "\bbox\b" hosts.txt
box
box
$ grep "\<box\>" hosts.txt
fedora box
ubuntu 24 box
$ grep "alma\|ubuntu" hosts.txt
alma 9 build
ubuntu 24 box
$ grep "fed\?ora" hosts.txt
fedora box
$ grep -e "box$" -e "^alma" hosts.txt
alma 9 build
fedora box
ubuntu 24 box
$ echo aaa | grep -o "^a"
a
$ grep -r --include="*.log" darkfi logs
logs/web.log:darkfi crashed
$ grep -l --null darkfi logs/web.log logs/notes.txt | tr "\0" "|"; echo
logs/web.log|logs/notes.txt|
$ egrep "alma|ubuntu" hosts.txt
alma 9 build
ubuntu 24 box
$ fgrep "a.b" hosts.txt; echo "status $?"
status 1
$ printf "5\nd\n" | grep \d
d
- POSIX classes and intervals —
[[:digit:]],\{2\}in a basic expression,{2}with-E— are in the POSIX grammar ↗, and both greps gave them the same answers. They are the safest spelling. - The GNU backslash extensions work in BSD grep too:
\wand\s,\band\<\>for word edges, and\|,\+and\?inside a basic expression. -e box$ -e ^almais two patterns, each with its own anchor. It is the portable form of an alternation whose branches carry^or$, whichbox$\|^almais not, below.grep -o "^a"onaaaprinted oneaon both.--include,--null,egrepandfgrepbehave alike, and neitheregrepnorfgrepprinted a warning on either machine.grep -Eandgrep -Fare the spellings POSIX ↗ specifies, and the ones to write.grep \dwithout quotes printeddon both, for a reason that has nothing to do with grep: the shell removed the backslash, and both greps received the single letterd. The quoted"\d"is where they part.
What does not travel¶
Each line of this script printed something different on the two machines. The key is split on purpose:
Verified output of gnubsd_differ_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ grep --version | head -n 1
grep (GNU grep) 3.11
$ grep -P "\d+" hosts.txt; echo "status $?"
alma 9 build
ubuntu 24 box
status 0
$ printf "5\nd\n" | grep "\d"
d
$ printf "A\nx41\n" | grep "\x41"
x41
$ grep "[[:<:]]box[[:>:]]" hosts.txt; echo "status $?"
grep: Invalid character class name
status 2
$ grep "box$\|^alma" hosts.txt
alma 9 build
fedora box
ubuntu 24 box
$ grep -cE "alma|" hosts.txt; echo "status $?"
4
status 0
$ printf "x\nax\n" | grep -E "a{,2}x"; echo "status $?"
x
ax
status 0
$ grep "[" hosts.txt; echo "status $?"
grep: Invalid regular expression
status 2
$ grep -r darkfi | sort
extra/more.txt:darkfi in extra
hosts.txt:darkfi node
logs/web.log:darkfi crashed
$ grep -R darkfi logs | sort
logs/extra_link/more.txt:darkfi in extra
logs/hosts_link.txt:darkfi node
logs/web.log:darkfi crashed
$ grep -r darkfi logs/hosts_link.txt; echo "status $?"
darkfi node
status 0
$ grep -lZ darkfi hosts.txt logs/web.log | tr "\0" "|"; echo
hosts.txt|logs/web.log|
$ grep -q darkfi hosts.txt nosuch.txt; echo "status $?"
status 0
Verified output of gnubsd_differ_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ grep --version | head -n 1
grep (BSD grep, GNU compatible) 2.6.0-FreeBSD
$ grep -P "\d+" hosts.txt; echo "status $?"
grep: invalid option -- P
usage: grep [-abcdDEFGHhIiJLlMmnOopqRSsUVvwXxZz] [-A num] [-B num] [-C[num]]
[-e pattern] [-f file] [--binary-files=value] [--color=when]
[--context[=num]] [--directories=action] [--label] [--line-buffered]
[--null] [pattern] [file ...]
status 2
$ printf "5\nd\n" | grep "\d"
5
$ printf "A\nx41\n" | grep "\x41"
A
$ grep "[[:<:]]box[[:>:]]" hosts.txt; echo "status $?"
fedora box
ubuntu 24 box
status 0
$ grep "box$\|^alma" hosts.txt
alma 9 build
$ grep -cE "alma|" hosts.txt; echo "status $?"
grep: empty (sub)expression
status 2
$ printf "x\nax\n" | grep -E "a{,2}x"; echo "status $?"
status 1
$ grep "[" hosts.txt; echo "status $?"
grep: brackets ([ ]) not balanced
status 2
$ grep -r darkfi | sort
./extra/more.txt:darkfi in extra
./hosts.txt:darkfi node
./logs/web.log:darkfi crashed
$ grep -R darkfi logs | sort
logs/web.log:darkfi crashed
$ grep -r darkfi logs/hosts_link.txt; echo "status $?"
status 1
$ grep -lZ darkfi hosts.txt logs/web.log | tr "\0" "|"; echo
hosts.txt
logs/web.log
$ grep -q darkfi hosts.txt nosuch.txt; echo "status $?"
grep: nosuch.txt: No such file or directory
status 0
| The line | GNU grep 3.11, Linux | BSD grep 2.6.0, macOS |
|---|---|---|
-P |
Perl-compatible regex: the lines with a digit | invalid option -- P, the usage text, status 2 |
"\d" |
the letter d: it matched d |
a digit: it matched 5 |
"\x41" |
the letters x41 |
the character A |
[[:<:]] and [[:>:]] |
an error, status 2 | BSD's own word edges: both box lines |
$ at the end of a branch in a basic expression |
an anchor: three lines | an ordinary character: only alma |
an empty branch after -E |
matches every line: 4 | empty (sub)expression, status 2 |
{,2} after -E |
0 to 2 of the atom: x and ax |
not an interval: no match, status 1 |
[ |
one wording of the error | another, same status 2 |
-r with no directory |
searches ., names like hosts.txt |
searches ., names like ./hosts.txt |
-R into a directory with symlinks |
follows both symlinks in logs |
follows neither |
-r on a symlink named on the command line |
follows it | does not, status 1 |
-lZ |
a NUL after each name | a newline after each name: -Z is not --null here |
-q with a missing second file |
silent, status 0 | the missing-file message, status 0 |
Three of those rows are worth remembering over the rest:
"\d"fails silently. A Linux book that writesgrep '\d'for a digit is wrong on Linux too, where it matches the letterd, and it is right by accident on a Mac. Neither grep said anything.[[:digit:]]or[0-9]means a digit to both, and so does\dbehind-Pon Linux.-lZ | xargs -0splits nothing on a Mac. GNU grep's-Zis--null; BSD grep's-Zis something else, and the names come out one per line.--nullworks on both, as the first script showed.grep -rwith no directory is not a stable format. Both searched the current directory, but a script that strips the./from BSD grep's names, or expects it, breaks on the other machine. Name the directory,grep -r darkfi ., and both print./.
On a Mac¶
This page is the Mac's half. The portable spellings, all measured on this page or the everyday options:
| Instead of | Write | Because |
|---|---|---|
grep '\d' |
grep '[[:digit:]]' or grep '[0-9]' |
\d means a digit to one grep and d to the other |
grep -P '...' |
grep -E with POSIX classes; or perl -ne 'print if /.../'; or fish's string match -r |
BSD grep has no -P |
an alternation with ^ or $ in a branch |
grep -e 'box$' -e '^alma' |
BSD grep reads $ at the end of a branch as a character |
grep -lZ |
grep -l --null |
BSD grep's -Z is not the NUL separator |
grep -r pattern |
grep -r pattern . |
the two greps name the files differently |
egrep, fgrep |
grep -E, grep -F |
the POSIX spellings |
The encodings library's grep page ↗ has two more differences, both on text that is not plain ASCII: BSD grep skipping a line it cannot decode, and the "binary file matches" notice going to a different stream on each machine.
In zsh and fish¶
The shell does not change what grep does with a pattern, but it does change what reaches grep. In zsh, as in bash, an unquoted backslash is removed first:
Verified output of gnubsd_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ print -r -- \d "\d"
d \d
$ printf "5\nd\n" | grep \d
d
$ [[ "ubuntu 24" =~ "[[:digit:]]+" ]] && echo "$MATCH"
24
$ [[ "ubuntu 24" =~ "[0-9]+" ]] && echo "$MATCH"
24
\d unquoted arrived as d; "\d" arrived with its backslash. zsh's own =~ compared against [[:digit:]]+ and [0-9]+ and put 24 in $MATCH on both machines.
fish removes an unquoted backslash too, and it has a regex matcher of its own:
Verified output of gnubsd_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ echo \d "\d"
d \d
$ printf "5\nd\n" | grep \d
d
$ string match -r "\d+" "ubuntu 24"
24
$ string match -re "\d" < hosts.txt
alma 9 build
ubuntu 24 box
$ string match -rq "\d" -- "fedora box"; or echo "no digit"
no digit
$ perl -ne "print if /\d/" hosts.txt
alma 9 build
ubuntu 24 box
string match -r is built into fish and uses PCRE2 on both machines, so \d is a digit in it on a Mac as well: it is the -P BSD grep does not have. With -e it prints entire matching lines, as grep does; without it, only the match. perl -ne 'print if /\d/' printed the same two lines, and perl is on both machines too.
If you are coming from another library¶
- Encodings. PCRE2 — the other regex engine ↗ is the engine behind
grep -P, measured throughrg -P.grepon text that is not ASCII ↗ has the GNU/BSD splits this page leaves out. - Perl.
-nand-pare a loop ↗ is whatperl -ne 'print if /\d/'turns into. - Ruby.
\wis ASCII,[[:alpha:]]is not ↗: the same\w,\d,\sand\b, in an engine where they disagree with the POSIX classes about text beyond ASCII.
See also¶
- The everyday options — the options that do behave the same
- grep exits 0, 1 or 2
- GNU grep manual ↗ and FreeBSD
grep(1)↗ - fish
string match↗