Chương 03 — Pipes & Redirects

Đây là chương mà CLI thật sự phát huy sức mạnh. Học cách ghép nhiều lệnh nhỏ thành một chuỗi xử lý dữ liệu — đếm dòng log, lọc process, gộp file, lưu output ra disk. Mỗi công cụ làm một việc, nhưng khi nối lại bằng |> thì gần như không có gì là không làm được.

1. stdout, stderr, stdin — 3 stream chuẩn

Mỗi process trên Unix khi chạy đều có 3 luồng dữ liệu mặc định, được đánh số bằng file descriptor (fd):

fdTênVai tròMặc định nối tới
0stdinInput — nơi lệnh đọc dữ liệu vàoBàn phím
1stdoutOutput bình thườngMàn hình terminal
2stderrOutput lỗi / cảnh báoMàn hình terminal

Hai stream output (1 và 2) cùng đổ ra terminal nên trông giống nhau, nhưng chúng tách biệt — và đó là điều quan trọng nhất chương này.

$ ls existing.txt missing.txt
existing.txt                                    # dòng này là stdout
ls: missing.txt: No such file or directory      # dòng này là stderr
📘 Vì sao tách stdout và stderr?
Để bạn có thể lưu kết quả tốt vào filevẫn thấy lỗi trên màn hình. Hoặc ngược lại: ẩn output, chỉ giữ lỗi để debug. Phần Redirect (mục 5) sẽ trực tiếp xử lý chuyện này.

2. Đọc file: cat, less, head, tail

cat — concatenate & in nội dung

$ cat README.md              # in toàn bộ file ra stdout
$ cat a.txt b.txt            # nối nhiều file (đó là tên gốc: con-CAT-enate)
$ cat -n script.sh           # -n: kèm số dòng

cat phù hợp cho file ngắn. File dài (vài nghìn dòng) sẽ tràn màn hình — dùng less.

less — đọc file dài có thể scroll

$ less /var/log/system.log   # mở pager

Khi đang trong less, các phím hữu ích:

head & tail — xem đầu / cuối file

$ head access.log              # 10 dòng đầu (mặc định)
$ head -n 20 access.log         # 20 dòng đầu
$ tail -n 50 error.log          # 50 dòng cuối
$ tail -f app.log               # follow: theo dõi log realtime, dòng mới hiện ngay
💡 tail -f là vũ khí số 1 của DevOps
Đang debug app crash? tail -f app.log ở 1 cửa sổ, restart app ở cửa sổ khác — bạn thấy log chạy realtime. Kết hợp với grep ở Chương 4: tail -f app.log | grep ERROR — chỉ hiện dòng có chữ ERROR.

3. Đếm & gộp: wc, sort, uniq, tee

wc — word count (line / word / char)

$ wc README.md
      42     180    1024 README.md             # lines  words  bytes  filename

$ wc -l *.js                  # -l: chỉ đếm dòng — đếm SLOC nhanh
$ wc -w essay.txt              # -w: đếm từ
$ wc -c file.bin               # -c: đếm byte

sort — sắp xếp dòng

$ sort names.txt              # A→Z
$ sort -r names.txt           # reverse: Z→A
$ sort -n sizes.txt           # -n: sort theo số (tự nhiên), không phải string
$ sort -u dup.txt             # -u: unique, bỏ trùng

uniq — bỏ dòng trùng liền kề

Quan trọng: uniq chỉ gộp dòng trùng kề nhau. Vì vậy luôn đi cặp với sort trước:

$ sort log.txt | uniq            # gộp dòng trùng
$ sort log.txt | uniq -c         # -c: kèm count mỗi dòng xuất hiện bao nhiêu lần
$ sort log.txt | uniq -d         # -d: chỉ in dòng trùng (duplicate)

tee — chia stream thành 2

Hình chữ T trong tên gọi: input đi vào, output chia làm 2 hướng — vừa ra stdout vừa ghi file.

$ ls | tee filelist.txt           # vừa hiện trên màn hình, vừa lưu file
$ make | tee build.log             # xem build live, đồng thời lưu log để xem lại
$ echo "127.0.0.1 dev" | sudo tee -a /etc/hosts  # -a: append; pattern phổ biến để ghi file cần sudo

4. Pipe | — nối stdout của lệnh A vào stdin của lệnh B

Đây là linh hồn của Unix philosophy: "Mỗi chương trình làm 1 việc, làm thật tốt. Nối chúng bằng pipe để giải bài toán lớn."

$ ls | wc -l                       # đếm số file/folder trong thư mục hiện tại
$ ps aux | grep node                # tìm mọi process node đang chạy
$ history | tail -n 20              # 20 lệnh gần nhất bạn đã gõ

Ví dụ kinh điển — top 10 IP truy cập nhiều nhất từ access log

$ cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 10

Đọc từ trái qua phải, mỗi pipe là một bước:

  1. cat access.log — đọc file log
  2. awk '{print $1}' — lấy cột 1 (IP address)
  3. sort — sắp xếp để các IP giống nhau nằm cạnh nhau
  4. uniq -c — gộp + đếm số lần
  5. sort -rn — sort theo số, giảm dần
  6. head -n 10 — lấy 10 dòng đầu
💡 Tư duy "stream"
Đừng nghĩ "tôi muốn lấy gì". Hãy nghĩ "dữ liệu chảy qua những bước biến đổi nào". Pipe biến CLI thành dây chuyền lắp ráp dữ liệu — và đây là kỹ năng cốt lõi phân biệt người dùng CLI sơ cấp với pro.

5. Redirect: > >> < 2> &>

Ghi stdout ra file: >>>

$ ls > files.txt              # ghi đè (overwrite): nội dung cũ MẤT
$ date >> log.txt            # append: nối thêm vào cuối
$ echo "Hello" > greet.txt    # tạo file mới với nội dung "Hello"
🛑 Cẩn thận: > ghi đè không hỏi
cat important.txt > important.txt sẽ xóa sạch file. Shell mở file để ghi (truncate về rỗng) trước khi chạy cat. Nếu cần append, luôn dùng >>. Để an toàn hơn, có thể bật set -o noclobber trong shell — khi đó > sẽ từ chối ghi đè file đã tồn tại.

Đọc file làm stdin: <

$ wc -l < access.log         # tương đương wc -l access.log nhưng đi qua stdin
$ sort < names.txt > sorted.txt   # đọc names.txt, sort, ghi ra sorted.txt

Redirect stderr: 2>

$ make 2> errors.log              # stderr → file, stdout vẫn ra màn hình
$ find / -name "*.conf" 2> /dev/null  # nuốt mọi "Permission denied"

Gộp cả stdout và stderr

$ make > build.log 2>&1          # stdout → file, stderr → cùng nơi với stdout
$ make &> build.log              # shortcut: stdout + stderr cùng vào file (bash/zsh)
⚠️ Thứ tự 2>&1 rất quan trọng
cmd > file 2>&1 đúng: stdout sang file, rồi stderr theo sau stdout (cũng vào file).
cmd 2>&1 > file SAI: stderr trỏ tới chỗ stdout đang trỏ (lúc đó vẫn là màn hình), rồi stdout mới đổi sang file. Kết quả: stderr ra màn hình, chỉ stdout vào file.

/dev/null — "thùng rác" của hệ thống

$ cmd > /dev/null               # vứt stdout đi, chỉ giữ stderr để debug
$ cmd 2> /dev/null              # vứt stderr, giữ output
$ cmd > /dev/null 2>&1          # im lặng tuyệt đối — chỉ quan tâm exit code

Bảng tổng kết redirect

Cú phápÝ nghĩa
> filestdout → file (ghi đè)
>> filestdout → file (append)
< filefile → stdin
2> filestderr → file
2>> filestderr → file (append)
2>&1stderr → đi cùng stdout
&> filecả stdout + stderr → file
> /dev/nullvứt output

6. Here-doc, Here-string & Process substitution

Here-document <<EOF — nhúng nhiều dòng làm stdin

$ cat > config.yml <<EOF
server:
  host: localhost
  port: 8080
EOF

Mọi thứ giữa <<EOF và dòng EOF được dùng làm stdin. EOF chỉ là quy ước — đặt tên gì cũng được, miễn nhất quán giữa mở và đóng.

Here-string <<< — 1 chuỗi ngắn làm stdin

$ wc -w <<< "hello world from cli"
       4
$ grep apple <<< "I like apple pie"
I like apple pie

Process substitution <(cmd)

Cho phép output của một lệnh được dùng như một file ảo. Cực kỳ tiện cho các lệnh chỉ nhận file (như diff):

$ diff <(ls dir1) <(ls dir2)        # so sánh nội dung 2 thư mục
$ diff <(sort a.txt) <(sort b.txt)    # so sánh sau khi sort
$ comm <(sort emails-2024.txt) <(sort emails-2025.txt)

Hướng ngược >(cmd) ít gặp hơn — gửi output đến nhiều consumer cùng lúc, ví dụ echo data | tee >(gzip > out.gz) >(wc -c).

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

Simulator đã hỗ trợ pipe và redirect cơ bản — thử các kịch bản:

  1. Tạo file demo: echo "apple" > fruits.txt
  2. Append thêm: echo "banana" >> fruits.txt rồi echo "apple" >> fruits.txt
  3. Xem nội dung: cat fruits.txt
  4. Đếm dòng: cat fruits.txt | wc -l
  5. Đếm tần suất: sort fruits.txt | uniq -c
  6. Lưu kết quả: sort fruits.txt | uniq -c > report.txt
  7. Xem report: cat report.txt
you@mac — chuong3 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. Đếm tổng số dòng trong tất cả file .js ở thư mục dự án (gợi ý: wc -l *.js — dòng cuối là tổng).
  2. Tạo file names.txt chứa 5 cái tên (mỗi dòng 1 tên, có vài tên trùng). Đếm xem mỗi tên xuất hiện bao nhiêu lần — chỉ dùng sort + uniq -c.
  3. Chạy ls /etc và lưu kết quả vào etc-listing.txt. Sau đó dùng wc -l để biết /etc có bao nhiêu mục.
  4. Chạy find / -name "*.log" 2> /dev/null | head. Giải thích vì sao có 2> /dev/null trong lệnh.
  5. Cùng lệnh ls -la: ghi output ra file list.txt nhưng vẫn hiện trên màn hình. Dùng lệnh nào?
  6. Tạo config.ini (3 dòng nội dung) chỉ bằng 1 lệnh dùng here-document <<EOF.
  7. So sánh nội dung 2 thư mục ~/Downloads~/Desktop bằng diff + process substitution.
  8. Trick câu hỏi: chạy cmd 2>&1 > out.txtcmd > out.txt 2>&1. Output (stderr) của 2 cách rơi vào đâu? Vì sao khác nhau?
  9. Bonus: tạo 1 chuỗi pipe đếm 5 lệnh bạn dùng nhiều nhất từ history (gợi ý: history | awk '{print $2}' | sort | uniq -c | sort -rn | head -n 5).

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

10. Tổng kết

Chương 4 chúng ta sẽ học searchgrep, findfzf — bộ ba để tìm bất cứ thứ gì trong file system và nội dung file. Kết hợp với pipe của chương này, bạn sẽ có superpower thật sự.

Trước ← Chương 02: File ops & Permissions Tiếp theo Chương 04: Search (grep, find, fzf) →