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ý text — grep, 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 |
|---|---|---|
| grep | lọc — giữ dòng khớp pattern | log files, config grep |
| sed | biến đổi — thay thế, xóa, in khoảng dòng | bulk find-replace, strip comments |
| awk | phân tích & tính — tách field, cộng cột, báo cáo | CSV, access logs, tabular data |
| cut / sort / uniq / tr / wc | hỗ trợ — lấy cột, sắp xếp, đếm, chuyển đổi, đo | prep before awk or after grep |
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
-rvớ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 -ikhác nhau giữa GNU sed (Linux) và BSD sed (macOS). Trên macOS,sed -i 's/x/y/' filetạo backup tênfilevớ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-itrong 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ỏ
awk là ngô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ệnh | Mục đích | Flag chính |
|---|---|---|
| cut | lấy cột theo vị trí ký tự hoặc delimiter | -d',' -f1,3 · -c1-10 |
| sort | sắp xếp dòng | -n numeric · -r reverse · -k2 key field |
| uniq | gộp dòng trùng liền kề | -c count · requires sorted input |
| tr | chuyể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 / tail | N dòng đầu / cuối | -n 20 · tail -f follow |
| find | tìm file theo tên, loại, mtime | -name '*.log' · -mtime -1 |
| xargs | ghé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ỏi | Dùng | Vì sao |
|---|---|---|
| ”File có chứa X không? | grep | Fast filter, exit code for scripts |
| ”Thay X bằng Y trên mọi dòng | sed | One-liner substitution |
| ”Cộng cột 3 / nhóm theo field 1 | awk | Field math and aggregation |
| ”Xóa dòng khớp pattern comment | sed or grep -v | sed for in-place; grep -v in a pipe |
| ”Lấy cột cố định từ CSV sạch | cut or awk -F',' | cut is simpler; awk handles messy rows |
| ”Đếm giá trị duy nhất | sort | uniq -c | uniq only works on sorted adjacent lines |
Lỗi người mới hay mắc
- Chạy
uniqkhôngsorttrước —uniqchỉ gộp duplicate liền kề, duplicate rải rác vẫn còn. - Dùng
cuttrên CSV có dấu phẩy trong field được quote — field lệch và số sai; dùngawkparse đúng hoặc công cụ CSV thật. - Tưởng
sed -igiống nhau trên Linux và macOS — GNU sed dùng-imộ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 -cmong đợ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
awkkhông đặt-Ftrê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.
- Từ
app.log, in chỉ dòng chứaERRORhoặcFATAL(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. - Dùng
sedxóa mọi dòng trống hoặc bắt đầu bằng#từnginx.conf, ghi kết quả vàonginx-clean.confkhông dùngsed -i(portable). - Cho
access.log(IP ở field 1, HTTP status ở field 9), viết pipeline in top 3 IP trong các request trả status500.
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 -3Bà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.