jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Bash & Shell Scripting · Part 8 — Text Processing: grep, sed & awk

grep, sed, and awk for shell text processing: flags, substitution, field parsing, cut/sort/uniq/tr/wc pipelines — bilingual, with real log pipelines and exercises.

Đây là Phần 8 của series 10 bài về viết shell script chắc chắn. Bạn đã biết redirection và pipe từ Phần 7. Giờ ta dùng các pipe đó với bộ ba xử lý textgrep, sed, và awk — cùng các công cụ nhỏ hoàn thiện mọi pipeline thực tế.

Log, export CSV, file config, và response API đều là dòng text. Shell không parse JSON sẵn, nhưng rất giỏi cắt, lọc, và biến đổi dữ liệu theo dòng với tốc độ cao. Nắm các công cụ này là bạn trả lời được “IP nào truy cập nhiều nhất?”, “tổng chi tiêu là bao nhiêu?”, và “bỏ field này khỏi mọi dòng?” mà không rời terminal.


Mô hình xử lý text

Hãy tưởng pipeline như băng chuyền các dòng. Mỗi công cụ làm một việc trên mỗi dòng (hoặc trên cả luồng) rồi chuyển kết quả sang công cụ tiếp theo:

Công cụNhiệm vụInput điển hình
greplọc — giữ dòng khớp patternlog files, config grep
sedbiến đổi — thay thế, xóa, in khoảng dòngbulk find-replace, strip comments
awkphân tích & tính — tách field, cộng cột, báo cáoCSV, access logs, tabular data
cut / sort / uniq / tr / wchỗ trợ — lấy cột, sắp xếp, đếm, chuyển đổi, đoprep before awk or after grep
cat log.txt whole file | grep ERROR keep matches | sort order them | uniq -c count dupes each | sends the left command's stdout into the right command's stdin
A multi-stage pipeline: each stage reads lines from stdin, processes them, and writes to stdout for the next command

Sơ đồ thể hiện mẫu kinh điển: đọc file, grep để thu hẹp, sort để nhóm, uniq -c để đếm, rồi head lấy top N. Một chuỗi đó trả lời hàng chục câu hỏi vận hành trên server thật.


grep — lọc dòng theo pattern

grep in các dòng khớp biểu thức chính quy (hoặc chuỗi cố định). Gần như luôn là giai đoạn đầu khi bạn cần thu hẹp file lớn.

Khớp cơ bản

# Keep lines containing "ERROR" (case-sensitive)
grep ERROR app.log

# Search multiple files
grep "connection refused" /var/log/syslog app.log

# No match → exit code 1 (useful in scripts with set -e if you expect hits)
grep "TODO" src/**/*.sh

Các flag dùng hàng ngày

# -i  case-insensitive
grep -i error app.log

# -v  invert — print lines that do NOT match
grep -v '^#' nginx.conf          # drop comment lines
grep -v DEBUG app.log            # hide DEBUG noise

# -n  show line numbers
grep -n "OutOfMemory" app.log

# -c  count matching lines (not the matches inside a line)
grep -c ERROR app.log

# -o  only the matching part (great with -E for extracting tokens)
grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log   # pull IPv4 addresses

# -r  recursive through directories
grep -r "API_KEY" ./config/

# -E  extended regex (|, +, ?, groups) — same as egrep
grep -E 'ERROR|WARN|FATAL' app.log
grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}' events.log    # lines starting with a date

Mẹo: kết hợp -r với --include để tránh tìm trong file nhị phân.

grep -r --include='*.sh' 'set -euo pipefail' .

Luôn quote pattern để shell không expand *, ?, hay $ trước khi grep nhận:

grep 'user=$' config.env    # correct — $ is literal end-of-line in regex
grep user=$ config.env      # wrong — shell expands $ to empty or a variable

sed — biên tập luồng để biến đổi dòng

sed đọc input từng dòng, áp lệnh chỉnh sửa, rồi ghi ra stdout. Đây là công cụ hàng đầu cho thay thế hàng loạt và phẫu thuật dòng nhẹ.

Thay thế: s/old/new/

# Replace first occurrence per line
echo 'foo bar foo' | sed 's/foo/baz/'

# Global — all occurrences on each line
echo 'foo bar foo' | sed 's/foo/baz/g'

# Use a different delimiter when the pattern contains /
echo 'path /usr/local/bin' | sed 's|/usr/local|/opt|g'

# Capture groups (extended regex with -E / -r)
echo 'user=alice' | sed -E 's/user=([a-z]+)/name=\1/'

Chỉnh sửa tại chỗ: -i

# GNU/Linux (most distros) — edits the file directly
sed -i 's/debug=false/debug=true/' config.properties

# macOS / BSD — REQUIRES a backup extension (even empty '')
sed -i '' 's/debug=false/debug=true/' config.properties

# Portable pattern: never use -i in scripts that must run everywhere;
# write to a temp file instead
sed 's/old/new/g' input.txt > input.txt.tmp && mv input.txt.tmp input.txt

Cảnh báo: sed -i khác nhau giữa GNU sed (Linux) và BSD sed (macOS). Trên macOS, sed -i 's/x/y/' file tạo backup tên file với suffix literal — gần như không bao giờ là điều bạn muốn. Test trên OS đích hoặc tránh -i trong script portable.

Xóa và in khoảng dòng

# Delete lines matching a pattern
sed '/^#/d' nginx.conf              # remove comment lines
sed '/^$/d' file.txt                # remove blank lines

# Delete a line range (lines 2 through 5)
sed '2,5d' report.txt

# Print only lines 10–20 (suppress default print with -n)
sed -n '10,20p' huge.log

# Print only lines matching a pattern
grep -n ERROR app.log | sed -n '1,5p'   # first 5 ERROR hits with numbers from grep

sed tỏa sáng trong pipeline khi bạn cần biến đổi nhanh mà không viết script:

# Normalize log level to uppercase in flight
grep ERROR app.log | sed -E 's/(ERROR|WARN|INFO)/\U\1/'
# Note: \U is GNU sed; on BSD use a different approach or perl

awk — pattern, field, và chương trình nhỏ

awkngôn ngữ nhỏ cho dữ liệu theo dòng. Mọi chương trình awk gồm khối pattern \{ action \}: khi dòng khớp pattern, awk chạy action. Pattern rỗng nghĩa là “mọi dòng”.

Field: $1, $2, $NF

Mặc định awk tách mỗi dòng theo khoảng trắng thành field $1, $2, … $NF (field cuối):

# access.log style: IP method path status
# 192.168.1.5 GET /api/users 200

awk '{ print $1 }' access.log           # first field — client IP
awk '{ print $NF }' access.log          # last field — status code
awk '{ print $(NF-1) }' access.log      # second-to-last — path segment trick

Biến sẵn: NR, NF

# NR = record (line) number, NF = number of fields on this line
awk '{ print NR, NF, $0 }' data.txt

# Print line 50 only
awk 'NR == 50' data.txt

# Skip the header row
awk 'NR > 1' report.csv

Dấu phân cách field tùy chỉnh: -F

# CSV with comma separator
awk -F',' '{ print $1, $3 }' sales.csv

# /etc/passwd uses colon
awk -F: '{ print $1, $6 }' /etc/passwd   # username and home dir

# Multiple characters as FS (GNU awk)
awk -F'[ ,]+' '{ print $2 }' messy.txt

Cộng và gộp một cột

# Sum the third column (amount) in a simple CSV: id,name,amount
awk -F',' 'NR > 1 { sum += $3 } END { print sum }' sales.csv

# Count rows and average
awk '{ total += $2; count++ } END { printf "avg=%.2f (n=%d)\n", total/count, count }' numbers.txt

# Count HTTP status codes from column 9 in combined log format
awk '{ codes[$9]++ } END { for (c in codes) print codes[c], c }' access.log

In cột cụ thể có định dạng

# Aligned columns from ps output
ps aux | awk '{ printf "%-8s %5s %s\n", $1, $3, $11 }'

# Extract date and IP from a structured log line
awk '/ERROR/ { print $1, $4 }' app.log

Khối BEGIN và END

# BEGIN runs before any input; END runs after all input
awk 'BEGIN { print "ip,hits" }
     { ips[$1]++ }
     END { for (ip in ips) print ip, ips[ip] }' access.log

awk thường rõ ràng hơn nối cut khi field độ rộng thay đổi hoặc bạn cần logic theo dòng.


Bộ công cụ hỗ trợ

Các lệnh này nhỏ, nhanh, và xuất hiện trong gần như mọi pipeline thật:

LệnhMục đíchFlag chính
cutlấy cột theo vị trí ký tự hoặc delimiter-d',' -f1,3 · -c1-10
sortsắp xếp dòng-n numeric · -r reverse · -k2 key field
uniqgộp dòng trùng liền kề-c count · requires sorted input
trchuyển hoặc xóa ký tựtr 'a-z' 'A-Z' · tr -d '\r'
wcđếm từ/dòng/byte-l lines · -w words · -c bytes
head / tailN dòng đầu / cuối-n 20 · tail -f follow
findtìm file theo tên, loại, mtime-name '*.log' · -mtime -1
xargsghép lệnh từ dòng stdin-0 with find -print0 · -I replace token
# cut — fixed delimiter (breaks if fields contain the delimiter)
cut -d',' -f1,3 report.csv

# sort — numeric, reverse, by second field
sort -t',' -k2 -n sales.csv
sort -rn amounts.txt          # -n numeric, -r reverse

# uniq — MUST sort first
sort names.txt | uniq
sort names.txt | uniq -c      # count occurrences
sort names.txt | uniq -c | sort -rn   # most common first

# tr — strip Windows CRLF, uppercase
tr -d '\r' < windows.txt > unix.txt
echo 'hello' | tr 'a-z' 'A-Z'

# wc
wc -l app.log                 # line count
wc -w essay.txt               # word count
wc -c archive.tar.gz          # byte size

# head / tail
head -n 5 access.log
tail -n 20 error.log
tail -f /var/log/syslog       # follow new lines (Ctrl-C to stop)

# find + xargs — safe with spaces via -print0
find . -name '*.log' -mtime -1 -print0 | xargs -0 wc -l

Pipeline thực tế

Top IP client từ access log

# Combined log format — IP is field 1
# 192.168.1.5 - - [24/Feb/2025:10:00:01 +0000] "GET /api HTTP/1.1" 200 1234

awk '{ print $1 }' access.log \
  | sort \
  | uniq -c \
  | sort -rn \
  | head -10

Chỉ lỗi HTTP 5xx, rồi top path:

awk '$9 ~ /^5/ { print $7 }' access.log \
  | sort \
  | uniq -c \
  | sort -rn \
  | head -5

Cộng cột CSV và lọc bằng grep

# sales.csv:
# id,region,amount
# 1,APAC,120.50
# 2,EMEA,89.00
# 3,APAC,200.00

# Total amount for APAC rows only
grep APAC sales.csv \
  | awk -F',' 'NR > 1 { sum += $3 } END { printf "APAC total: %.2f\n", sum }'

# Full pipeline: strip header, sum column 3, format result
awk -F',' 'NR > 1 { sum += $3 } END { print sum }' sales.csv

Quét bảo trì đa công cụ

# Find recent .sh files, count lines, show the largest
find ./scripts -name '*.sh' -mtime -7 -print0 \
  | xargs -0 wc -l \
  | sort -rn \
  | head -5

grep vs sed vs awk — khi nào dùng cái nào

Câu hỏiDùngVì sao
”File có chứa X không?grepFast filter, exit code for scripts
”Thay X bằng Y trên mọi dòngsedOne-liner substitution
”Cộng cột 3 / nhóm theo field 1awkField math and aggregation
”Xóa dòng khớp pattern commentsed or grep -vsed for in-place; grep -v in a pipe
”Lấy cột cố định từ CSV sạchcut or awk -F','cut is simpler; awk handles messy rows
”Đếm giá trị duy nhấtsort | uniq -cuniq only works on sorted adjacent lines

Lỗi người mới hay mắc

  • Chạy uniq không sort trướcuniq chỉ gộp duplicate liền kề, duplicate rải rác vẫn còn.
  • Dùng cut trên CSV có dấu phẩy trong field được quote — field lệch và số sai; dùng awk parse đúng hoặc công cụ CSV thật.
  • Tưởng sed -i giống nhau trên Linux và macOS — GNU sed dùng -i một mình; BSD sed cần -i ''.
  • Quên quote pattern regex — shell nuốt $, *, và ? trước khi grep/sed/awk nhận.
  • Dùng grep -c mong đợi số lần khớp trên dòng — nó đếm dòng có ít nhất một khớp, không phải tổng lần xuất hiện.
  • Pipe vào awk không đặt -F trên dữ liệu có delimiter — mọi thứ dồn vào $1.

Bài tập

Thử từng bài trước khi mở lời giải.

  1. Từ app.log, in chỉ dòng chứa ERROR hoặc FATAL (không phân biệt hoa thường), kèm số dòng, và đếm có bao nhiêu dòng như vậy.
  2. Dùng sed xóa mọi dòng trống hoặc bắt đầu bằng # từ nginx.conf, ghi kết quả vào nginx-clean.conf không dùng sed -i (portable).
  3. Cho access.log (IP ở field 1, HTTP status ở field 9), viết pipeline in top 3 IP trong các request trả status 500.
Lời giải
# Exercise 1 — filter, number, and count
grep -inE 'ERROR|FATAL' app.log
grep -icE 'ERROR|FATAL' app.log    # -c gives the count; combine with -n in two steps

# Or one pipeline for numbered lines + separate count:
grep -inE 'ERROR|FATAL' app.log
echo "total: $(grep -icE 'ERROR|FATAL' app.log)"
# Exercise 2 — portable sed to a new file
sed -e '/^#/d' -e '/^$/d' nginx.conf > nginx-clean.conf
# Exercise 3 — filter 500s, count IPs, top 3
awk '$9 == 500 { print $1 }' access.log \
  | sort \
  | uniq -c \
  | sort -rn \
  | head -3

Bài 1 dùng -E cho alternation và -i cho không phân biệt hoa thường. grep -c không kết hợp với số dòng trong một lần gọi — chạy hai lần hoặc dùng pipeline. Bài 2 nối hai biểu thức sed bằng -e và redirect sang file mới, tránh -i phụ thuộc OS. Bài 3 là mẫu awk | sort | uniq -c | sort -rn | head kinh điển trong bài này.


Điều cốt lõi

grep lọc, sed biến đổi, awk phân tích và tính toán. Nối chúng với sort, uniq, cut, tr, và wc để trả lời câu hỏi thật trên log và CSV trong một dòng. Quote pattern, sort trước uniq, và biết dialect sed -i của bạn trước khi sửa file production. Tiếp theo: làm script fail an toàn với set -euo pipefail, trap, và xử lý lỗi có cấu trúc.