Dòng lệnh Unix mạnh nhờ triết lý "công cụ nhỏ ghép lại qua pipe". Nhưng để ghép đúng, phải hiểu một thứ nền tảng mà nhiều người dùng mỗi ngày mà chưa nắm rõ: ba luồng chuẩn (stdin, stdout, stderr) và cách chuyển hướng chúng. Không hiểu nó là dính đủ loại bẫy — điển hình là cmd 2>&1 > file mà lỗi vẫn ra màn hình chứ không vào file. Bài mở đầu sê-ri Unix mổ xẻ và đo thật bằng bash trong container.

Ba luồng chuẩn

Mỗi tiến trình Unix mở sẵn ba "file descriptor" (fd):

  • fd 0 = stdin: đầu vào.
  • fd 1 = stdout: đầu ra bình thường (kết quả).
  • fd 2 = stderr: đầu ra lỗi — tách riêng khỏi stdout có chủ đích, để thông báo lỗi không lẫn vào dữ liệu bạn đang xử lý.

Chính vì stdout và stderr là hai luồng khác nhau, bạn có thể xử lý chúng riêng: đẩy kết quả vào file mà vẫn thấy lỗi trên màn hình, hay ngược lại.

a | b          # stdout của a -> stdin của b (pipe)
> file         # ghi stdout ra file (đè);  >> nối thêm
2> file        # ghi stderr ra file
2>/dev/null    # vứt lỗi đi
2>&1           # gộp stderr VÀO chỗ stdout đang trỏ tới

Ảnh chụp đoạn mã nền tối giải thích pipe và file descriptor ba luồng và cách nối chúng, mỗi tiến trình có ba luồng chuẩn fd 0 stdin đầu vào fd 1 stdout đầu ra bình thường fd 2 stderr đầu ra lỗi tách riêng để không lẫn vào dữ liệu stdout và stderr là hai luồng khác nhau lọc chuyển riêng được, pipe và chuyển hướng a gạch b stdout của a tới stdin của b lớn hơn file ghi stdout ra file đè lớn hơn lớn hơn nối thêm 2 lớn hơn file ghi stderr ra file 2 lớn hơn dev null vứt lỗi đi 2 lớn hơn và 1 gộp stderr vào chỗ stdout đang trỏ tới, bẫy kinh điển thứ tự của 2 lớn hơn và 1 cmd lớn hơn f 2 lớn hơn và 1 đúng stdout tới f rồi stderr theo stdout tới f cmd 2 lớn hơn và 1 lớn hơn f sai stderr theo stdout màn hình trước rồi mới đổi stdout sang f lỗi vẫn ra màn hình 2 lớn hơn và 1 nghĩa stderr trỏ tới nơi stdout đang trỏ vị trí quyết định

Hình 1: Ba luồng stdin(0)/stdout(1)/stderr(2). Pipe nối stdout→stdin; các toán tử chuyển hướng; và 2>&1 nghĩa "stderr trỏ tới nơi stdout đang trỏ" — nên vị trí của nó quyết định kết quả.

Đo thật: tách luồng và bẫy 2>&1

Chạy ls với một file có (cofile.txt) và một file không có (khongco.txt) — ls in tên file có ra stdout và thông báo lỗi ra stderr:

Ảnh chụp bảng kết quả đo thật nền tối chạy bash ls cofile.txt có khongco.txt không có, phần một pipe seq 1 5 gạch wc -l bằng 5 stdout của seq thành stdin của wc, phần hai tách stdout stderr ls 2 lớn hơn dev null ra cofile.txt chỉ stdout ls 1 lớn hơn dev null ra ls cannot access khongco.txt chỉ stderr, phần ba thứ tự 2 lớn hơn và 1 đúng vs sai ls lớn hơn out_dung.txt 2 lớn hơn và 1 file có 2 dòng stdout cộng stderr ls 2 lớn hơn và 1 lớn hơn out_sai.txt file có 1 dòng lỗi ra màn hình, phần bốn nội dung file chứng minh out_dung.txt ls cannot access khongco.txt cofile.txt cả hai out_sai.txt cofile.txt chỉ stdout

Hình 2: seq 1 5 | wc -l = 5 (pipe). 2>/dev/null giữ lại stdout (cofile.txt), 1>/dev/null giữ lại stderr (lỗi). > out_dung.txt 2>&1 cho file 2 dòng (cả hai); 2>&1 > out_sai.txt cho file 1 dòng (lỗi ra màn hình).

Kết quả làm rõ mọi thứ:

  • Pipe: seq 1 5 | wc -l cho 5 — stdout của seq (5 dòng) trở thành stdin của wc.
  • Tách luồng: ls ... 2>/dev/null vứt stderr, chỉ còn stdout (cofile.txt). ls ... 1>/dev/null vứt stdout, chỉ còn stderr (ls: cannot access 'khongco.txt'). Hai luồng độc lập hoàn toàn.
  • Bẫy thứ tự 2>&1 — điểm quan trọng nhất: ls ... > out_dung.txt 2>&1 cho file 2 dòng (cả kết quả lẫn lỗi); nhưng ls ... 2>&1 > out_sai.txt cho file chỉ 1 dòng (kết quả), còn lỗi ra màn hình. Vì sao? 2>&1 nghĩa là "cho stderr trỏ tới nơi stdout đang trỏ ngay lúc đó". Ở lệnh đúng, > out_dung.txt đổi stdout sang file trước, rồi 2>&1 khiến stderr cũng vào file. Ở lệnh sai, 2>&1 chạy trước (stdout còn đang là màn hình → stderr trỏ về màn hình), rồi mới > out_sai.txt đổi stdout sang file — stderr đã "chốt" ở màn hình. Thứ tự đọc từ trái sang phải, và vị trí quyết định.

Đánh đổi và lưu ý

&>file là lối tắt gộp cả hai (bash). Muốn cả stdout lẫn stderr vào file, bash có cmd &>file (tương đương > file 2>&1) — gọn và tránh bẫy thứ tự. Nhưng đây là cú pháp bash, không phải POSIX chuẩn; script cần chạy trên sh thuần thì dùng > file 2>&1.

Pipe chỉ nối stdout, không nối stderr. a | b chỉ đưa stdout của a sang b; stderr của a vẫn ra màn hình. Muốn pipe cả stderr: a 2>&1 | b (gộp stderr vào stdout trước khi pipe). Đây là lý do đôi khi lỗi "lọt" qua | grep — vì nó ở stderr.

Mỗi lệnh trong pipe là một tiến trình riêng. a | b | c chạy song song, dữ liệu chảy qua buffer. Hệ quả: biến gán trong lệnh cuối của pipe (trong subshell) không thấy được ở shell cha (echo x | read v; echo $v ra rỗng) — một bẫy hay gặp, sẽ gặp lại ở bài quoting/biến.

Ba ý mang về

  1. Unix có ba luồng tách biệt: stdin(0), stdout(1), stderr(2) — stderr tách riêng để lỗi không lẫn dữ liệu; đo thật, 2>/dev/null giữ stdout, 1>/dev/null giữ stderr.
  2. Thứ tự 2>&1 quyết định kết quả: > file 2>&1 gộp cả hai vào file (2 dòng), nhưng 2>&1 > file chỉ đưa stdout vào file (1 dòng), lỗi ra màn hình — vì 2>&1 trỏ stderr tới nơi stdout đang trỏ lúc đó.
  3. Pipe chỉ nối stdout: a | b không đưa stderr của a sang b (dùng a 2>&1 | b); dùng &>file (bash) để gộp gọn, và nhớ mỗi lệnh trong pipe là tiến trình riêng.

Nguồn

Phần sau ta vào công cụ lọc văn bản dùng nhiều nhất: grep và biểu thức chính quy — cơ bản vs mở rộng -E, các cờ -o -c -v, và cách đọc regex tham lam, đo trên file thật.