grep: Pattern Matching, Exit Codes, and Pipeline Pitfalls

1 min readSystems & Networking

grep searches files or stdin for lines matching a regex and prints matches. Exit code 0=match found, 1=no match, 2=error. In CI pipelines with set -o pipefail, exit code 1 (no match) terminates the script. Fix with || true or capture PIPESTATUS separately. grep -c counts matches; grep -l lists matching filenames; grep -v inverts selection.

shelltext-processing

Common grep patterns

# Basic pattern matching
grep "error" application.log

# Case-insensitive
grep -i "error" application.log

# Count matches (not lines with -c)
grep -c "error" application.log

# Show line numbers
grep -n "error" application.log

# Show N lines of context
grep -A 3 "error" application.log  # 3 lines after
grep -B 3 "error" application.log  # 3 lines before
grep -C 3 "error" application.log  # 3 lines before and after

# Invert match (lines NOT matching)
grep -v "DEBUG" application.log

# Extended regex (-E or egrep)
grep -E "error|warn|fatal" application.log

# Fixed string (no regex interpretation, faster for literal strings)
grep -F "error[0]" application.log

# Recursive search in directory
grep -r "TODO" ./src/

# Only filenames, not content
grep -l "TODO" ./src/**/*.go

# Only matching part (not the whole line)
grep -o "user_id=[0-9]*" access.log

The exit code problem in CI

# grep exit codes:
# 0 = at least one match found
# 1 = no matches (NOT an error — pattern just wasn't there)
# 2 = I/O error or invalid pattern

echo "anything" | grep "a"; echo $?   # → 0
echo "anything" | grep "b"; echo $?   # → 1 (no match)

In a CI pipeline script:

# PROBLEM: grep exits 1 → terminates the set -e script
set -e
set -o pipefail

cat deploy.log | grep "Deploy complete"  # script dies if "Deploy complete" not found
# FIX 1: || true — suppress exit code
cat deploy.log | grep "Deploy complete" || true

# FIX 2: check exit code explicitly
if grep -q "Deploy complete" deploy.log; then
    echo "Deployment succeeded"
else
    echo "Deploy complete not found"
fi

# FIX 3: count matches to use in conditionals
matches=$(grep -c "Deploy complete" deploy.log || true)
echo "Found $matches instances"

The || true pattern works but hides real errors (grep exit code 2). Use if grep for cases where "no match" should trigger different behavior.

CircleCI PIPESTATUS behavior differs between automation mode and SSH debug mode

GotchaShell / CI

In CircleCI's automated workflow, each step runs in a clean shell with errexit (set -e) behavior. The pipeline exit code affects the step result. In SSH debug mode (rerun with SSH), the shell is interactive and set -e is not active by default — commands that fail in automation may succeed in SSH debugging because the exit code behavior differs. This is why `grep pattern | wc -l` returns exit code 0 in SSH debug but terminates the workflow in automation: the pipeline exit code is 0 in interactive mode (wc exits 0), but pipefail propagates grep's exit code 1 in the workflow.

Prerequisites

  • set -e behavior
  • PIPESTATUS
  • Pipeline exit codes

Key Points

  • grep exits 1 for no match — this is normal behavior, not an error.
  • set -o pipefail: pipeline exit code = first non-zero stage. grep | wc -l exits 1 if grep finds nothing.
  • || true: makes the combined expression always succeed — use when no-match is acceptable.
  • grep -q: quiet mode (no output), just sets exit code — clean for conditional checks.

Useful grep combinations

# Find files modified in last 24h containing "error"
find . -mtime -1 -name "*.log" -exec grep -l "error" {} +

# Extract unique IP addresses from access log
grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" access.log | sort -u

# Count errors by type
grep -oE "ERROR|WARN|FATAL" application.log | sort | uniq -c | sort -rn

# Find lines with both patterns (AND)
grep "user_id=123" access.log | grep "POST"

# Exclude binary files and .git
grep -r --exclude-dir=.git --binary-files=without-match "TODO" .

In a CircleCI job, `cat app.log | grep 'started' | wc -l` returns '0' in SSH debug mode but the job fails in automation with exit code 1. Same command, same log file. Why?

medium

grep exits 1 when no matches are found. set -o pipefail is active in CircleCI automation but not in the SSH debug shell.

  • AThe log file is different in automation vs SSH debug
    Incorrect.Both use the same log file from the same run. The difference is in the shell configuration, not the input.
  • BIn SSH debug mode, the interactive shell doesn't have pipefail set. grep's exit code 1 (no match) is silently ignored by wc. In automation, pipefail propagates grep's exit code 1 as the pipeline result, failing the step.
    Correct!CircleCI automation runs steps with set -o pipefail active. grep finding no matches exits 1. With pipefail, the pipeline exit code is 1 (grep's non-zero code), not 0 (wc's code). The step fails. In SSH debug, the interactive bash shell doesn't have pipefail — wc's exit code 0 is the pipeline result. The command completes successfully. Fix: append `|| true` to suppress grep's exit code — `grep 'started' | wc -l || true`.
  • Cwc -l behaves differently on empty input in automation vs interactive mode
    Incorrect.wc -l on empty input outputs '0' and exits 0 in all contexts. The issue is grep's exit code propagating through pipefail.
  • DCircleCI uses a different grep version that has different exit codes
    Incorrect.grep exit codes (0=match, 1=no match, 2=error) are standardized in POSIX. CircleCI uses standard Linux tools.

Hint:What does set -o pipefail do to a pipeline where grep exits 1 and wc exits 0?