Chương 05 — Text processing: sed, awk, cut, sort, uniq, jq

Đây là lúc CLI thực sự đáng giá — xử lý hàng triệu dòng trong vài giây. Bạn sẽ dùng cut, sort, uniq, sed, awk, jq như những "lego" lắp với pipe để biến đống log lộn xộn thành insight.

1. cut, paste, tr, column — cắt & biến đổi cột text

cut — cắt theo cột

$ cut -d, -f1,3 data.csv      # delimiter dấu phẩy, lấy cột 1 và 3
$ cut -d: -f1 /etc/passwd     # lấy username từ /etc/passwd
$ cut -c1-10 file.txt           # 10 ký tự đầu mỗi dòng
$ cut -f2 data.tsv               # mặc định delimiter là TAB

tr — translate / squeeze / delete

$ echo "Hello World" | tr 'a-z' 'A-Z'     # HELLO WORLD
$ tr -d ' ' < file.txt                    # xóa toàn bộ dấu cách
$ tr -s ' ' < file.txt                    # squeeze: nhiều space liên tiếp → 1 space
$ cat file.txt | tr ',' '\n'          # thay , bằng newline (CSV → 1 cột)

paste — ghép cột

$ paste names.txt ages.txt           # ghép 2 file thành 2 cột (tab giữa)
$ paste -d, a.txt b.txt             # dùng "," làm delimiter

column — căn cột cho đẹp

$ cat data.csv | column -t -s,     # bảng gọn gàng, tách bằng dấu phẩy
$ mount | column -t                # output mount hiển thị thẳng cột
💡 Mánh nhỏ
cut không hỗ trợ delimiter "nhiều khoảng trắng" — nếu file có nhiều space ngăn cách (output của ls -l, ps...), hãy dùng awk hoặc tr -s ' ' để gom space lại trước.

2. sort & uniq — nâng cao

sort — sắp xếp

$ sort file.txt                       # mặc định: alphabet, A-Z
$ sort -r file.txt                    # reverse: Z-A
$ sort -n numbers.txt                 # numeric: 2 < 10 < 100 (KHÔNG so chuỗi)
$ sort -rn numbers.txt                # giảm dần theo số
$ sort -k2 data.txt                   # sort theo cột 2
$ sort -t, -k3 -n data.csv             # delimiter ",", sort cột 3 dạng số
$ sort -u file.txt                    # unique: bỏ dòng trùng (như sort | uniq)

uniq — lọc dòng trùng (dòng phải LIỀN nhau!)

$ sort file.txt | uniq              # bỏ dòng trùng
$ sort file.txt | uniq -c           # đếm số lần xuất hiện mỗi dòng
$ sort file.txt | uniq -d           # chỉ in dòng bị trùng
$ sort file.txt | uniq -u           # chỉ in dòng KHÔNG bị trùng
⚠️ uniq CẦN input đã sort
uniq chỉ so sánh dòng hiện tại với dòng liền trước. Nếu input chưa sort, các dòng giống nhau ở xa nhau sẽ không bị gộp. Luôn sort | uniq, không bao giờ uniq đơn lẻ trừ khi bạn biết input đã sort.

Idiom kinh điển: top-N

$ sort file.txt | uniq -c | sort -rn | head
# Đếm dòng trùng → sort giảm dần theo count → 10 dòng đầu
# → đây là cách tìm "top N item phổ biến" trong mọi dataset text

3. sed — stream editor

sed đọc input theo dòng, áp dụng quy tắc, in ra. Dùng nhiều nhất cho tìm & thay thế.

Substitution — s/cũ/mới/

$ sed 's/foo/bar/' file.txt          # thay foo → bar (dòng đầu tiên gặp)
$ sed 's/foo/bar/g' file.txt         # g = global, thay TẤT CẢ trên mỗi dòng
$ sed 's|/usr/bin|/opt/bin|g' conf  # dùng | làm delimiter khi chuỗi có /
$ sed -E 's/[0-9]+/N/g' file.txt    # -E: regex extended; thay mọi số bằng N

In-place editing — -i

$ sed -i 's/old/new/g' config.txt   # Linux: sửa trực tiếp file
$ sed -i '' 's/old/new/g' config.txt # macOS: BẮT BUỘC '' sau -i (BSD sed)
$ sed -i.bak 's/old/new/g' config.txt # tạo backup config.txt.bak (Linux)
⚠️ macOS vs Linux — bẫy sed -i
macOS dùng BSD sed: sed -i '' 's/.../.../' file — phải có chuỗi rỗng '' ngay sau -i, nếu không sed sẽ tưởng đối số kế tiếp là extension và tạo file backup tên kỳ quặc. Linux (GNU sed) thì sed -i 's/.../.../' file chạy bình thường. Đây là một trong những khác biệt khó chịu nhất khi viết script chạy ở cả hai bên.

Xóa dòng & chỉ in dòng khớp

$ sed '/^#/d' config.conf            # xóa mọi dòng comment (bắt đầu bằng #)
$ sed '/^$/d' file.txt                # xóa dòng trống
$ sed -n '/ERROR/p' app.log         # -n: tắt in mặc định; chỉ in dòng có ERROR
$ sed -n '10,20p' file.txt           # in dòng 10-20
$ sed '5d' file.txt                   # xóa dòng 5

4. awk — mini language cho text

Nghĩ về awk như "Python mini cho text" — nó đọc từng dòng, tách thành các field, rồi cho bạn viết logic xử lý. Một dòng awk đôi khi tương đương 20 dòng Python.

Bảng tham chiếu biến built-in

BiếnÝ nghĩa
$0Toàn bộ dòng hiện tại
$1, $2, $3...Field thứ 1, 2, 3... (mặc định cách bằng whitespace)
NRNumber of Records — số dòng đã đọc (line number hiện tại)
NFNumber of Fields — số cột của dòng hiện tại
FSField Separator (mặc định: whitespace)
OFSOutput Field Separator (mặc định: space)

Cú pháp cơ bản

$ awk '{print $1}' file.txt           # in cột đầu
$ awk '{print $1, $3}' file.txt       # cột 1 và 3 (cách bằng OFS)
$ awk '{print NR, $0}' file.txt       # thêm số dòng phía trước
$ awk '{print NF}' file.txt           # in số cột mỗi dòng

Field separator — -F

$ awk -F, '{print $2}' data.csv      # delimiter là dấu phẩy
$ awk -F: '{print $1}' /etc/passwd  # : là field separator
📘 Lưu ý về Field Separator
Mặc định awk tách bằng whitespace (1 hoặc nhiều space/tab) — mạnh hơn cut. Nếu file dùng delimiter cụ thể (vd CSV), luôn nhớ -F. Quên -F, với CSV là lỗi rất thường gặp: $1 sẽ là cả dòng vì không có space.

BEGIN / END — chạy trước & sau khi đọc

$ awk 'BEGIN {print "Bắt đầu"} {print $1} END {print "Tổng:", NR}' file.txt

Filter (như WHERE trong SQL)

$ awk '$3 > 100' data.txt              # in dòng có cột 3 > 100
$ awk '/ERROR/ {print $0}' app.log     # dòng chứa ERROR
$ awk 'NR>1' data.csv                  # bỏ dòng tiêu đề (skip header)

Ví dụ thực tế 1 — Xử lý CSV

# sales.csv:
# date,product,amount
# 2026-01-15,coffee,45000
# 2026-01-15,tea,30000
# 2026-01-16,coffee,50000

$ awk -F, 'NR>1 {print $2, $3}' sales.csv
coffee 45000
tea 30000
coffee 50000

Ví dụ thực tế 2 — Tính tổng & trung bình cột

$ awk -F, 'NR>1 {sum += $3} END {print "Tổng:", sum}' sales.csv
Tổng: 125000

$ awk -F, 'NR>1 {sum += $3; n++} END {print "Trung bình:", sum/n}' sales.csv
Trung bình: 41666.7

Ví dụ thực tế 3 — Log analysis (đếm theo nhóm)

$ awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn
# Đếm số request theo IP (cột 1), sort giảm dần
  4521 192.168.1.10
  1203 10.0.0.5
   847 172.16.0.99

5. jq — JSON xử lý

Mọi REST API hôm nay trả JSON. jqawk cho JSON: query, filter, transform. Cài: brew install jq.

Bảng tham chiếu cú pháp jq

Cú phápÝ nghĩa
.Identity — trả nguyên input (pretty-print)
.fieldLấy giá trị của field trong object
.[]Iterate — duyệt mọi phần tử trong array
.[0]Phần tử thứ 0 của array
select(cond)Chỉ giữ phần tử thoả điều kiện
map(expr)Áp expr cho mỗi phần tử array
lengthĐộ dài array / số ký tự string / số key của object
keysDanh sách key của object
|Pipe trong jq (giống pipe shell, nhưng giữa các expression)

Ví dụ với REST API

$ curl -s https://api.github.com/users/torvalds | jq '.'
# Pretty-print toàn bộ JSON, tô màu

$ curl -s https://api.github.com/users/torvalds | jq '.name'
"Linus Torvalds"

$ curl -s https://api.github.com/users/torvalds/repos | jq '.[].name'
# Lấy tên mọi repo (array → từng phần tử → field name)

$ curl -s https://api.github.com/users/torvalds/repos | jq '.[] | select(.stargazers_count > 1000) | .name'
# Chỉ repo có > 1000 sao

$ curl -s https://api.github.com/users/torvalds/repos | jq '[.[] | {name, stars: .stargazers_count}]'
# Map sang object mới gọn hơn → wrap thành array

$ curl -s https://api.github.com/users/torvalds/repos | jq 'length'
7

Ví dụ với file JSON local

# users.json: [{"name":"Alice","age":30},{"name":"Bob","age":25}]

$ jq '.[].name' users.json
"Alice"
"Bob"

$ jq 'map(.age) | add' users.json     # tổng age = 55
$ jq -r '.[].name' users.json        # -r: raw output, bỏ dấu nháy

6. Pipeline thực chiến — gộp tất cả

Bài toán 1: Top 10 IP gọi nhiều nhất từ access.log

# access.log dạng nginx: 192.168.1.10 - - [08/May/2026...] "GET /api ..." 200

$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
  4521 192.168.1.10
  1203 10.0.0.5
   847 172.16.0.99
   ...

Bài toán 2: Top 10 URL được gọi nhiều nhất

$ awk '{print $7}' access.log | sort | uniq -c | sort -rn | head
# $7 trong nginx log là path của request (/api, /login...)

Bài toán 3: Đếm response status code

$ awk '{print $9}' access.log | sort | uniq -c | sort -rn
 18203 200
   452 304
   189 404
    23 500

Bài toán 4: Top contributor trong git log

$ git log --format='%an' | sort | uniq -c | sort -rn | head
   312 Linus Torvalds
   147 Greg KH
    89 ...

Bài toán 5: Lọc CSV phức tạp

# Tìm các giao dịch > 100k, sort theo amount giảm dần, lấy top 5
$ awk -F, 'NR>1 && $3 > 100000 {print $0}' sales.csv | sort -t, -k3 -rn | head -5
💡 Tư duy pipeline
Mỗi lệnh = 1 bước biến đổi text. Đừng cố viết "1 lệnh khủng" — hãy chia nhỏ rồi pipe lại: extract → sort → count → re-sort → cắt top. Khi gặp output sai, chạy từng đoạn pipeline một (chèn head giữa) để xem dữ liệu mỗi bước trông thế nào.

7. Thử ngay trong terminal giả lập

Gợi ý các lệnh để thử (terminal demo có thể chỉ giả lập một phần — phần còn lại làm trên Terminal thật):

  1. echo "apple,banana,cherry" | tr ',' '\n'
  2. echo "Hello World" | tr 'a-z' 'A-Z'
  3. printf "3\n1\n2\n10\n" | sort -n
  4. printf "a\nb\na\nb\nc\n" | sort | uniq -c
  5. echo "id,name,age" | awk -F, '{print NF}' (kết quả: 3)
  6. echo "hello sed" | sed 's/sed/awk/'
you@mac — chuong5 practice
you@mac ~ $

8. Bài tập về nhà

Làm trên Terminal thật. Tạo một thư mục chuong5-practice và đặt mọi file đầu vào trong đó.

  1. Tạo file names.csv với nội dung:
    id,name,score
    1,Alice,87
    2,Bob,72
    3,Charlie,95
    4,Diana,68
    5,Evan,91
    Dùng awk in ra tên và điểm (cột 2 và 3), bỏ dòng tiêu đề.
  2. Vẫn từ names.csv: tính tổng điểmđiểm trung bình bằng awk.
  3. Lọc người có điểm > 80 bằng awk -F, '...'.
  4. Tạo fruits.txt với 10 dòng (có nhiều dòng trùng), ví dụ mỗi dòng 1 loại quả: apple, banana, apple, orange, apple, banana, kiwi, kiwi, apple, orange. Dùng pipeline sort | uniq -c | sort -rn để liệt kê loại quả nào xuất hiện nhiều nhất.
  5. Phân tích log thật: tải một file access log mẫu (hoặc tự bịa 50 dòng theo format nginx). Dùng pipeline awk + sort + uniq + sort -rn + head để tìm:
    • Top 5 IP gọi nhiều nhất.
    • Đếm số request theo status code.
  6. Bài sed: tạo file config.txt chứa vài dòng có localhost:3000. Dùng sed thay tất cả thành example.com:443lưu thẳng vào file (chú ý macOS cần sed -i '').
  7. Vẫn config.txt: dùng sed xóa mọi dòng comment (bắt đầu bằng #) và mọi dòng trống.
  8. Bài jq: chạy curl -s https://api.github.com/users/torvalds/repos > repos.json. Dùng jq để:
    • In số repo (length).
    • Liệt kê tên repo và số sao, sort giảm dần theo sao.
    • Chỉ in repo có ngôn ngữ là "C".
  9. Kết hợp: dùng git log --format='%an' trong một repo bất kỳ + pipeline đếm xem ai commit nhiều nhất top 5.

9. Quiz — Kiểm tra kiến thức

10. Tổng kết

Chương 6 chúng ta sẽ chuyển sang Process & Job control — quản lý tiến trình đang chạy: ps, top/htop, kill, foreground/background, jobs, nohup, &. Sau khi đã thuần thục text, chúng ta sẽ điều khiển những thứ đang chạy trong máy.

Trước ← Chương 04: Tìm kiếm (find, grep) Tiếp theo Chương 06: Process & Job control →