grep, sed và awk: ví dụ xử lý văn bản trong terminal Linux
grep tìm dòng theo mẫu (grep -rn error /var/log), sed thay đổi văn bản trong luồng hoặc trong file (sed -i 's/old/new/g' file), còn awk làm việc với các cột và tính toán (awk '{print $1}' access.log). Kết hợp trong pipeline, chúng thay được nhiều script: grep lọc dòng, sed sửa văn bản, awk tách trường và tổng hợp.
grep, sed và awk là ba tiện ích kinh điển của Linux để xử lý văn bản. grep tìm dòng theo mẫu, sed thay thế và xóa văn bản trong luồng mà không cần mở trình soạn thảo, còn awk tách dòng thành các cột và có thể đếm, tính toán. Dưới đây là các ví dụ grep sed awk thường dùng khi phân tích log, file cấu hình và kết quả lệnh, kèm những lỗi hay gặp.
Khi nào dùng công cụ nào
Cả ba công cụ đọc văn bản theo từng dòng và ghi kết quả ra đầu ra chuẩn, nên dễ dàng nối với nhau bằng |. Khác biệt nằm ở việc mỗi công cụ giải quyết tác vụ nào tốt nhất.
| Tác vụ | Công cụ | Ví dụ |
|---|---|---|
| Tìm dòng chứa một đoạn văn bản | grep | grep ERROR app.log |
| Thay thế văn bản | sed | sed 's/http:/https:/g' file |
| Xóa hoặc hiển thị dòng theo số thứ tự | sed | sed -n '10,20p' file |
| Lấy một cột | awk | awk '{print $1}' access.log |
| Tính tổng, trung bình, nhóm dữ liệu | awk | awk '{s+=$10} END {print s}' |
Quy tắc đơn giản: nếu chỉ cần lọc thì dùng grep, nếu cần sửa văn bản thì dùng sed, nếu văn bản có cột hoặc cần tính toán thì dùng awk.
grep: tìm dòng theo mẫu
Ví dụ grep cơ bản
# Các dòng có từ error trong file
grep error /var/log/syslog
# Không phân biệt hoa thường: Error, ERROR, error
grep -i error app.log
# Hiển thị số thứ tự dòng
grep -n "Connection refused" app.log
# Tìm đệ quy trong thư mục, hiển thị tên file
grep -rn "DB_HOST" /etc/myapp/
# Chỉ hiển thị tên file có khớp
grep -rl "TODO" src/
# Đảo ngược: tất cả TRỪ comment và dòng trống
grep -v -e '^#' -e '^$' /etc/ssh/sshd_config
# Đếm số dòng khớp
grep -c " 500 " access.log
Cờ -w tìm nguyên từ: grep -w log sẽ không tìm thấy login. Cờ -F tắt regex và tìm chuỗi theo đúng nghĩa đen, hữu ích khi mẫu có dấu chấm, ngoặc hoặc dấu sao.
Ngữ cảnh xung quanh dòng tìm được
Một lỗi trong log hiếm khi rõ ràng chỉ với một dòng, bạn cần stack trace hoặc truy vấn dẫn tới lỗi đó. Có các cờ ngữ cảnh cho việc này:
# 3 dòng sau dòng khớp (After)
grep -A 3 "Traceback" app.log
# 2 dòng trước dòng khớp (Before)
grep -B 2 "OOM" /var/log/kern.log
# 5 dòng ở cả hai phía (Context)
grep -C 5 "segfault" /var/log/syslog
grep ngăn các nhóm kết quả bằng dòng --, nên đầu ra dễ đọc hơn.
grep và biểu thức chính quy
Mặc định grep hiểu regex cơ bản (BRE). Với cờ -E, bạn bật regex mở rộng (ERE), trong đó +, ?, | và ngoặc hoạt động mà không cần dấu gạch chéo ngược.
| Cấu trúc | Ý nghĩa | Ví dụ |
|---|---|---|
^ | đầu dòng | grep '^root' /etc/passwd |
$ | cuối dòng | grep 'bash$' /etc/passwd |
. | ký tự bất kỳ | grep 'h.t' |
* | 0 lần trở lên | grep 'ab*c' |
+ (ERE) | 1 lần trở lên | grep -E '[0-9]+' |
| (ERE) | hoặc | grep -E 'error|warn' |
[...] | một ký tự trong tập hợp | grep '[Ee]rror' |
# Các dòng có lỗi hoặc cảnh báo
grep -E "ERROR|WARN" app.log
# Chỉ lấy địa chỉ IP từ log, mỗi dòng một IP (-o chỉ in phần khớp)
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log
grep -P: lấy giá trị nằm giữa các đoạn đã biết
GNU grep có cờ -P cho regex kiểu Perl. Với nó có thể dùng \K («bỏ qua mọi thứ đã khớp trước vị trí này») và lookahead, nhờ đó chỉ lấy được phần giá trị:
$ echo 'user_id=4821 status=ok' | grep -oP 'user_id=\K[0-9]+'
4821
$ grep -oP '"version":\s*"\K[^"]+' package.json
2.4.1
Cờ -P không có trong mọi bản grep (ví dụ BSD grep trên macOS không có), vì vậy trong script cần chạy trên nhiều hệ thống nên dùng sed hoặc awk.
grep -q trong điều kiện của script
grep -q không in gì, chỉ trả về mã thoát: 0 nếu tìm thấy, 1 nếu không. Nhờ vậy có thể viết kiểm tra trực tiếp trong if:
if grep -q "^PermitRootLogin no" /etc/ssh/sshd_config; then
echo "Đã tắt đăng nhập root"
fi
Cách này ngắn hơn việc lưu kết quả vào biến rồi kiểm tra chuỗi rỗng, và grep dừng ngay khi gặp lần khớp đầu tiên.
sed: thay thế và xóa văn bản
Thay thế: lệnh s
Lệnh chính của sed là s/mẫu/thay-thế/cờ. Nếu không có cờ g, chỉ lần khớp đầu tiên trong mỗi dòng được thay thế.
# In văn bản đã thay thế (file không thay đổi)
sed 's/foo/bar/' file.txt
# Thay thế tất cả lần xuất hiện trong dòng
sed 's/foo/bar/g' file.txt
# Không phân biệt hoa thường (GNU sed)
sed 's/error/ERROR/gI' app.log
# Dùng dấu phân cách khác khi văn bản có dấu gạch chéo
sed 's|/var/www|/srv/www|g' nginx.conf
sed -i: thay thế trực tiếp trong file
Cờ -i ghi kết quả trở lại file. Để không mất bản gốc, hãy chỉ định hậu tố cho bản sao lưu:
# Tạo config.ini.bak và sửa config.ini
sed -i.bak 's/^debug=true/debug=false/' config.ini
# Thay thế trong mọi file .conf của thư mục
find /etc/myapp -name "*.conf" -exec sed -i 's/old-host/new-host/g' {} +
Trước khi sửa hàng loạt, hãy chạy lệnh không có -i và xem kết quả. Trên macOS (BSD sed), -i bắt buộc có đối số: sed -i '' 's/a/b/' file.
Xử lý dòng theo số thứ tự và theo mẫu
# Hiển thị dòng 10–20 (-n tắt tự động in, p là lệnh in)
sed -n '10,20p' file.txt
# Xóa dòng 5
sed '5d' file.txt
# Xóa dòng cuối
sed '$d' file.txt
# Xóa dòng trống và comment
sed -e '/^$/d' -e '/^#/d' nginx.conf
# Chỉ thay thế trong các dòng có "listen"
sed '/listen/s/80/8080/' nginx.conf
# Chèn dòng sau dòng khớp (GNU sed)
sed '/^\[mysqld\]/a max_connections = 500' my.cnf
Tham chiếu ngược và &
Phần mẫu nằm trong ngoặc có thể được đưa vào chuỗi thay thế bằng \1, \2; ký hiệu & nghĩa là toàn bộ đoạn đã khớp.
# Đổi chỗ họ và tên (ERE qua -E)
$ echo "Nguyễn Minh" | sed -E 's/(\S+) (\S+)/\2 \1/'
Minh Nguyễn
# Bọc tất cả các số trong dấu ngoặc vuông
$ echo "port 8080, timeout 30" | sed -E 's/[0-9]+/[&]/g'
port [8080], timeout [30]
# Chuyển ngày 2026-10-14 thành 14.10.2026
$ echo 2026-10-14 | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3.\2.\1/'
14.10.2026
awk: cột, điều kiện và tính toán
Lấy cột cần thiết
awk tách mỗi dòng thành trường theo khoảng trắng và tab: $1 là trường thứ nhất, $2 là thứ hai, $NF là trường cuối, $0 là cả dòng.
# Địa chỉ IP từ log nginx (cột đầu tiên)
awk '{print $1}' /var/log/nginx/access.log
# Tên người dùng và shell từ /etc/passwd (phân cách bằng dấu hai chấm)
awk -F: '{print $1, $7}' /etc/passwd
# Cột cuối cùng
df -h | awk '{print $NF}'
# Đặt dấu phân cách riêng khi in
awk -F: -v OFS=';' '{print $1, $3}' /etc/passwd
Điều kiện: chỉ in các dòng cần thiết
Trước khối trong dấu ngoặc nhọn có thể đặt điều kiện, và awk chỉ thực hiện hành động với các dòng thỏa điều kiện.
# Người dùng có UID từ 1000 trở lên (người dùng thường, không phải hệ thống)
awk -F: '$3 >= 1000 {print $1}' /etc/passwd
# Các yêu cầu có mã phản hồi từ 500 trở lên (trường thứ 9 ở định dạng combined)
awk '$9 >= 500' access.log
# Các dòng mà cột 6 khớp với regex
awk '$6 ~ /POST/' access.log
# File lớn hơn 100 MB từ kết quả ls -l
ls -l | awk '$5 > 100*1024*1024 {print $9}'
BEGIN, END và NR: awk như một chương trình nhỏ
Khối BEGIN chạy trước khi đọc dòng đầu tiên, còn END chạy sau dòng cuối. NR lưu số thứ tự của dòng hiện tại, còn trong khối END nó chứa tổng số dòng.
# Tổng kích thước phản hồi (trường thứ 10) tính bằng megabyte
awk '{sum += $10} END {printf "%.1f MB\n", sum/1024/1024}' access.log
# Thời gian phản hồi trung bình từ cột cuối
awk '{s += $NF} END {if (NR) print s/NR}' timings.log
# Bỏ qua dòng tiêu đề CSV
awk -F, 'NR > 1 {print $2}' users.csv
# Đếm số yêu cầu theo IP: mảng kết hợp
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn | head
Ví dụ cuối là cách kinh điển để phân tích log: mảng count với khóa là IP thay thế cho cặp sort | uniq -c, và dễ mở rộng, ví dụ thành tổng lưu lượng theo từng địa chỉ.
Pipeline: grep, sed và awk cùng nhau
Sức mạnh thực sự nằm ở việc kết hợp. Mỗi công cụ làm phần việc của mình, còn | chuyển kết quả đi tiếp.
Top 10 IP gây lỗi 5xx
grep ' 50[0-9] ' /var/log/nginx/access.log \
| awk '{print $1}' \
| sort | uniq -c | sort -rn | head -10
412 203.0.113.45
118 198.51.100.7
36 192.0.2.19
Ở đây grep lọc sơ bộ các dòng, awk lấy trường cần thiết, còn sort | uniq -c đếm số lần lặp lại. Cũng có thể viết bằng một awk duy nhất: awk '$9 ~ /^5/ {c[$1]++} END {for (i in c) print c[i], i}' access.log | sort -rn | head. Cách này chính xác hơn, vì kiểm tra đúng cột mã phản hồi.
Các URL được truy cập nhiều nhất
awk '{print $7}' access.log | sed 's/?.*//' | sort | uniq -c | sort -rn | head
sed ở đây cắt phần query string, để /search?q=a và /search?q=b được đếm như cùng một trang.
Giá trị trong cấu hình, bỏ qua comment
grep -v '^\s*#' /etc/myapp/app.env | grep '=' | awk -F= '{print $1}'
Lỗi trong một giờ cụ thể
grep '14/Oct/2026:03:' access.log | awk '$9 >= 500 {print $4, $7, $9}'
Lỗi thường gặp
- Dùng dấu nháy kép thay cho dấu nháy đơn với awk. Trong
awk "{print $1}", shell sẽ thay$1bằng biến của chính nó trước khi awk chạy. Hãy viết chương trình awk và biểu thức sed trong dấu nháy đơn. - Quên
gtrong sed. Nếu thiếu, chỉ lần khớp đầu tiên trong dòng được thay, các lần còn lại giữ nguyên. - Dùng
sed -imà không kiểm tra. Một regex sai sẽ làm hỏng file một cách âm thầm. Hãy chạy thử không có-ihoặc dùng-i.bak. +và|không hoạt động trong grep. Đây là cú pháp mở rộng: cầngrep -E(hoặcsed -E).- Dấu chấm trong mẫu.
grep 192.168.1.1cũng khớp với192x168y1z1, vì dấu chấm là «ký tự bất kỳ». Hãy dùnggrep -Fhoặc escape thành\.. - Khoảng trắng trong cột. Nếu trường có thể chứa khoảng trắng (CSV, đường dẫn), hãy khai báo rõ dấu phân cách:
awk -F,hoặcawk -F'\t'. - cat thừa.
cat file | grep xvẫn chạy, nhưnggrep x filengắn hơn và không tạo thêm tiến trình.
Các công cụ có thể thay thế grep, sed và awk
Với JSON, các công cụ này không tiện: nên phân tích cấu trúc bằng jq (jq '.items[] | select(.status=="active") | .name'), còn YAML thì dùng yq. Để tìm kiếm đệ quy nhanh trong dự án lớn, người ta thường dùng ripgrep (lệnh rg), công cụ mặc định bỏ qua những gì có trong .gitignore. Nếu quen cú pháp Perl, perl -pe 's/a/b/g' và perl -lane 'print $F[0]' thay được sed và awk chỉ trong một dòng. Tuy vậy, grep, sed và awk gần như có sẵn trên mọi máy chủ mà không cần cài đặt, nên đây là những công cụ cần biết trước tiên.
Tổng kết
grep tìm dòng, sed thay đổi chúng, còn awk làm việc với cột và tính toán. Hãy ghi nhớ bộ lệnh cơ bản: grep -rniE cùng -A/-B/-C, sed 's/.../.../g' với -i.bak và -n 'Np', awk -F với $1, $NF, điều kiện và khối END. Gần như mọi việc phân tích log đều được xây từ những khối này. Các ví dụ grep sed awk này, cùng với find, jq và curl, được thực hành trong bài giảng về xử lý văn bản trong terminal của khóa học Linux.
Câu hỏi thường gặp
grep khác egrep ở điểm nào?
egrep là bí danh cũ của grep -E (regex mở rộng). Các phiên bản GNU grep hiện đại sẽ in cảnh báo khi gọi egrep, vì vậy hãy dùng grep -E.
Làm sao tìm một đoạn văn bản trong tất cả file của thư mục trên Linux?
Dùng grep -rn "văn-bản" /đường/dẫn: -r tìm đệ quy, -n hiển thị số dòng. Để giới hạn loại file, thêm --include="*.php".
Làm sao thay thế văn bản trong nhiều file cùng lúc?
Kết hợp tìm kiếm với sed: grep -rl "old" . | xargs sed -i 's/old/new/g'. Trước khi làm, nên commit hoặc sao lưu.
Làm sao in nhiều cột trong awk, cách nhau bằng tab?
Đặt dấu phân cách đầu ra: awk -v OFS='\t' '{print $1, $3}' file. Nếu không có OFS, các trường được nối bằng khoảng trắng.
Làm sao xóa khoảng trắng cuối dòng bằng sed?
Lệnh sed -i 's/[[:space:]]*$//' file xóa các khoảng trắng và tab ở cuối mọi dòng trong file.
Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Linux: с нуля до уверенного администрирования”.
Đến khóa học