Skip to content

Text Processing

Regular expressions are the backbone of text processing on Linux. Three major flavors exist, each With different capabilities and syntax.

| Flavor | Engine | Activator | Metacharacters Require Escape | Lookaround | | ------ | --------------- | ------------------ | ----------------------------- | ---------- | --- | | BRE | POSIX grep | Default | +?{`` |() | No | | ERE | POSIX grep -E | grep -E``egrep | None | No | | PCRE | Perl-compatible | grep -P``ripgrep | None | Yes |

BRE: \{1,3\} \+ \? \(group\)
ERE: {1,3} + ? (group)
PCRE: {1,3} + ? (group) + lookaround + backreferences + named groups
Character Classes:
[abc] any of a, b, c
[a-z] lowercase letters
[^0-9] NOT a digit
[[:alpha:]] POSIX class — any letter
[[:digit:]] POSIX class — any digit
[[:alnum:]] letters or digits
[[:space:]] whitespace
[[:upper:]] uppercase letters
[[:lower:]] lowercase letters
Anchors:
^ start of line (or start of string in multiline mode)
$ end of line
\b word boundary
\B non-word boundary
\< start of word (GNU extension)
\> end of word (GNU extension)
Greedy (match as much as possible):
* zero or more
+ one or more
? zero or one
{n} exactly n
{n,} n or more
{n,m} between n and m
Lazy (match as little as possible — PCRE only):
*? zero or more (lazy)
+? one or more (lazy)
?? zero or one (lazy)
Terminal window
# Positive lookahead — match "foo" only when followed by "bar"
grep -P "foo(?=bar)' file.txt
# Negative lookahead — match "foo" only when NOT followed by "bar"
grep -P 'foo(?!bar)' file.txt
# Positive lookbehind — match "bar" only when preceded by "foo"
grep -P '(?<=foo)bar' file.txt
# Negative lookbehind — match "bar" only when NOT preceded by "foo"
grep -P '(?<!foo)bar' file.txt
Terminal window
# IPv4 address
grep -P '(\d{1,3}\.){3}\d{1,3}' file.txt
# Email address (basic)
grep -P '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' file.txt
# MAC address
grep -P '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' file.txt
# UUID
grep -P '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' file.txt
# ISO 8601 date
grep -P '\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?' file.txt
# Semantic version
grep -P '\bv[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?\b' file.txt

sed reads input line by line, applies editing commands, and writes output. It operates on a pattern space (a working buffer holding the current line) and a hold space (a secondary Buffer for multi-line operations).

Terminal window
# Replace first occurrence per line
sed 's/old/new/' file.txt
# Replace all occurrences per line
sed 's/old/new/g' file.txt
# Replace on lines matching a pattern
sed '/error/s/warning/ERROR/g' file.txt
# Replace on specific line numbers
sed '3s/foo/bar/' file.txt
# Replace from line 3 to 5
sed '3,5s/foo/bar/' file.txt
# Replace from line 3 to end
sed '3,$s/foo/bar/' file.txt
Terminal window
# Line numbers
sed -n '10,20p' file.txt # print lines 10-20
sed '1d' file.txt # delete first line
sed '$d' file.txt # delete last line
# Pattern ranges
sed '/start/,/end/d' file.txt # delete block between markers
sed '/ERROR/,+3d' file.txt # delete ERROR line and 3 following lines
# Step ranges
sed '1~2d' file.txt # delete every 2nd line (odd lines)
sed '0~3d' file.txt # delete every 3rd line (lines 3, 6, 9...)
# Regex with flags
sed -n '/^#.*enabled/Ip' config # case-insensitive, print matching

The hold space is a secondary buffer. It persists across lines, enabling multi-line transformations.

Terminal window
# Copy pattern space to hold space (h), append (H)
# Get hold space to pattern space (g), append (G)
# Exchange pattern and hold space (x)
# Reverse the order of lines in a file
sed '1!G;h;$!d' file.txt
# Join every two lines into one
sed 'N;s/\n/ /' file.txt
# Delete blank lines and join previous line with next
sed '/^$/N;/\n$/d' file.txt
# Double-space a file
sed G file.txt
# Print the line AFTER a pattern match
sed -n '/pattern/{n;p}' file.txt
Terminal window
# Label and branch
sed '/start/b skip; s/foo/bar/; :skip' file.txt
# Conditional branch — skip substitution on comment lines
sed '/^#/b; s/enabled/disabled/' config
# Loop with t (branch if substitution was made)
# Remove all leading spaces (not tabs) — one at a time
sed ':loop; s/^ //; t loop' file.txt
# Infinite loop with break
sed ':top; s/ / /; t top' file.txt # collapse multiple spaces to one

For complex operations, use a sed script file:

Terminal window
cat > edit.sed << 'EOF'
# Comment lines are ignored by sed
/^#/d
s/TODO/FIXME/g
1,10s/enabled/disabled/
/^Listen /s/80/8080/
$ a \
# End of processed file
EOF
sed -f edit.sed httpd.conf
Terminal window
# Create backup with .bak extension
sed -i.bak 's/old/new/g' file.txt
# In-place without backup (dangerous — no recovery)
sed -i 's/old/new/g' file.txt
# Operate on multiple files
sed -i 's/192.168.1.100/10.0.0.1/g' /etc/hosts /etc/resolv.conf

Prefer cut when you only need simple field extraction — it is significantly faster than awk for Large files. Use awk when you need conditional logic, field manipulation, or aggregation.

Terminal window
# Merge two files side by side (tab-separated)
paste file1.txt file2.txt
# Merge with a different delimiter
paste -d',' file1.txt file2.txt
# Paste files sequentially (not side by side)
paste -s file.txt # all lines on one line
# Paste multiple files
paste file1.txt file2.txt file3.txt
Terminal window
# Inner join on first field (files must be sorted)
join file1.txt file2.txt
# Join on specific fields
join -1 2 -2 1 file1.txt file2.txt # field 2 of file1, field 1 of file2
# Unpaired lines
join -a 1 file1.txt file2.txt # show unpaired from file1 (left join)
join -a 2 file1.txt file2.txt # show unpaired from file2 (right join)
join -a 1 -a 2 file1.txt file2.txt # full outer join
# Auto-fill empty fields
join -e 'N/A' file1.txt file2.txt
# Specify output format
join -o '1.1 2.2' file1.txt file2.txt
Terminal window
# Translate characters
echo 'hello' | tr 'a-z' 'A-Z' # HELLO
echo 'Hello World' | tr '[:lower:]' '[:upper:]'
# Delete characters
echo 'hello 123' | tr -d '0-9' # hello
echo 'hello' | tr -d 'l' # heo
# Complement and delete
echo 'hello123' | tr -d -c 'a-z' # hello (delete everything except a-z)
# Squeeze repeats
echo 'heeello' | tr -s 'l' # heelo
# Translate to single character (all spaces to newlines)
echo 'one two three' | tr ' ' '\n'
# Convert DOS line endings to Unix
tr -d '\r' < file.txt > file_unix.txt
# Remove non-printable characters
tr -cd '[:print:]\n' < file.txt
# ROT13
echo 'secret' | tr 'a-zA-Z' 'n-za-mN-ZA-M'
Terminal window
# WRONG — BRE requires escaping quantifiers
sed 's/[0-9]+/NUMBER/g' file.txt # matches "[0-9]" followed by literal "+"
# CORRECT — escape the quantifier in BRE
sed 's/[0-9]\+/NUMBER/g' file.txt # matches one or more digits
# ALTERNATIVE — use ERE with -E flag
sed -E 's/[0-9]+/NUMBER/g' file.txt # matches one or more digits
Terminal window
# awk uses double-precision floating point — expect rounding errors
awk 'BEGIN {print 0.1 + 0.2}' # outputs 0.3 (display rounding)
awk 'BEGIN {printf "%.20f\n", 0.1 + 0.2}' # outputs 0.30000000000000004441
# For financial data, use integer arithmetic (cents)
awk '{printf "%d.%02d\n", $1/100, $1%100}' transactions.txt

Pitfall: grep Returning Non-Zero on No Match

Section titled “Pitfall: grep Returning Non-Zero on No Match”
Terminal window
# grep exits with 1 when no lines match — this breaks set -e scripts
set -e
grep "pattern" file.txt # script exits if no match!
# Fix: use || true
grep "pattern" file.txt || true
# Fix: use --quiet with a test
if grep -q "pattern" file.txt; then
echo "found"
fi
Terminal window
# jq loads the entire JSON into memory
jq '.' huge.json # may OOM on multi-gigabyte files
# For large files, use streaming mode
jq -c '.[]' huge.json | while read -r obj; do
echo "$obj" | jq '.id'
done
# Or use jq's --stream flag for very large files
jq --stream 'fromstream(1|truncate_stream(inputs))' huge.json

Pitfall: sed with Paths Containing Delimiters

Section titled “Pitfall: sed with Paths Containing Delimiters”
Terminal window
# WRONG — slashes in the path break the sed command
sed 's|/old/path|/new/path|g' file.txt # works if using | as delimiter
# CORRECT — use a different delimiter
sed 's|/old/path|/new/path|g' file.txt
# CORRECT — escape the delimiter
sed 's/\/old\/path/\/new\/path/g' file.txt
# Use | or # or @ as delimiter when paths contain /
sed 's#/var/log#/opt/log#g' config
Terminal window
# Default sort uses locale — order may be unexpected
sort file.txt # 'A' may sort after 'z' depending on locale
# Fix: use C locale for byte-order sorting
LC_ALL=C sort file.txt
# Fix: set LC_ALL=C for the duration of a pipeline
export LC_ALL=C; sort file.txt | uniq

Pitfall: cut Does Not Handle Multi-Character Delimiters

Section titled “Pitfall: cut Does Not Handle Multi-Character Delimiters”
Terminal window
# cut only accepts single-character delimiters
cut -d'|' file.txt # works — single character
cut -d'||' file.txt # WRONG — uses only the first '|'
# Fix: use awk instead
awk -F'\\|\\|' '{print $1, $2}' file.txt

Pitfall: In-Place sed on Files with No Write Permission

Section titled “Pitfall: In-Place sed on Files with No Write Permission”
Terminal window
# sed -i creates a temporary file, then renames it
# If the target directory is read-only, this fails silently or partially
sed -i 's/old/new/g' /readonly/file.txt # may fail
# Fix: write to a temp directory
sed "s/old/new/g" /readonly/file.txt > /tmp/file.txt && sudo cp /tmp/file.txt /readonly/file.txt

This topic covers the core concepts of text processing, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • variables, data types, and control flow
  • functions and procedures
  • object-oriented programming
  • error handling and debugging
  • modular design

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.