Hướng dẫn

grep, sed và awk: ví dụ xử lý văn bản trong terminal Linux

Cập nhật: · Skillaria.Top

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ảngrepgrep ERROR app.log
Thay thế văn bảnsedsed 's/http:/https:/g' file
Xóa hoặc hiển thị dòng theo số thứ tựsedsed -n '10,20p' file
Lấy một cộtawkawk '{print $1}' access.log
Tính tổng, trung bình, nhóm dữ liệuawkawk '{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ĩaVí dụ
^đầu dònggrep '^root' /etc/passwd
$cuối dònggrep 'bash$' /etc/passwd
.ký tự bất kỳgrep 'h.t'
*0 lần trở lêngrep 'ab*c'
+ (ERE)1 lần trở lêngrep -E '[0-9]+'
| (ERE)hoặcgrep -E 'error|warn'
[...]một ký tự trong tập hợpgrep '[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 $1 bằ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 g trong 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 -i mà 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ó -i hoặc dùng -i.bak.
  • + và | không hoạt động trong grep. Đây là cú pháp mở rộng: cần grep -E (hoặc sed -E).
  • Dấu chấm trong mẫu. grep 192.168.1.1 cũng khớp với 192x168y1z1, vì dấu chấm là «ký tự bất kỳ». Hãy dùng grep -F hoặ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ặc awk -F'\t'.
  • cat thừa. cat file | grep x vẫn chạy, nhưng grep x file ngắ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.

Học chủ đề này qua video

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