Pipe, Redirect và Luồng Dữ Liệu

K
Kai··5 min read

Bài 5 cho ta nhiều công cụ nhỏ. Bài này là cơ chế ghép chúng lại — và là một deep-dive đáng giá, vì hiểu nó thì cả dòng lệnh Linux "sáng" ra. Mọi thứ dựa trên một ý tưởng: chương trình đọc từ một luồng vào và ghi ra các luồng ra, còn bạn điều khiển các luồng đó đi đâu.

Ba luồng chuẩn

Mỗi chương trình trên Linux khi chạy được cấp sẵn ba luồng (stream), mỗi luồng là một file descriptor (fd) — một con số định danh (nhớ Bài 2: "mọi thứ là file", kể cả luồng dữ liệu):

                  ┌──────────────┐
   stdin (fd 0) ─►│              │─► stdout (fd 1)  ← kết quả bình thường
   (đầu vào)      │  chương trình │
                  │              │─► stderr (fd 2)  ← thông báo lỗi
                  └──────────────┘
  • stdin (fd 0) — đầu vào, mặc định là bàn phím.
  • stdout (fd 1) — đầu ra bình thường, mặc định ra màn hình.
  • stderr (fd 2) — đầu ra cho lỗi/thông báo, cũng mặc định ra màn hình nhưng là luồng riêng.

Vì sao tách stdout và stderr? Để bạn xử lý kết quả và lỗi riêng biệt — ví dụ lưu kết quả vào file nhưng vẫn thấy lỗi trên màn hình. Thấy rõ qua ví dụ sau.

Redirect: đổi hướng luồng vào file

Lệnh ls real.txt nope.txt (một file có, một file không) in kết quả ra stdout và lỗi ra stderr — cả hai trộn trên màn hình. Tách chúng:

ls real.txt nope.txt > out.txt 2> err.txt
# out.txt (stdout, fd 1):
real.txt
# err.txt (stderr, fd 2):
ls: cannot access 'nope.txt': No such file or directory
  • > file chuyển stdout vào file (ngầm hiểu là 1>).
  • 2> file chuyển stderr vào file.

Kết quả "real.txt" vào out.txt, lỗi vào err.txt — tách bạch hoàn toàn. Đây là lý do hai luồng tồn tại riêng.

Ghi đè và nối thêm

echo "dong 1" > f.txt     # > tạo mới / GHI ĐÈ toàn bộ file
echo "dong 2" >> f.txt    # >> NỐI THÊM vào cuối file
cat f.txt                 # dong 1 \n dong 2

Nhớ kỹ: > ghi đè (xóa sạch nội dung cũ), >> nối thêm. Gõ nhầm > thay vì >> lên file quan trọng là mất dữ liệu.

Đầu vào từ file: <

wc -l < f.txt    # đưa nội dung f.txt vào stdin của wc

< file chuyển stdin đọc từ file thay vì bàn phím. Ít dùng hơn > vì phần lớn lệnh nhận tên file trực tiếp, nhưng nó là phần đối xứng của mô hình.

Gộp stderr vào stdout: 2>&1

Đôi khi bạn muốn cả kết quả lẫn lỗi vào cùng một nơi (ví dụ một file log đầy đủ):

ls real.txt nope.txt > all.txt 2>&1

2>&1 nghĩa là "chuyển fd 2 (stderr) tới chỗ fd 1 (stdout) đang trỏ". Đặt sau > all.txt, nên cả hai cùng vào all.txt. Đây là lý do bạn hay thấy > file 2>&1 ở cuối các lệnh trong script và crontab — gom hết output lẫn lỗi vào một log.

Thứ tự quan trọng: > all.txt 2>&1 (gộp được) khác 2>&1 > all.txt (không). Vì 2>&1 sao chép đích hiện tại của fd 1; phải đặt sau khi fd 1 đã trỏ vào file.

Vứt bỏ output: /dev/null

/dev/null (Bài 2 — "hố đen") nuốt mọi thứ ghi vào nó. Dùng để vứt output không cần:

ls nope.txt 2>/dev/null      # vứt lỗi đi, không hiện trên màn hình
some-command > /dev/null 2>&1  # vứt CẢ stdout lẫn stderr (chạy im lặng)

Mẫu > /dev/null 2>&1 nghĩa là "chạy hoàn toàn im lặng" — gặp rất nhiều trong script khi bạn chỉ quan tâm lệnh chạy hay không, không cần output.

Pipe: nối các lệnh

Đây là mảnh ghép làm nên sức mạnh Unix. Dấu | (pipe) nối stdout của lệnh trái vào stdin của lệnh phải:

   cat /etc/passwd ─► grep root ─► wc -l
        stdout │ stdin    stdout │ stdin
cat /etc/passwd | grep root | wc -l
1

Đọc chuỗi này: cat tuôn nội dung file → grep root lọc dòng chứa "root" → wc -l đếm số dòng. Mỗi lệnh làm một việc, ghép lại thành "đếm số dòng chứa root trong /etc/passwd". Đây chính là triết lý Unix ở Bài 0 thành hiện thực: công cụ nhỏ + pipe = thao tác phức tạp.

Vài pipeline thực tế hay dùng:

# 5 dòng lỗi gần nhất trong log
grep -i error /var/log/syslog | tail -5

# top 3 IP xuất hiện nhiều nhất trong access log
cut -d ' ' -f1 access.log | sort | uniq -c | sort -rn | head -3

# có tiến trình nginx nào đang chạy không
ps aux | grep nginx | grep -v grep

Lưu ý: pipe chỉ nối stdout (không nối stderr). Muốn đẩy cả lỗi qua pipe, dùng 2>&1 trước | (hoặc |& trong bash).

Vừa xem vừa lưu: tee

Pipe đẩy dữ liệu đi tiếp, nên bạn không thấy nó. tee "rẽ đôi" luồng: vừa ghi ra file, vừa cho đi tiếp ra màn hình/pipe:

echo "ghi 2 noi" | tee out.txt

In "ghi 2 noi" ra màn hình ghi vào out.txt. Hữu ích khi muốn lưu lại output của một lệnh mà vẫn theo dõi được nó chạy. (tee -a để nối thêm thay vì ghi đè.)

🧹 Dọn dẹp

cd /tmp && rm -f real.txt out.txt err.txt f.txt all.txt t.txt out.txt

Tổng kết

Mỗi chương trình có ba luồng: stdin (fd 0), stdout (fd 1), stderr (fd 2). Bạn điều khiển chúng: >/>> ghi đè/nối stdout vào file, 2> cho stderr, 2>&1 gộp lỗi vào stdout, < cho stdin, /dev/null để vứt bỏ. Và | (pipe) nối stdout lệnh này vào stdin lệnh kia — biến các công cụ nhỏ ở Bài 5 thành pipeline mạnh. tee để vừa lưu vừa xem.

Hiểu mô hình này, bạn đọc được những dòng lệnh dài đáng sợ chỉ bằng cách lần theo luồng dữ liệu. Bài 7 chuyển sang một deep-dive khác mà bạn gặp mỗi ngày trên server: quyền truy cập file.