Grep by example: Interactive guide
grep is the ultimate text search tool available on virtually all Linux machines. While there are now better alternatives (such as ripgrep), you will still often find yourself on a server where grep is the only search tool available. So it's nice to have a working knowledge of it.
That's why is I've created this interactive step-by-step guide to grep operations. You can read it from start to finish to (hopefully) learn more about grep, or jump to a specific use case that interests you.
Feel free to experiment with the examples by changing the commands and clicking Run.
Basics · Recursive search · Search options · Output options · Final thoughts
This guide is also available in other formats:
Basics
Basically, grep works like this:
- You give it a search pattern and a file.
- grep reads the file line by line, printing the lines that match the pattern and ignoring others.
Let's look at an example. We'll search the httpurr source code, which I've already downloaded to the /opt/httpurr
directory like this:
cd /opt
curl -OL https://github.com/rednafi/httpurr/archive/refs/tags/v0.1.2.tar.gz
tar xvzf v0.1.2.tar.gz
mv httpurr-0.1.2 httpurr
cd httpurr
Search in file · Matches · Regular expressions · Fixed strings · Multiple patterns
Search in file
Let's find all occurrences of the word codes
in README.md
:
grep -n codes README.md
3: <strong><i> >> HTTP status codes on speed dial << </i></strong>
30:* List the HTTP status codes:
54:* Filter the status codes by categories:
124: Print HTTP status codes by category with --list;
131: Print HTTP status codes
grep read the contents of README.md
, and for each line that contained codes
, grep printed it to the terminal.
grep also included the line number for each line, thanks to the -n
(--line-number
) option.
Not all grep versions support the long option syntax (e.g.
--line-number
). If you get an error using the long version, try the short one (e.g.-n
) — it may work fine.
Matches
grep uses partial matches by default:
grep -n descr README.md
81:* Display the description of a status code:
127: Print the description of an HTTP status code
The word description
matches the descr
search pattern.
To search for whole words instead, use the -w
(--word-regexp
) option:
grep -n --word-regexp code README.md
81:* Display the description of a status code:
84: httpurr --code 410
94: The HyperText Transfer Protocol (HTTP) 410 Gone client error response code
99: code should be used instead.
126: -c, --code [status code]
127: Print the description of an HTTP status code
grep found strings containing the word code
, but not codes
. Try removing --word-regexp
and see how the results change.
When using multiple short options, you can combine them like this:
grep -nw code README.md
. This gives exactly the same result as using the separate options (-n -w
).
To search for whole lines instead of partial matches of whole words, use the -x
(--line-regexp
) option:
grep -n --line-regexp end httpurr.rb
47:end
Try removing --line-regexp
and see how the results change.
Regular expressions
To make grep use regular expressions (Perl-compatible regular expressions in grep terminology), use the -P
(--perl-regexp
) option.
Let's find all lines with a word that contains res
followed by other letters:
grep -Pn 'res\w+' README.md
94: The HyperText Transfer Protocol (HTTP) 410 Gone client error response code
95: indicates that access to the target resource is no longer available at the
152:of the rest.
\w+
means "one or more word-like characters" (e.g. letters like p
or o
, but not punctuation like .
or !
), so response
, resource
, and rest
all match.
Regular expression dialects in grep
Without --perl-regexp
, grep treats the search pattern as something called a basic regular expression. While regular expressions are quite common in the software world, the basic dialect is really weird, so it's better not to use it at all.
Another dialect supported by grep is extended regular expressions. You can use the -E
(--extended-regexp
) option to enable them. Extended regular expressions are almost like normal regular expressions, but not quite. So I wouldn't use them either.
Some grep versions do not support --perl-regexp
. For those, --extended-regexp
is the best you can get.
Suppose we are only interested in 4 letter words starting with res
:
grep -Pn 'res\w\b' README.md
152:of the rest.
\b
means "word boundary" (e.g. a space, a punctuation character, or the end of a line), so rest
matches, but response
and resource
don't.
Finally, let's search for 3-digit numbers (showing first 10 matches with head
):
grep -Pn '\d\d\d' README.md | head
45: 100 Continue
46: 101 Switching Protocols
47: 102 Processing
48: 103 Early Hints
69: 200 OK
70: 201 Created
71: 202 Accepted
72: 203 Non-Authoritative Information
73: 204 No Content
74: 205 Reset Content
A full tutorial on regular expressions is beyond the scope of this guide, but grep's "Perl-compatible" syntax is documented in the PCRE2 manual.
Fixed strings
What if we want to search for a literal string instead of a regular expression? Suppose we are interested in a word code
followed by a dot:
grep -Pn 'code.' src/data.go | head
8:The HTTP 100 Continue informational status response code indicates that
14:status code in response before sending the body.
31:The HTTP 101 Switching Protocols response code indicates a protocol to which the
53:Deprecated: This status code is deprecated. When used, clients may still accept
56:The HTTP 102 Processing informational status response code indicates to client
59:This status code is only sent if the server expects the request to take
112:The HTTP 200 OK success status response code indicates that the request has
141:The HTTP 201 Created success status response code indicates that the request has
149:The common use case of this status code is as the result of a POST request.
165:The HTTP 202 Accepted response status code indicates that the request has been
Since .
means "any character" in regular expressions, our pattern also matches code
, codes
and other cases we are not interested in.
To treat the pattern as a literal string, use the -F
(--fixed-strings
) option:
grep -Fn 'code.' src/data.go
197:to responses with any status code.
283:Browsers accessing web pages will never encounter this status code.
695:to an error code.
1027:erroneous cases it happens, they will handle it as a generic 400 status code.
1051:Regular web servers will normally not return this status code. But some other
1418:then the server responds with the 510 status code.
Much better!
Multiple patterns
To search for multiple patterns, list them with the -e
(--regexp
) option. grep will output lines matching at least one of the specified patterns.
For example, search for make
or run
:
grep -En -e make -e run README.md
139:* Go to the root directory and run:
141: make init
145: make lint
149: make test
Unfortunately, grep can't use Perl-compatible regular expressions (
-P
) with multiple patterns. So we are stuck with the extended (-E
) dialect.
If you have many patterns, it may be easier to put them in a file and point grep to it with -f
(--file
):
echo 'install' > /tmp/patterns.txt
echo 'make' >> /tmp/patterns.txt
echo 'run' >> /tmp/patterns.txt
grep -En --file=/tmp/patterns.txt README.md
13:* On MacOS, brew install:
17: && brew install httpurr
20:* Or elsewhere, go install:
23: go install github.com/rednafi/httpurr/cmd/httpurr
139:* Go to the root directory and run:
141: make init
145: make lint
149: make test
Recursive search
grep searches directories recursively when called with the -r
(--recursive
) option.
Search in directory · File globs · Binary files
Search in directory
Let's find all unexported functions (they start with a lowercase letter):
grep -Pnr 'func [a-z]\w+' .
./cmd/httpurr/main.go:12:func main() {
./src/cli.go:16:func formatStatusText(text string) string {
./src/cli.go:21:func printHeader(w *tabwriter.Writer) {
./src/cli.go:35:func printStatusCodes(w *tabwriter.Writer, category string) error {
./src/cli.go:105:func printStatusText(w *tabwriter.Writer, code string) error {
This search returned matches from both the cmd
and src
directories. If you are only interested in cmd
, specify it instead of .
:
grep -Pnr 'func [a-z]\w+' cmd
cmd/httpurr/main.go:12:func main() {
To search multiple directories, list them all like this:
grep -Pnr 'func [a-z]\w+' cmd src
cmd/httpurr/main.go:12:func main() {
src/cli.go:16:func formatStatusText(text string) string {
src/cli.go:21:func printHeader(w *tabwriter.Writer) {
src/cli.go:35:func printStatusCodes(w *tabwriter.Writer, category string) error {
src/cli.go:105:func printStatusText(w *tabwriter.Writer, code string) error {
File globs
Let's search for httpurr
:
grep -Pnr --max-count=5 httpurr .
./README.md:2: <h1>ᗢ httpurr</h1>
./README.md:16: brew tap rednafi/httpurr https://github.com/rednafi/httpurr \
./README.md:17: && brew install httpurr
./README.md:23: go install github.com/rednafi/httpurr/cmd/httpurr
./README.md:33: httpurr --list
./cmd/httpurr/main.go:4: "github.com/rednafi/httpurr/src"
./go.mod:1:module github.com/rednafi/httpurr
./httpurr.rb:7: homepage "https://github.com/rednafi/httpurr"
./httpurr.rb:12: url "https://github.com/rednafi/httpurr/releases/download/v0.1.1/httpurr_Darwin_x86_64.tar.gz"
./httpurr.rb:16: bin.install "httpurr"
./httpurr.rb:20: url "https://github.com/rednafi/httpurr/releases/download/v0.1.1/httpurr_Darwin_arm64.tar.gz"
./httpurr.rb:24: bin.install "httpurr"
./src/cli.go:24: fmt.Fprintf(w, "\nᗢ httpurr\n")
./src/cli_test.go:64: want := "\nᗢ httpurr\n==========\n\n"
Note that I have limited the number of results per file to 5 with the -m
(--max-count
) option to keep the results readable in case there are many matches.
Quite a lot of results. Let's narrow it down by searching only in .go
files:
grep -Pnr --include='*.go' httpurr .
./cmd/httpurr/main.go:4: "github.com/rednafi/httpurr/src"
./src/cli.go:24: fmt.Fprintf(w, "\nᗢ httpurr\n")
./src/cli_test.go:64: want := "\nᗢ httpurr\n==========\n\n"
The --include
option (there is no short version) takes a glob (filename pattern), typically containing a fixed part (.go
in our example) and a wildcard *
("anything but the path separator").
Another example — search in files named http
-something:
grep -Pnr --include='http*' httpurr .
./httpurr.rb:7: homepage "https://github.com/rednafi/httpurr"
./httpurr.rb:12: url "https://github.com/rednafi/httpurr/releases/download/v0.1.1/httpurr_Darwin_x86_64.tar.gz"
./httpurr.rb:16: bin.install "httpurr"
./httpurr.rb:20: url "https://github.com/rednafi/httpurr/releases/download/v0.1.1/httpurr_Darwin_arm64.tar.gz"
./httpurr.rb:24: bin.install "httpurr"
./httpurr.rb:31: url "https://github.com/rednafi/httpurr/releases/download/v0.1.1/httpurr_Linux_arm64.tar.gz"
./httpurr.rb:35: bin.install "httpurr"
./httpurr.rb:39: url "https://github.com/rednafi/httpurr/releases/download/v0.1.1/httpurr_Linux_x86_64.tar.gz"
./httpurr.rb:43: bin.install "httpurr"
To negate the glob, use the --exclude
option. For example, search everywhere except the .go
files:
grep -Pnr --exclude '*.go' def .
./.goreleaser.yml:1:# This is an example .goreleaser.yml file with some sensible defaults.
./httpurr.rb:15: def install
./httpurr.rb:21: sha256 "82acefd1222f6228636f2cda6518e0316f46624398adc722defb55c68ac3bb30"
./httpurr.rb:23: def install
./httpurr.rb:34: def install
./httpurr.rb:42: def install
To apply multiple filters, specify multiple glob options. For example, find all functions except those in test files:
grep -Pnr --include '*.go' --exclude '*_test.go' 'func ' .
./cmd/httpurr/main.go:12:func main() {
./src/cli.go:16:func formatStatusText(text string) string {
./src/cli.go:21:func printHeader(w *tabwriter.Writer) {
./src/cli.go:35:func printStatusCodes(w *tabwriter.Writer, category string) error {
./src/cli.go:105:func printStatusText(w *tabwriter.Writer, code string) error {
./src/cli.go:123:func Cli(w *tabwriter.Writer, version string, exitFunc func(int)) {
Binary files
By default, grep does not ignore binary files:
grep -Pnr aha .
grep: ./data.bin: binary file matches
Most of the time, this is probably not what you want. If you're searching in a directory that might contain binary files, it's better to ignore them altogether with the -I
(--binary-files=without-match
) option:
grep -Pnr --binary-files=without-match aha .
(not found)
If for some reason you want grep to search binary files and print the actual matches (as it does with text files), use the -a
(--text
) option.
Search options
grep supports a couple of additional search options you may find handy.
Ignore case · Inverse matching
Ignore case
Let's find all occurrences of the word codes
in README.md
:
grep -Pnr codes README.md
3: <strong><i> >> HTTP status codes on speed dial << </i></strong>
30:* List the HTTP status codes:
54:* Filter the status codes by categories:
124: Print HTTP status codes by category with --list;
131: Print HTTP status codes
It returns codes
matches, but not Codes
because grep is case-sensitive by default. To change this, use -i
(--ignore-case
):
grep -Pnr --ignore-case codes README.md
3: <strong><i> >> HTTP status codes on speed dial << </i></strong>
30:* List the HTTP status codes:
40: Status Codes
54:* Filter the status codes by categories:
64: Status Codes
124: Print HTTP status codes by category with --list;
131: Print HTTP status codes
Inverse matching
To find lines that do not contain the pattern, use -v
(--invert-match
). For example, find the non-empty lines without the @
symbol:
grep -Enr --invert-match -e '@' -e '^$' Makefile
1:.PHONY: lint
2:lint:
8:.PHONY: lint-check
9:lint-check:
14:.PHONY: test
15:test:
20:.PHONY: clean
21:clean:
27:.PHONY: init
28:init:
Output options
grep supports a number of additional output options you may find handy.
Count matches · Limit matches · Show matches only · Show files only · Show context · Silent mode · Colors
Count matches
To count the number of matched lines (per file), use -c
(--count
). For example, count the number of functions in each .go
file:
grep -Pnr --count --include '*.go' 'func ' .
./cmd/httpurr/main.go:1
./src/cli.go:5
./src/cli_test.go:10
./src/data_test.go:2
Note that --count
counts the number of lines, not the number of matches. For example, there are 6 words string
in src/cli.go
, but two of them are on the same line, so --count
reports 5:
grep -nrw --count string src/cli.go
5
Limit matches
To limit the number of matching lines per file, use the -m
(--max-count
) option:
grep -Pnrw --max-count=5 func .
./cmd/httpurr/main.go:12:func main() {
./src/cli.go:16:func formatStatusText(text string) string {
./src/cli.go:21:func printHeader(w *tabwriter.Writer) {
./src/cli.go:35:func printStatusCodes(w *tabwriter.Writer, category string) error {
./src/cli.go:105:func printStatusText(w *tabwriter.Writer, code string) error {
./src/cli.go:123:func Cli(w *tabwriter.Writer, version string, exitFunc func(int)) {
./src/cli_test.go:15:func TestFormatStatusText(t *testing.T) {
./src/cli_test.go:54:func TestPrintHeader(t *testing.T) {
./src/cli_test.go:71:func TestPrintStatusCodes(t *testing.T) {
./src/cli_test.go:159: t.Run(want, func(t *testing.T) {
./src/cli_test.go:168:func TestPrintStatusText(t *testing.T) {
./src/data_test.go:9:func TestStatusCodes(t *testing.T) {
./src/data_test.go:99:func TestStatusCodeMap(t *testing.T) {
With --max-count=N
, grep stops searching the file after finding the first N matching lines (or non-matching lines if used with --invert-match
).
Show matches only
By default, grep prints the entire line containing the match. To show only the matching part, use -o
(--only-matching
).
Suppose we want to find functions named print
-something:
grep -Pnr --only-matching --include '*.go' 'func print\w+' .
./src/cli.go:21:func printHeader
./src/cli.go:35:func printStatusCodes
./src/cli.go:105:func printStatusText
The results are much cleaner than without --only-matching
(try removing the option in the above command and see for yourself).
Show files only
If there are too many matches, you may prefer to show only the files where the matches occurred. Use -l
(--files-with-matches
) to do this:
grep -Pnr --files-with-matches 'httpurr' .
./README.md
./cmd/httpurr/main.go
./go.mod
./httpurr.rb
./src/cli.go
./src/cli_test.go
Show context
Let's search for GitHub action jobs:
grep -Pnr 'jobs:' .github/workflows
.github/workflows/automerge.yml:8:jobs:
.github/workflows/lint.yml:11:jobs:
.github/workflows/release.yml:10:jobs:
.github/workflows/test.yml:11:jobs:
These results are kind of useless, because they don't return the actual job name (which is on the next line after jobs
). To fix this, let's use -C
(--context
), which shows N
lines around each match:
grep -Pnr --context=1 'jobs:' .github/workflows
.github/workflows/automerge.yml-7-
.github/workflows/automerge.yml:8:jobs:
.github/workflows/automerge.yml-9- dependabot:
--
.github/workflows/lint.yml-10-
.github/workflows/lint.yml:11:jobs:
.github/workflows/lint.yml-12- golangci:
--
.github/workflows/release.yml-9-
.github/workflows/release.yml:10:jobs:
.github/workflows/release.yml-11- goreleaser:
--
.github/workflows/test.yml-10-
.github/workflows/test.yml:11:jobs:
.github/workflows/test.yml-12- test:
It might be even better to show only the next line after the match, since we are not interested in the previous one. Use -A
(--after-context
) for this:
grep -Pnr --after-context=1 'jobs:' .github/workflows
.github/workflows/automerge.yml:8:jobs:
.github/workflows/automerge.yml-9- dependabot:
--
.github/workflows/lint.yml:11:jobs:
.github/workflows/lint.yml-12- golangci:
--
.github/workflows/release.yml:10:jobs:
.github/workflows/release.yml-11- goreleaser:
--
.github/workflows/test.yml:11:jobs:
.github/workflows/test.yml-12- test:
There is also
-B
(--before-context
) for showing N lines before the match.
Nice!
Silent mode
Sometimes you just want to know if a file contains a certain string; you don't care about the number or positions of the matches.
To make grep quit immediately after the first match and not print anything, use the -q
(--quiet
or --silent
) option. Use the return code ($?
) to see if grep found anything (0 — found, 1 — not found):
grep -Pnrw --quiet main cmd/httpurr/main.go
if [ $? = "0" ]; then echo "found!"; else echo "not found"; fi
found!
Try changing the search pattern from main
to Main
and see how the results change.
When searching in multiple files with --quiet
, grep stops after the first match in any file and does not check other files:
grep -Pnrw --quiet main .
if [ $? = "0" ]; then echo "found!"; else echo "not found"; fi
found!
Colors
To highlight matches and line numbers, use the --color=always
option:
grep -Pnr --color=always codes README.md
3: <strong><i> >> HTTP status codes on speed dial << </i></strong>
30:* List the HTTP status codes:
54:* Filter the status codes by categories:
124: Print HTTP status codes by category with --list;
131: Print HTTP status codes
Use --color=auto
to let grep decide whether to use colors based on your terminal. Use --color=never
to force no-color mode.
Final thoughts
That's it! We've covered just about everything grep can do. Unfortunately, it doesn't support replacing text, reading options from a configuration file, or other fancy features provided by grep alternatives like ack
or ripgrep
. But grep is still quite powerful, as you can probably see now.
Use grep --help
to quickly see all supported options and see the official guide for option descriptions.
Have fun grepping!
──
P.S. Interactive examples in this post are powered by codapi — an open source tool I'm building. Use it to embed live code snippets into your product docs, online course or blog.
★ Subscribe to keep up with new posts.