A line filter with three distinct jobs
grep selects input records that match a pattern. Ordinarily a record is a newline-delimited line, and the output is the entire selected line. That last detail matters: an empty pattern matches every line, while -o changes the unit of output to just matching text.
The three engines worth distinguishing are basic regular expressions by default, extended expressions with -E, and literal strings with -F. Extended syntax changes how parentheses, alternation, and + are written; it is not a switch to a more powerful PCRE engine. GNU grep -P is a separate, optional Perl-compatible mode and is not a portable default.
When a search term comes from a ticket, username, IP address, or another literal value, -F avoids accidental regex meaning. When it comes from an untrusted source, also use -e: a value starting with a hyphen must never become an option.
grep -F -e 'user.name@example.com' access.log
grep -E -e '^(ERROR|WARN)[[:space:]]' app.log
grep -n -e '^' config.iniMultiple patterns are an OR, and empty patterns are real
Several -e options select a line if any pattern matches. A pattern file supplied with -f contains one pattern per line; its empty lines match every input line. That is a subtle failure mode for generated allowlists: an accidental blank line can turn a selective search into a full dump.
grep -v reverses selection. It does not mean “subtract these matching fragments”; it rejects the entire line when any pattern matches. Combining -v with a pattern file is useful for excluding a set of known messages, but inspect that file for blanks first.
grep -E -e 'timeout' -e 'connection reset' service.log
grep -v -F -f known-noise.txt service.log
# Empty pattern: every line, including an empty line, is selected.
grep '' /etc/hostsSearch a record that spans lines
A normal grep cannot make one match cross a newline, because the newline ends the record. GNU grep -z treats NUL as the record separator. If the input contains no NUL, the whole file becomes one record, so a -P expression can cross newlines. This is useful for a bounded small document; it is dangerous on a large one because GNU grep may need to hold the entire record in memory.
Do not confuse -z with -Z. Lowercase -z changes input and output record separators. Uppercase -Z puts a NUL after each filename grep prints, so a downstream program can distinguish filenames containing spaces or newlines. These GNU options are not the POSIX core.
# GNU grep with PCRE support; small, bounded input only.
grep -Pzo 'BEGIN\n(?s:.*?)\nEND' sample.txt
# GNU grep: filenames safely separated by NUL.
grep -lZ -F 'deprecated' -- ./*.conf | xargs -0 -r printf '%s\n'Recursive search has a filesystem policy
GNU grep -r descends directories but does not follow symbolic links found during traversal; -R follows all symbolic links. Following a link into a large mount, a loop, or a sensitive tree can make a routine search expensive or surprising. Include and exclude options constrain names, but their exact availability and traversal details should be checked on the installed implementation.
Shell globs expand before grep sees them. A pattern like *.log at the shell prompt refers only to matches in that directory; it does not tell recursive grep to select every log file below it. Pass a directory to -r and use GNU --include when that is the intended scope.
A name beginning with - needs protection as an operand. The portable habit is to put -- before paths where supported, or prefix a relative filename with ./; use -e for the pattern itself.
grep -r -n -F --include='*.conf' -e 'PermitRootLogin' /etc/ssh
grep -R -n -F -e 'TODO' project/ # follows symlinks: inspect scope first
grep -F -e '-debug' -- ./settings.confFind matches in names without breaking names
A filename may contain a space, tab, or even a newline. Parsing ordinary grep -l output line by line is therefore unsafe when filenames are arbitrary. GNU grep -lZ emits each matching filename followed by NUL; pair it with a NUL-aware consumer. On systems without -Z, use a different file traversal strategy instead of pretending newline is a safe delimiter.
The familiar grep | head pattern is fine for human viewing, but do not turn that display into a machine-readable filename protocol. Binary input is another trap: grep may report that a binary file matches rather than print its contents. GNU -a forces text treatment, which can send control bytes to the terminal; avoid it on unknown files.
# GNU-specific: inspect files containing a fixed marker safely.
grep -lZ -r -F -e 'LegacyMode' ./configs | xargs -0 -r ls -ld --
# For a one-off human inspection, filename and line number are useful.
grep -nH -C 2 -F -e 'LegacyMode' ./configs/app.confOutput controls with surprising consequences
The -c result is a count of selected lines, not the number of occurrences. A line with ten matches adds one. For matching substrings, -o prints each nonempty match separately; pairing it with wc -l can count occurrences in ordinary newline-delimited text, but it changes behavior around empty matches and unusual input.
The -m NUM option stops after NUM selected lines per file. GNU grep documents a useful detail for standard input from a regular file: it positions the file descriptor just after the last selected line before returning. That property can support a sequential reader, but do not assume it for pipes.
Context flags -A, -B, and -C add nearby lines. Output may merge adjacent context groups; do not treat the number of printed lines as a count of matches.
printf 'aa aa\naa\n' | grep -o 'aa' | wc -l # 3 occurrences
printf 'aa aa\naa\n' | grep -c 'aa' # 2 matching lines
grep -m 1 -n -F -e 'START' event.logExit status, quiet mode, and SIGPIPE
grep reports 0 for a match, 1 for no match, and greater than 1 for an error. -q suppresses normal output and can stop once a match is known. GNU grep warns that -q can return success after a match even if an error occurs; that is acceptable for some predicates but not for a comprehensive file audit.
A producer piped into grep -q may receive SIGPIPE when grep exits early. With shell pipefail enabled, a successful match can still leave the pipeline nonzero because the producer failed on its closed output. Test the predicate and error semantics you actually need, especially in scripts using set -e and pipefail.
If the requirement is “this entire file was readable and a pattern was found,” do not casually use -q as if it proves the read completed. Separate file errors from absence where that distinction matters.
if grep -qF -e 'ready' status.txt; then
echo 'matched'
else
rc=$?
if [ "$rc" -eq 1 ]; then echo 'no match'; else echo "grep error: $rc" >&2; fi
fiLocale and regex cost are operational concerns
Character classes, case folding, collation, and what counts as a word depend on locale. An ASCII-oriented search can be made more predictable with LC_ALL=C, but that also changes how non-ASCII text is interpreted. Prefer a deliberate locale choice to a superstition that C is universally faster or correct.
A simple literal search with -F avoids regex complexity. GNU grep documents that back-references may trigger costly backtracking, large repetition counts may use substantial memory, and some obscure expressions can take exponential time and space. A service that accepts user-supplied patterns and searches attacker-controlled data needs limits on input size, runtime, and privileges; do not treat grep as a sandbox.
GNU -P depends on PCRE support and has its own syntax and behavior. Keep it as a clearly marked implementation-specific tool for tasks that actually need lookarounds or other PCRE features.
LC_ALL=C grep -F -e 'ERROR' bounded.log
# Avoid feeding arbitrary user-supplied regex to a privileged search job.
grep -F -e "$literal_query" -- "$approved_file"A security boundary: grep can read what its caller can read
The documented GTFOBins example is almost embarrassingly simple: an empty pattern matches all text lines. If an operator grants unrestricted sudo access to grep, a user can use that privilege to print readable text from files that the operator intended to protect. This is a privilege and policy mistake, not a flaw in grep. Binary data may be altered in the display.
The same principle matters in support scripts, CI jobs, and web wrappers: running grep with elevated privileges against caller-selected paths can expose secrets. Constrain both the file set and the result, avoid broad sudoers grants, and use a narrowly scoped service or purpose-built command for privileged checks.
Grep is also a common discovery tool after an intrusion because it can rapidly search readable configuration and logs for tokens or keys. Seeing grep in a process list is not evidence of compromise by itself; correlate the account, command line, paths, and surrounding activity.
# Demonstration with a harmless local file: empty pattern selects every line.
printf 'alpha\nbeta\n' > /tmp/grep-demo.txt
grep '' /tmp/grep-demo.txt
# Audit privileged command rules rather than granting general sudo grep.
sudo -lA real historical flaw in GNU grep
CVE-2012-5667 described integer overflows in GNU grep versions before 2.11, where very long input lines could trigger a heap buffer overflow. The published record says arbitrary code execution might be possible in context. Debian tracks fixes in its packages, including an older backport, so the displayed upstream version alone is not always enough to determine exposure.
This is a historical example, not a claim that current packaged grep is vulnerable. Determine the implementation and package revision on the actual host, then consult that distribution’s advisory. Modern systems should take vendor security updates; do not reproduce the huge-input proof of concept on a production host.
grep --version 2>/dev/null | head -n 1
# Debian or Ubuntu: inspect installed package revision.
dpkg-query -W grep
# RHEL-family systems: inspect installed package revision.
rpm -q grepRelated name, different package: zgrep
CVE-2022-1271 affected the zgrep script shipped with GNU gzip, not the grep executable. A crafted filename could lead to an arbitrary file write in affected versions. It is relevant to a grep workflow because an administrator may use zgrep interchangeably on compressed logs and overlook that it is a separate wrapper with a separate package and update path.
For either case, distinguish an implementation defect from an insecure way of invoking a legitimate tool. A CVE applies to specific affected packages and versions; a broad sudo grant remains risky even after every CVE is patched.
# Identify the packages independently.
command -v grep zgrep
dpkg-query -W grep gzip 2>/dev/null || rpm -q grep gzipField exercise: investigate a noisy log safely
Start with a bounded file, a literal search, and context. Count selected lines before claiming you counted events. If you need several error signatures, use separate -e arguments or a reviewed pattern file. Move to recursive and binary handling only when you know why the ordinary line search is insufficient.
When the query comes from another person, treat it as data. Use -F -e, constrain the readable paths, and avoid running the search with broader privileges than necessary.
grep -nH -C 3 -F -e 'connection reset' ./sample.log
grep -c -F -e 'connection reset' ./sample.log
grep -nH -E -e 'timeout|connection reset' ./sample.log