Chương 04 — Search: grep, find, fzf

Tìm nội dung trong file với grep, tìm file trong cây thư mục với find, và tăng tốc gấp 10 lần với bộ ba hiện đại ripgrep, fd, fzf. Đây là kỹ năng phân biệt người dùng CLI biết qua loa với người thật sự nhanh.

1. grep cơ bản — tìm chuỗi trong file

grep (Global Regular Expression Print) đọc input dòng-theo-dòng và in ra những dòng khớp pattern. Cú pháp tổng quát:

$ grep [options] "pattern" file

Các cờ phải nhớ

$ grep "error" server.log         # tìm "error" — phân biệt hoa/thường
$ grep -i "error" server.log      # -i: case-insensitive (Error, ERROR, error)
$ grep -n "TODO" main.py          # -n: in kèm số dòng
$ grep -c "error" server.log      # -c: ĐẾM số dòng khớp (không in ra dòng)
$ grep -v "DEBUG" server.log      # -v: INVERT — in dòng KHÔNG khớp
$ grep -r "console.log" src/      # -r: đệ quy qua mọi file trong src/
$ grep -w "log" src/main.js       # -w: word boundary — không khớp "login", "logger"

Ghép cờ — rất phổ biến

$ grep -rin "todo" src/         # đệ quy + case-insensitive + số dòng
$ grep -rwn "id" .              # từ "id" đứng riêng, không phải "uuid"/"width"
💡 Mẹo: -v để loại bỏ noise
Có file log lớn nhưng đầy dòng DEBUG? Lọc chúng ra trước: grep -v "DEBUG" server.log | grep "user_id=42". Đây là use case kinh điển ở Chương 3 (pipes).

2. grep với regex

Pattern của grep mặc định là BRE (Basic Regular Expression). Để dùng cú pháp regex hiện đại, thêm -E (Extended) — tránh phải escape +, ?, |, {}.

Anchors & character class

$ grep "^ERROR" server.log              # ^ : đầu dòng
$ grep "\.js$" filelist.txt             # $ : cuối dòng — file kết thúc bằng .js
$ grep "[0-9]" data.txt                 # bất kỳ chữ số
$ grep "[A-Za-z]" data.txt              # chữ cái
$ grep "[^0-9]" data.txt                # [^...] : KHÔNG phải chữ số

Extended regex với -E

$ grep -E "error|warning|fatal" log    # | : alternation (HOẶC)
$ grep -E "colou?r" doc.txt              # ? : 0 hoặc 1 (color, colour)
$ grep -E "a+" input                    # + : 1 hoặc nhiều
$ grep -E "[0-9]{3}-[0-9]{4}" phones    # {n} : đúng n lần — "555-1234"

Ví dụ thực tế — tìm email và IP

$ grep -Eo "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}" contacts.txt
alice@example.com
bob@dev.io

$ grep -Eo "([0-9]{1,3}\.){3}[0-9]{1,3}" access.log
192.168.1.10
10.0.0.42

Cờ -o (only-matching): chỉ in ra phần KHỚP pattern, không in cả dòng. Rất hữu dụng khi muốn extract dữ liệu.

📘 Quote pattern luôn luôn
Hãy luôn bọc pattern trong "..." hoặc '...'. Nếu không, shell sẽ expand các ký tự như *, $, ? TRƯỚC KHI grep nhìn thấy chúng — kết quả thường sai bí ẩn.

3. grep nâng cao — context, filter, pipe

Context: in cả dòng xung quanh

$ grep -A 3 "error" server.log    # -A 3: in 3 dòng SAU dòng khớp (After)
$ grep -B 2 "error" server.log    # -B 2: in 2 dòng TRƯỚC (Before)
$ grep -C 3 "error" server.log    # -C 3: cả trên 3 + dưới 3 (Context)

Lọc theo loại file

$ grep -r "TODO" . --include="*.js"      # chỉ tìm trong .js
$ grep -r "TODO" . --exclude="*.min.js"  # bỏ qua file đã minify
$ grep -r "TODO" . --exclude-dir=node_modules

Chỉ in TÊN FILE chứa pattern (-l)

$ grep -rl "console.log" src/
src/api/auth.js
src/utils/log.js
src/components/Button.js

Hữu dụng khi muốn pipe danh sách file sang lệnh khác (vd: mở trong editor, xoá, sửa hàng loạt).

Pipe nhiều grep liên tiếp — boolean AND

$ grep "user_id=42" server.log | grep "error" | grep -v "timeout"
# Dòng có user_id=42 VÀ có error VÀ KHÔNG có timeout
💡 Use case: tìm TODO trong toàn bộ codebase
grep -rn "TODO\|FIXME\|XXX" src/ --exclude-dir=node_modules
Lệnh trên kết hợp -r (đệ quy), -n (số dòng), regex alternation, và loại bỏ node_modules. Đây là lệnh bạn sẽ gõ hàng nghìn lần trong sự nghiệp coding.

4. find — tìm FILE theo thuộc tính

Khác với grep (tìm nội dung), find tìm file theo tên/loại/kích thước/thời gian. Cú pháp:

$ find <path> <tests> <actions>

Tìm theo tên

$ find . -name "*.js"            # mọi file .js từ folder hiện tại trở xuống
$ find . -iname "readme*"       # -iname: không phân biệt hoa/thường
$ find ~/Documents -name "*.pdf"
⚠️ LUÔN quote pattern trong -name
Nếu gõ find . -name *.txt KHÔNG có dấu nháy, shell expand *.txt thành danh sách file ở folder hiện tại trước, rồi truyền vào find — kết quả sai hoàn toàn. Phải là find . -name "*.txt" để find tự xử lý wildcard.

Tìm theo loại

$ find . -type f                # f = file thường
$ find . -type d                # d = directory
$ find . -type l                # l = symbolic link
$ find . -type f -name "*.log" # kết hợp tests

Tìm theo kích thước & thời gian

$ find . -size +100M           # lớn hơn 100MB (k/M/G)
$ find . -size -1k             # nhỏ hơn 1KB
$ find . -mtime +30           # sửa lần cuối > 30 ngày trước
$ find . -mtime -7            # sửa trong 7 ngày qua
$ find . -newer reference.txt  # sửa SAU file reference.txt

Use case kinh điển: file lớn cũ cần dọn

$ find ~/Downloads -type f -size +100M -mtime +30
/Users/you/Downloads/big-video.mp4
/Users/you/Downloads/dataset.zip

$ find ~/Downloads -type f -size +100M -mtime +30 -delete
# -delete: action xoá luôn (cẩn thận!)

5. find -exec & xargs

Tìm xong, ta thường muốn làm gì đó với danh sách file: xoá, copy, đổi quyền, mở, nén... Có 2 cách:

Cách 1 — -exec (built-in của find)

$ find . -name "*.tmp" -exec rm {} \;
#                                  ^^  ^^
#                                  |   kết thúc -exec (escape ; cho shell)
#                                  placeholder: chỗ này thay bằng tên file

$ find . -name "*.sh" -exec chmod +x {} \;
$ find . -name "*.log" -exec gzip {} \;

Cách 2 — pipe sang xargs

$ find . -name "*.tmp" | xargs rm
$ find . -name "*.log" | xargs gzip

xargs đọc input từ stdin (mỗi dòng = 1 argument) và đưa vào lệnh ở phía sau. Thường nhanh hơn -exec ... \; vì gọi lệnh 1 lần với nhiều args, thay vì gọi lần lượt từng file.

CáchSố lần gọi lệnhTốc độ
-exec ... {} \;1 lần / fileChậm
-exec ... {} +1 lần / nhiều file (gom batch)Nhanh
| xargs ...1 lần / nhiều fileNhanh
🛑 File có dấu cách / ký tự lạ → dùng -print0 + xargs -0

Mặc định xargs tách theo khoảng trắng — nên file tên "my file.txt" sẽ bị tách thành 2 đối số! Hậu quả: lệnh chạy sai, có khi xoá nhầm.

# SAI (nguy hiểm với tên file có space):
find . -name "*.tmp" | xargs rm

# ĐÚNG: dùng NUL byte làm separator
find . -name "*.tmp" -print0 | xargs -0 rm
-print0 in các tên cách nhau bằng byte NUL (\0); xargs -0 đọc theo cùng quy ước. Đây là pattern an toàn chuẩn — hãy thuộc lòng.

Use case thực — tìm và sửa hàng loạt

# Đếm tổng số dòng trong mọi file .py
$ find . -name "*.py" | xargs wc -l

# Grep "import" trong mọi file .js (an toàn với tên file có space)
$ find . -name "*.js" -print0 | xargs -0 grep -l "import"

6. Bộ ba hiện đại: ripgrep, fd, fzf

grepfindtổ tiên — vẫn có ở mọi máy Unix. Nhưng cộng đồng dev hiện đại hầu hết đã chuyển sang 3 công cụ viết bằng Rust, nhanh và dễ dùng hơn nhiều:

Cài đặt (macOS với Homebrew)

$ brew install ripgrep fd fzf

rg (ripgrep) — thay thế grep -r

$ rg "TODO"                       # mặc định đệ quy, có màu, có số dòng
$ rg "useState" -t js              # -t js: chỉ trong file JavaScript
$ rg -i "error" --glob "!*.min.js" # bỏ qua file minified
Tính nănggrep -rrg
Đệ quy mặc địnhKhông (cần -r)
Bỏ qua .gitignoreKhôngCó (mặc định)
Bỏ qua node_modules, .gitKhông
Tốc độ trên repo lớnChậmNhanh hơn 5–20×
Output có màuCần --colorMặc định

fd — thay thế find -name

$ fd readme                       # tìm file/folder tên có "readme"
$ fd -e py                          # -e: extension — mọi file .py
$ fd -t d node_modules               # -t d: chỉ folder, tên là node_modules
$ fd "\.log$" -x rm                  # -x: execute (giống find -exec)

Cú pháp fd ngắn hơn rất nhiều: không cần ., không cần -name, không cần quote. Mặc định bỏ qua .gitignore và file ẩn.

fzf — fuzzy finder interactive

$ fzf                              # mở UI tìm kiếm fuzzy file ở folder hiện tại
$ vim $(fzf)                       # mở vim với file chọn từ fzf
$ history | fzf                    # tìm lệnh cũ trong lịch sử
$ git branch | fzf | xargs git checkout # chọn branch để checkout

Sau khi cài, gõ $(brew --prefix)/opt/fzf/install để kích hoạt phím tắt:

📘 Khuyến nghị
Vẫn cần biết grep/find vì chúng có ở MỌI máy Unix (server, container, CI). Nhưng trên máy cá nhân hằng ngày, hãy dùng rg/fd/fzf — nhanh hơn, ít gõ hơn, ít sai hơn.

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

Simulator đã hỗ trợ các lệnh tìm kiếm cơ bản — thử các kịch bản:

  1. Liệt kê file: ls
  2. Tìm "TODO" trong file: grep "TODO" notes.txt
  3. Tìm đệ quy có số dòng: grep -rn "error" .
  4. Đảo ngược: grep -v "DEBUG" log.txt
  5. Tìm file theo tên: find . -name "*.txt"
  6. Tìm file lớn: find . -size +1k
  7. Pipe grep: cat log.txt | grep "error" | grep -v "timeout"
you@mac — chuong4 practice
you@mac ~ $

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

Trên Terminal thật, làm tuần tự (xong báo lại tôi check):

  1. Tải 1 repo bất kỳ về máy (vd git clone https://github.com/cli/cli). Tìm tất cả dòng chứa TODO trong source code, in kèm số dòng.
  2. Trong cùng repo, đếm tổng số dòng có FIXME bằng grep -c. So sánh với grep | wc -l — kết quả có khác không?
  3. Dùng find liệt kê mọi file .md trong ~/Documents (nếu chưa có thì tạo vài file test).
  4. Tìm 5 file lớn nhất trong ~/Downloads: find ~/Downloads -type f -size +50M. Ghi lại danh sách trước khi quyết định xoá.
  5. Pipe nâng cao (ôn Chương 3): tìm trong /var/log/system.log các dòng có "error", loại bỏ những dòng chứa "harmless", lấy 10 dòng cuối: grep "error" /var/log/system.log | grep -v "harmless" | tail -10.
  6. find -exec: tạo folder ~/scripts với vài file .sh, sau đó cấp quyền chạy hàng loạt bằng 1 lệnh: find ~/scripts -name "*.sh" -exec chmod +x {} \;.
  7. Cài brew install ripgrep fd fzf. Chạy rg "TODO" trong cùng repo ở câu 1 — so sánh tốc độ với grep -rn "TODO" . (dùng time trước lệnh).
  8. Thử Ctrl-R sau khi setup fzf. Tìm lại 1 lệnh dài bạn đã gõ trước đó.
  9. Bonus: viết 1 dòng tìm mọi file .log lớn hơn 10MB đã không sửa trong 7 ngày, và xoá an toàn (gợi ý: find ... -print0 | xargs -0 rm).

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

10. Tổng kết

Chương 5 chúng ta sẽ học text processingsed, awk, cut, sort, uniq: biến terminal thành công cụ ETL mini, xử lý CSV/log/text như chuyên gia data.

Trước ← Chương 03: Pipes & Redirects Tiếp theo Chương 05: Text Processing →