grep and pattern matching

Quote

“Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.”

  • Jamie Zawinski, alt.religion.emacs post (1997)

grep remains the default text filter in Unix-like shells because it is cheap to compose with pipes, predictable on plain text, and already present on almost every Linux system. Select-String serves the same operational role in PowerShell, but it uses the .NET regex engine and returns objects rather than plain text lines.

Some regex ideas transfer to Python’s re module and C#‘s Regex class, but the engines in this note are not interchangeable. Plain grep uses POSIX BRE, grep -E uses ERE, GNU grep -P adds Perl-style features, rg uses Rust regex by default, and Select-String uses .NET regex. While grep finds matches, sed-stream-editing complements it by editing matching streams.

Demo fixtures

Run the fixture block for your platform once before reusing the later commands. The outputs in this note were captured live from these disposable trees.

Bash/Linux | demo fixtures

Use a temporary directory so the examples stay isolated from real logs and source trees.

Build a disposable grep demo tree

Run the commands in this section to build a disposable grep demo tree.

tmp=/tmp/elysium-grep-demo
rm -rf "$tmp"
mkdir -p "$tmp/project/src" "$tmp/project/logs" "$tmp/secrets"
 
cat > "$tmp/app.log" <<'EOF'
2026-04-14 09:00:00 INFO bootstrap complete
2026-04-14 09:01:00 WARN cache warming
2026-04-14 09:02:00 ERROR payment timeout
2026-04-14 09:03:00 ERROR disk full
2026-04-14 09:04:00 INFO retry scheduled
EOF
 
cp "$tmp/app.log" "$tmp/project/logs/app.log"
 
cat > "$tmp/notes.txt" <<'EOF'
log
logfile
catalog
blog
EOF
 
cat > "$tmp/config.ini" <<'EOF'
# sample config
mode=prod
log_level=DEBUG
EOF
 
cat > "$tmp/ip.txt" <<'EOF'
client=10.132.0.2
client=10X132Y0Z2
EOF
 
cat > "$tmp/users.txt" <<'EOF'
user=alice user=bob
EOF
 
cat > "$tmp/project/src/app.py" <<'EOF'
# TODO: remove fallback
print("ok")
EOF
 
cat > "$tmp/project/src/test_app.py" <<'EOF'
# TODO: keep in tests
print("test")
EOF
 
cat > "$tmp/project/src/query.sql" <<'EOF'
SELECT * FROM raw.events JOIN mart.sales ON 1=1;
EOF
 
cat > "$tmp/secrets/app.env" <<'EOF'
password=topsecret
EOF
 
cat > "$tmp/secrets/readme.txt" <<'EOF'
no secret here
EOF
 
gzip -c "$tmp/app.log" > "$tmp/app.log.gz"
 
find "$tmp" -maxdepth 3 -type f | sort
/tmp/elysium-grep-demo/app.log
/tmp/elysium-grep-demo/app.log.gz
/tmp/elysium-grep-demo/config.ini
/tmp/elysium-grep-demo/ip.txt
/tmp/elysium-grep-demo/notes.txt
/tmp/elysium-grep-demo/project/logs/app.log
/tmp/elysium-grep-demo/project/src/app.py
/tmp/elysium-grep-demo/project/src/query.sql
/tmp/elysium-grep-demo/project/src/test_app.py
/tmp/elysium-grep-demo/secrets/app.env
/tmp/elysium-grep-demo/secrets/readme.txt
/tmp/elysium-grep-demo/users.txt

PowerShell | demo fixtures

Mirror the same tree under $env:TEMP so the PowerShell and findstr examples use disposable data instead of real files.

Build a disposable Select-String demo tree

Run the commands in this section to build a disposable Select-String demo tree.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue
 
New-Item -ItemType Directory -Path (Join-Path $tmp 'project\src') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $tmp 'project\logs') -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $tmp 'secrets') -Force | Out-Null
 
@(
    '2026-04-14 09:00:00 INFO bootstrap complete',
    '2026-04-14 09:01:00 WARN cache warming',
    '2026-04-14 09:02:00 ERROR payment timeout',
    '2026-04-14 09:03:00 ERROR disk full',
    '2026-04-14 09:04:00 INFO retry scheduled'
) | Set-Content -LiteralPath (Join-Path $tmp 'app.log')
 
Copy-Item -LiteralPath (Join-Path $tmp 'app.log') -Destination (Join-Path $tmp 'project\logs\app.log')
 
@('log', 'logfile', 'catalog', 'blog') |
    Set-Content -LiteralPath (Join-Path $tmp 'notes.txt')
 
@('# sample config', 'mode=prod', 'log_level=DEBUG') |
    Set-Content -LiteralPath (Join-Path $tmp 'config.ini')
 
@('client=10.132.0.2', 'client=10X132Y0Z2') |
    Set-Content -LiteralPath (Join-Path $tmp 'ip.txt')
 
@('user=alice user=bob') |
    Set-Content -LiteralPath (Join-Path $tmp 'users.txt')
 
@('# TODO: remove fallback', 'print("ok")') |
    Set-Content -LiteralPath (Join-Path $tmp 'project\src\app.py')
 
@('# TODO: keep in tests', 'print("test")') |
    Set-Content -LiteralPath (Join-Path $tmp 'project\src\test_app.py')
 
@('SELECT * FROM raw.events JOIN mart.sales ON 1=1;') |
    Set-Content -LiteralPath (Join-Path $tmp 'project\src\query.sql')
 
@('password=topsecret') |
    Set-Content -LiteralPath (Join-Path $tmp 'secrets\app.env')
 
@('no secret here') |
    Set-Content -LiteralPath (Join-Path $tmp 'secrets\readme.txt')
 
$gzip = Join-Path $tmp 'app.log.gz'
$fs = [System.IO.File]::Create($gzip)
$gzipStream = New-Object System.IO.Compression.GzipStream(
    $fs,
    [System.IO.Compression.CompressionMode]::Compress
)
$writer = New-Object System.IO.StreamWriter($gzipStream)
Get-Content -LiteralPath (Join-Path $tmp 'app.log') | ForEach-Object {
    $writer.WriteLine($_)
}
$writer.Dispose()
 
Get-ChildItem -LiteralPath $tmp -Recurse -File |
    Sort-Object FullName |
    Select-Object -ExpandProperty FullName
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\app.log
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\app.log.gz
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\config.ini
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\ip.txt
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\notes.txt
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\project\logs\app.log
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\project\src\app.py
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\project\src\query.sql
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\project\src\test_app.py
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\secrets\app.env
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\secrets\readme.txt
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\users.txt

Basic matching

These are the day-one search operations: exact line filtering, case handling, whole-word matching, counting, filename-only output, and inverted matches.

Bash/Linux | basic matching

Use plain grep first, then add flags only when the behavior needs to change.

Find ERROR lines in one file

Use plain grep when the pattern is already case-correct and the file set is explicit.

Run the commands in this section to find ERROR lines in one file.

tmp=/tmp/elysium-grep-demo
grep 'ERROR' "$tmp/app.log"
2026-04-14 09:02:00 ERROR payment timeout
2026-04-14 09:03:00 ERROR disk full

Ignore case when the log level changes

-i keeps the search stable when the source mixes uppercase and lowercase log tokens. -n is added here because case-insensitive searches are usually followed by navigation.

Run the commands in this section to ignore case when the log level changes.

grep -in 'error' '/tmp/elysium-grep-demo/app.log'
3:2026-04-14 09:02:00 ERROR payment timeout
4:2026-04-14 09:03:00 ERROR disk full

Match log but not logfile

-w forces a whole-word match, which matters when the token can appear as a substring inside longer identifiers.

Run the commands in this section to match log but not logfile.

tmp=/tmp/elysium-grep-demo
grep -w 'log' "$tmp/notes.txt"
log

Count matching lines

-c reports the number of matching lines, not the number of individual match occurrences on each line.

Run the commands in this section to count matching lines.

tmp=/tmp/elysium-grep-demo
grep -c 'ERROR' "$tmp/app.log"
2

List only the files that contain a match

-l is safer than printing the matching lines when the content itself may be noisy or sensitive.

Run the commands in this section to list only the files that contain a match.

tmp=/tmp/elysium-grep-demo
grep -l 'ERROR' "$tmp/app.log" "$tmp/project/logs/app.log"
/tmp/elysium-grep-demo/app.log
/tmp/elysium-grep-demo/project/logs/app.log

Remove noisy DEBUG lines

-v inverts the match and leaves only the lines that do not contain the pattern.

Run the commands in this section to remove noisy DEBUG lines.

tmp=/tmp/elysium-grep-demo
grep -v 'DEBUG' "$tmp/config.ini"
# sample config
mode=prod

PowerShell | basic matching

Select-String covers the same operational surface, but it emits MatchInfo objects and is case-insensitive by default.

Find ERROR lines in one file

Project the MatchInfo object into predictable text when you want line numbers and the matched line without the default console emphasis.

Run the commands in this section to find ERROR lines in one file.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'ERROR' -Path (Join-Path $tmp 'app.log') |
    ForEach-Object { '{0}:{1}: {2}' -f $_.Filename, $_.LineNumber, $_.Line.Trim() }
app.log:3: 2026-04-14 09:02:00 ERROR payment timeout
app.log:4: 2026-04-14 09:03:00 ERROR disk full

Find ERROR regardless of case

This is the default behavior in PowerShell, which is the opposite of plain grep.

Run the commands in this section to find ERROR regardless of case.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'error' -Path (Join-Path $tmp 'app.log') |
    ForEach-Object { '{0}:{1}: {2}' -f $_.Filename, $_.LineNumber, $_.Line.Trim() }
app.log:3: 2026-04-14 09:02:00 ERROR payment timeout
app.log:4: 2026-04-14 09:03:00 ERROR disk full

Search pipeline input instead of a path

Select-String reads strings from the pipeline without losing regex support.

Run the commands in this section to search pipeline input instead of a path.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-Content -LiteralPath (Join-Path $tmp 'app.log') |
    Select-String 'WARN' |
    ForEach-Object { $_.Line.Trim() }
2026-04-14 09:01:00 WARN cache warming

Match log but not logfile

PowerShell has no -w switch, so the equivalent is an explicit word-boundary regex.

Run the commands in this section to match log but not logfile.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern '\blog\b' -Path (Join-Path $tmp 'notes.txt') |
    ForEach-Object { $_.Line }
log

Count matching lines

The direct count is the number of matching lines in the returned result set.

Run the commands in this section to count matching lines.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
(Select-String -Pattern 'ERROR' -Path (Join-Path $tmp 'app.log')).Count
2

List only the files that contain a match

Expand Path and deduplicate it when multiple matches may come from the same file.

Run the commands in this section to list only the files that contain a match.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'ERROR' -Path (Join-Path $tmp 'app.log'), (Join-Path $tmp 'project\logs\app.log') |
    Select-Object -ExpandProperty Path -Unique
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\app.log
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\project\logs\app.log

Remove noisy DEBUG lines

-NotMatch is the direct inverse filter for lines that should be excluded from the stream.

Run the commands in this section to remove noisy DEBUG lines.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-Content -LiteralPath (Join-Path $tmp 'config.ini') |
    Select-String -Pattern 'DEBUG' -NotMatch |
    ForEach-Object { $_.Line }
# sample config
mode=prod

Regex modes

The important split is not “regex or no regex.” The real split is which engine and which operator set you are using.


flowchart TD
    A["Need regex in grep?"] --> B{"Need +, ?, |, or grouping?"}
    B -->|No| C["BRE - plain grep"]
    B -->|Yes| D{"Need lookahead or lookbehind?"}
    D -->|No| E["ERE - grep -E"]
    D -->|Yes| F{"GNU grep with -P available?"}
    F -->|Yes| G["PCRE-style search - grep -P"]
    F -->|No| H["Use rg -P or another PCRE-capable tool"]

    style A fill:#292e42,stroke:#565f89,color:#c0caf5
    style C fill:#1a1b26,stroke:#565f89,color:#9ece6a
    style E fill:#1a1b26,stroke:#565f89,color:#9ece6a
    style G fill:#1a1b26,stroke:#565f89,color:#9ece6a
    style H fill:#1a1b26,stroke:#565f89,color:#e0af68

Bash/Linux | regex modes

On Linux, the critical distinction is between BRE, ERE, and GNU -P.

Alternate between ERROR and WARN

Plain grep 'ERROR|WARN' searches for the literal character |. Use grep -E 'ERROR|WARN' or the BRE form grep 'ERROR\|WARN' when alternation is required.

Run the commands in this section to alternate between ERROR and WARN.

grep -n 'ERROR|WARN' '/tmp/elysium-grep-demo/app.log' || echo 'no match'
no match

Run the commands in this section to alternate between ERROR and WARN.

grep -nE 'ERROR|WARN' '/tmp/elysium-grep-demo/app.log'
2:2026-04-14 09:01:00 WARN cache warming
3:2026-04-14 09:02:00 ERROR payment timeout
4:2026-04-14 09:03:00 ERROR disk full

-o is useful when downstream commands should work on the captured token instead of the full line.

Run the commands in this section to print only the matching level tokens.

tmp=/tmp/elysium-grep-demo
grep -oE 'ERROR|WARN' "$tmp/app.log"
WARN
ERROR
ERROR

Extract user= values with GNU grep PCRE

GNU grep -P supports lookbehind and other Perl-style constructs, but do not assume that option exists on every non-GNU build.

Run the commands in this section to extract user= values with GNU grep PCRE.

tmp=/tmp/elysium-grep-demo
grep -Po '(?<=user=)\w+' "$tmp/users.txt"
alice
bob

The following POSIX classes remain useful for portable grep and grep -E patterns.

POSIX character classes

ClassMeaningExample
[[:digit:]]Decimal digits^[[:digit:]]+$
[[:alpha:]]Letters in the current locale^[[:alpha:]]+$
[[:alnum:]]Letters or digits^[[:alnum:]_]+$
[[:space:]]Whitespace^[[:space:]]+
[[:upper:]]Uppercase letters^[[:upper:]]

PowerShell | regex modes

PowerShell uses the .NET regex engine. Grouping, alternation, lookahead, and lookbehind are available, but that does not make it identical to POSIX grep or PCRE.

Match both ERROR and WARN with multiple patterns

Passing a string array to -Pattern is the closest Select-String equivalent to repeated grep -e.

Run the commands in this section to match both ERROR and WARN with multiple patterns.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'ERROR', 'WARN' -Path (Join-Path $tmp 'app.log') |
    ForEach-Object { '{0}:{1}: {2}' -f $_.Filename, $_.LineNumber, $_.Line.Trim() }
app.log:2: 2026-04-14 09:01:00 WARN cache warming
app.log:3: 2026-04-14 09:02:00 ERROR payment timeout
app.log:4: 2026-04-14 09:03:00 ERROR disk full

Return every user= value from one line

-AllMatches matters whenever multiple values can appear on the same input line.

Run the commands in this section to return every user= value from one line.

'user=alice user=bob' |
    Select-String -Pattern '(?<=user=)\w+' -AllMatches |
    ForEach-Object { $_.Matches.Value }
alice
bob

Recursive search and context

Once the search moves beyond a single file, the important controls are scope, context, and scriptability.

Bash/Linux | recursive search and context

Recursive grep is useful, but it should be narrowed deliberately so you do not read every file on disk by accident.

Search only Python files and skip test files

Combine -r with --include and --exclude when the directory tree contains mixed content.

Run the commands in this section to search only Python files and skip test files.

tmp=/tmp/elysium-grep-demo
grep -r --include='*.py' --exclude='test_*' 'TODO' "$tmp/project"
/tmp/elysium-grep-demo/project/src/app.py:# TODO: remove fallback

Show one line of context around a failure

-C 1 keeps the matching line together with its immediate neighbors.

Run the commands in this section to show one line of context around a failure.

tmp=/tmp/elysium-grep-demo
grep -C 1 'ERROR payment timeout' "$tmp/app.log"
2026-04-14 09:01:00 WARN cache warming
2026-04-14 09:02:00 ERROR payment timeout
2026-04-14 09:03:00 ERROR disk full

Verify a quiet check with its exit status

grep -q intentionally prints nothing, so the follow-up verification here is the exit code itself.

Run the commands in this section to verify a quiet check with its exit status.

tmp=/tmp/elysium-grep-demo
grep -q 'ERROR' "$tmp/app.log"
printf 'exit=%s\n' "$?"
exit=0

PowerShell | recursive search and context

PowerShell reaches the same outcome by composing Get-ChildItem and Select-String.

Search only Python files and skip test files

Filter the file list before Select-String so the match set stays clean.

Run the commands in this section to search only Python files and skip test files.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-ChildItem (Join-Path $tmp 'project') -Recurse -Filter *.py |
    Where-Object { $_.Name -notlike 'test_*' } |
    Select-String -Pattern 'TODO' |
    ForEach-Object { '{0}:{1}: {2}' -f $_.Path, $_.LineNumber, $_.Line.Trim() }
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\project\src\app.py:1: # TODO: remove fallback

Show one line of context around a failure

-Context 1,1 returns one line before and after each match.

Run the commands in this section to show one line of context around a failure.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'ERROR payment timeout' -Path (Join-Path $tmp 'app.log') -Context 1,1 |
    ForEach-Object { $_.Context.PreContext + $_.Line + $_.Context.PostContext }
2026-04-14 09:01:00 WARN cache warming
2026-04-14 09:02:00 ERROR payment timeout
2026-04-14 09:03:00 ERROR disk full

Return a Boolean instead of MatchInfo objects

Use -Quiet when the calling code only needs a true-or-false answer.

Run the commands in this section to return a Boolean instead of MatchInfo objects.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'ERROR' -Path (Join-Path $tmp 'app.log') -Quiet
True

Data engineering scenarios

These are the search patterns that show up in daily operations work: compressed logs, secret scans, report extraction, and minimal-tool fallbacks.

Bash/Linux | data engineering scenarios

The Linux side is usually strongest at stream processing and compressed-log search.

Search compressed logs without unpacking them

zgrep preserves the same search behavior while reading from a gzip-compressed file.

Run the commands in this section to search compressed logs without unpacking them.

tmp=/tmp/elysium-grep-demo
zgrep 'ERROR' "$tmp/app.log.gz"
2026-04-14 09:02:00 ERROR payment timeout
2026-04-14 09:03:00 ERROR disk full

Scan for candidate secret files without printing the secret

-l is the safer first pass because it reports only the filenames that need review.

Run the commands in this section to scan for candidate secret files without printing the secret.

tmp=/tmp/elysium-grep-demo
grep -rlE 'password|api_key' "$tmp/secrets"
/tmp/elysium-grep-demo/secrets/app.env

PowerShell | data engineering scenarios

The PowerShell side is strongest when the search result needs to feed a report or another cmdlet.

Project match objects into a report row set

Extract only the fields that are operationally useful and discard the rest of the object shape.

Run the commands in this section to project match objects into a report row set.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern '(ERROR|WARN)' -Path (Join-Path $tmp 'app.log') |
    Select-Object LineNumber,
        @{Name='Level'; Expression={ $_.Matches[0].Value }},
        @{Name='Message'; Expression={ $_.Line.Trim() }} |
    Format-Table -HideTableHeaders
 
         2 WARN  2026-04-14 09:01:00 WARN cache warming
         3 ERROR 2026-04-14 09:02:00 ERROR payment timeout
         4 ERROR 2026-04-14 09:03:00 ERROR disk full

Fall back to findstr when only cmd.exe tooling is available

findstr is far less capable than Select-String, but it remains useful in constrained Windows environments.

Run the commands in this section to fall back to findstr when only cmd.exe tooling is available.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
findstr /N /I "error" (Join-Path $tmp 'app.log')
3:2026-04-14 09:02:00 ERROR payment timeout
4:2026-04-14 09:03:00 ERROR disk full

Performance and alternatives

Literal search and recursive tree search are the two places where tool choice matters most. Use fixed-string mode when the token is literal, and switch to rg when the scope is an entire repository.


flowchart TD
    A["Text search task"] --> B{"Single file or pipe?"}
    B -->|Yes| C{"Literal token?"}
    C -->|Yes| D["grep -F or Select-String -SimpleMatch"]
    C -->|No| E["grep -E or Select-String regex"]
    B -->|No| F{"Repository-style recursive search?"}
    F -->|Yes| G["rg with globs and ignore rules"]
    F -->|No| H{"Need lookaround?"}
    H -->|Yes| I["grep -P or rg -P when available"]
    H -->|No| E

    style A fill:#292e42,stroke:#565f89,color:#c0caf5
    style D fill:#1a1b26,stroke:#565f89,color:#9ece6a
    style E fill:#1a1b26,stroke:#565f89,color:#9ece6a
    style G fill:#1a1b26,stroke:#565f89,color:#9ece6a
    style I fill:#1a1b26,stroke:#565f89,color:#e0af68

Bash/Linux | performance and alternatives

Literal tokens and recursive trees are where grep and rg diverge most clearly.

Search for a literal IP address

In regex mode, . means “any character.” Fixed-string mode avoids that overmatch.

Run the commands in this section to search for a literal IP address.

tmp=/tmp/elysium-grep-demo
grep '10.132.0.2' "$tmp/ip.txt"
client=10.132.0.2
client=10X132Y0Z2

Run the commands in this section to search for a literal IP address.

tmp=/tmp/elysium-grep-demo
grep -F '10.132.0.2' "$tmp/ip.txt"
client=10.132.0.2

Search Python files with ripgrep globs

rg is a better default for repository search because recursion is built in and glob filtering is concise.

Run the commands in this section to search Python files with ripgrep globs.

tmp=/tmp/elysium-grep-demo
rg --glob '*.py' --glob '!**/test_*' 'TODO' "$tmp/project"
/tmp/elysium-grep-demo/project/src/app.py:# TODO: remove fallback

PowerShell | performance and alternatives

The same distinction exists on Windows: use literal matching for literal tokens and rg for repository-style text search.

Search for a literal IP address

Regex mode and literal mode are different operations in Select-String, even when the pattern looks simple.

Run the commands in this section to search for a literal IP address.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-Content -LiteralPath (Join-Path $tmp 'ip.txt') |
    Select-String -Pattern '10.132.0.2' |
    ForEach-Object { $_.Line }
client=10.132.0.2
client=10X132Y0Z2

Run the commands in this section to search for a literal IP address.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-Content -LiteralPath (Join-Path $tmp 'ip.txt') |
    Select-String -Pattern '10.132.0.2' -SimpleMatch |
    ForEach-Object { $_.Line }
client=10.132.0.2

Search Python files with ripgrep globs

rg behaves the same way on Windows, so it is often the cleanest cross-platform recursive search tool.

Run the commands in this section to search Python files with ripgrep globs.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
rg --glob '*.py' --glob '!**/test_*' 'TODO' (Join-Path $tmp 'project')
C:\Users\aperi\AppData\Local\Temp\elysium-grep-demo\project\src\app.py:# TODO: remove fallback

These are the defaults worth carrying into scripts and incident-response sessions.

Keep the default choices simple and predictable.

Prefer fixed-string mode for literal tokens

If the token is an IP address, path, URL, or version string, the safest default is grep -F.

Literal mode disables regex parsing

GNU grep treats patterns as regular expressions unless you ask for fixed strings. -F makes punctuation such as ., ?, +, and | literal, which is the safer default for copied error text, IP addresses, paths, and version identifiers.

Run the commands in this section to prefer fixed-string mode for literal tokens.

tmp=/tmp/elysium-grep-demo
grep -F '10.132.0.2' "$tmp/ip.txt"
client=10.132.0.2

Prefer filename-only scans for secret hunting

Start with -l so the terminal does not become an accidental secret sink.

Run the commands in this section to prefer filename-only scans for secret hunting.

tmp=/tmp/elysium-grep-demo
grep -rlE 'password|api_key' "$tmp/secrets"
/tmp/elysium-grep-demo/secrets/app.env

On the PowerShell side, the most useful defaults are the literal mode and the Boolean mode.

Prefer -SimpleMatch for literal tokens

Use regex mode only when the wildcard behavior is intentional.

Regex is still the default

Select-String interprets -Pattern as regex unless -SimpleMatch is added. Use -SimpleMatch for exact text searches, and pair it with -Quiet when the calling code only needs a true-or-false result instead of MatchInfo objects.

Run the commands in this section to prefer -SimpleMatch for literal tokens.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-Content -LiteralPath (Join-Path $tmp 'ip.txt') |
    Select-String -Pattern '10.132.0.2' -SimpleMatch |
    ForEach-Object { $_.Line }
client=10.132.0.2

Prefer -Quiet when a script needs a Boolean

This keeps the branch condition explicit and avoids carrying full MatchInfo objects through a control-flow check.

Run the commands in this section to prefer -Quiet when a script needs a Boolean.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'ERROR' -Path (Join-Path $tmp 'app.log') -Quiet
True

Troubleshooting

Most grep failures come from using the wrong regex mode, the wrong case behavior, or regex when a literal search was intended.

Bash/Linux | troubleshooting

On Linux, the main failure mode is assuming ERE behavior while still using plain BRE grep.

grep 'ERROR|WARN' returns no matches

If the pattern contains |, +, ?, or grouping, plain grep is the first thing to inspect.

Run the commands in this section to grep 'ERROR|WARN' returns no matches.

grep -n 'ERROR|WARN' '/tmp/elysium-grep-demo/app.log' || echo 'no match'
no match

Run the commands in this section to grep 'ERROR|WARN' returns no matches.

grep -nE 'ERROR|WARN' '/tmp/elysium-grep-demo/app.log'
2:2026-04-14 09:01:00 WARN cache warming
3:2026-04-14 09:02:00 ERROR payment timeout
4:2026-04-14 09:03:00 ERROR disk full

A dotted pattern matches more lines than expected

Dots are regex wildcards. Switch to -F when the dots are literal punctuation.

Run the commands in this section to a dotted pattern matches more lines than expected.

tmp=/tmp/elysium-grep-demo
grep '10.132.0.2' "$tmp/ip.txt"
client=10.132.0.2
client=10X132Y0Z2

Run the commands in this section to a dotted pattern matches more lines than expected.

tmp=/tmp/elysium-grep-demo
grep -F '10.132.0.2' "$tmp/ip.txt"
client=10.132.0.2

PowerShell | troubleshooting

On Windows, the main surprises are the default case-insensitive behavior and the difference between regex mode and -SimpleMatch.

A ported grep check starts matching regardless of case

PowerShell is case-insensitive by default. Add -CaseSensitive when porting a plain grep check that should remain case-sensitive.

Run the commands in this section to a ported grep check starts matching regardless of case.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'error' -Path (Join-Path $tmp 'app.log') -Quiet
True

Run the commands in this section to a ported grep check starts matching regardless of case.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Select-String -Pattern 'error' -Path (Join-Path $tmp 'app.log') -CaseSensitive -Quiet
False

A literal search is still being parsed as regex

Select-String treats the pattern as regex unless -SimpleMatch is added.

Run the commands in this section to a literal search is still being parsed as regex.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-Content -LiteralPath (Join-Path $tmp 'ip.txt') |
    Select-String -Pattern '10.132.0.2' |
    ForEach-Object { $_.Line }
client=10.132.0.2
client=10X132Y0Z2

Run the commands in this section to a literal search is still being parsed as regex.

$tmp = Join-Path $env:TEMP 'elysium-grep-demo'
Get-Content -LiteralPath (Join-Path $tmp 'ip.txt') |
    Select-String -Pattern '10.132.0.2' -SimpleMatch |
    ForEach-Object { $_.Line }
client=10.132.0.2

Operational cautions

  • Plain grep is BRE. Use grep -E or escaped BRE operators when you need alternation or grouping.
  • grep -P is a GNU-specific feature and should not be assumed on every Unix-like system.
  • Select-String is case-insensitive by default. Add -CaseSensitive when you need grep-like default behavior.
  • Recursive grep can traverse binaries and unexpected subtrees. Use --include, --exclude, --exclude-dir, or --binary-files=without-match when the scope must stay tight.
  • rg is not “faster grep syntax.” Its default regex engine and ignore-file behavior differ from both POSIX grep and PowerShell.

Reference tables

Keep these tables for fast lookup. They summarize flags and equivalences that do not need full narrative treatment every time.

grep and PowerShell flag mapping

grepMeaningPowerShell equivalent
(none)Case-sensitive searchSelect-String -CaseSensitive
-iCase-insensitive searchDefault Select-String behavior
-FFixed-string search-SimpleMatch
-nShow line numbers.LineNumber in MatchInfo
-cCount matching lines(Select-String ...).Count
-lFiles with matchesSelect-Object -ExpandProperty Path -Unique
-LFiles without matchesSubtract matching paths from the full path set
-vInvert the match-NotMatch
-oPrint only the matching textForEach-Object { $_.Matches.Value }
-r / -RRecursive search`Get-ChildItem -Recurse
-C NN lines of context before and after-Context N,N
-A NN lines after-Context 0,N
-B NN lines before-Context N,0
-qQuiet Boolean check-Quiet
-e patMultiple explicit patterns-Pattern 'pat1', 'pat2'
-f fileRead patterns from a file-Pattern (Get-Content file)
-m NStop after N matchesSelect-Object -First N
-xMatch entire linesAnchor the pattern: '^pattern$'
--include='*.py'Include file globGet-ChildItem -Filter *.py
--exclude-dir=dirExclude a directoryFilter the path before Select-String

rg quick lookup

FlagSyntaxUse
-Frg -F 'literal'Fixed-string search
-nrg -n 'pattern'Show line numbers
-lrg -l 'pattern'Print matching files only
-crg -c 'pattern'Count matching lines per file
-g / --globrg --glob '*.py' 'pattern'Include or exclude files by glob
-Prg -P 'pattern'Use PCRE-style regex when the build supports it
-U / --multilinerg -U 'pattern'Enable multiline search
--hiddenrg --hidden 'pattern'Include hidden files
--no-ignorerg --no-ignore 'pattern'Ignore .gitignore and related ignore files
--jsonrg --json 'pattern'Emit machine-readable JSON events

Tool selection: grep vs rg

Concerngreprg
Default recursionNoYes
Ignore-file awarenessNoYes
Fixed-string mode-F-F
POSIX-style regex behaviorYesNo
PCRE-style features-P on GNU builds that support it-P when PCRE support is available
Multiline searchLine-oriented-U / --multiline
Best fitSingle files, pipes, and small explicit path setsRepository trees and developer search workflows

findstr quick lookup

FlagSyntaxUse
/Ifindstr /I "pat" fileCase-insensitive search
/Nfindstr /N "pat" filePrefix each match with its line number
/Sfindstr /S "pat" *.extRecurse through subdirectories
/Rfindstr /R "pat" fileRegex search with findstr’s limited engine
/Wfindstr /W "pat" fileMatch whole words
/Vfindstr /V "pat" fileInvert the match
/G:filefindstr /G:patterns.txt fileRead patterns from a file
/C:"text"findstr /C:"literal text" fileTreat the full string as one literal pattern
/Mfindstr /M "pat" *.extPrint matching filenames only

Cross-references