Bạn thêm set -e vào đầu script với niềm tin "giờ nó sẽ dừng ngay khi có lỗi". Đó là một trong những niềm tin sai lầm phổ biến nhất trong shell. set -e có dừng script khi một lệnh lỗi — nhưng với hàng loạt ngoại lệ khiến lỗi vẫn lọt qua âm thầm, và nguy hiểm nhất là cách shell tính exit code của một pipe. Kết quả: một script trông "phòng thủ" vẫn chạy tiếp trên dữ liệu hỏng và thất bại theo cách khó lần. Bài này (phần 8 loạt Unix) chạy thật để bạn thấy chính xác set -e bắt gì, bỏ sót gì, và cách viết script thật sự dừng đúng lúc.
Exit code: ngôn ngữ thành–bại của shell
Mỗi lệnh khi kết thúc trả về một exit code (0–255): 0 là thành công, khác 0 là lỗi. Shell lưu code của lệnh vừa chạy trong biến $?. Đo thật:
true; echo $? # 0
false; echo $? # 1
ls /khong-ton-tai 2>/dev/null; echo $? # 2
grep xyz file; echo $? # 1 (KHÔNG khớp — không phải lỗi thật)
Điểm quan trọng: mỗi lệnh có quy ước exit code riêng. grep trả 1 khi không tìm thấy (bình thường) và 2 khi lỗi thật (file không đọc được). Coi mọi giá trị khác 0 là "hỏng" sẽ khiến script hiểu nhầm — grep không khớp không có nghĩa là script phải thất bại.

Hình 1: Exit code 0=thành công, khác 0=lỗi (đọc bằng $?); set -e dừng khi lệnh lỗi nhưng bỏ qua lỗi trong if/||/&&/giữa pipe; bẫy pipe lấy exit code lệnh cuối; bộ ba set -euo pipefail.
set -e: dừng ở đâu, và KHÔNG dừng ở đâu
set -e khiến script thoát ngay khi một lệnh trả về khác 0. Đo thật: script in buoc 1, gặp false thì dừng — buoc 2 không bao giờ chạy, và script thoát với code 1. Đúng như kỳ vọng.
Nhưng đây là các ngoại lệ khiến nhiều người mất cảnh giác — set -e không dừng khi lệnh lỗi nằm ở:
- Điều kiện của
if:if false; then ...—falselỗi nhưng chỉ được coi là "điều kiện sai", không làm script thoát. - Vế trái của
||hoặc&&:false || echo "..."— lỗi được "xử lý" nên bỏ qua. - Bất cứ đâu trừ lệnh cuối trong pipe (xem dưới).
Đo thật xác nhận: một script set -e chứa if false; then... và false || echo vẫn chạy tiếp bình thường tới cuối. Điều này hữu ích (bạn cần kiểm điều kiện mà không thoát), nhưng cũng là nơi lỗi thật lọt qua nếu bạn tưởng set -e bắt tất cả.
Bẫy nguy hiểm nhất: exit code của pipe
Đây là chỗ nuốt lỗi âm thầm nhất. Exit code của một pipe a | b mặc định là exit code của lệnh cuối (b), bỏ qua hoàn toàn a. Đo thật:

Hình 2: Chạy thật — exit code true/false/ls/grep = 0/1/2/1; set -e dừng tại false (buoc 2 không chạy); lỗi trong if/|| không làm dừng; false | echo hi cho $?=0 không pipefail nhưng $?=1 khi set -o pipefail; set -u bắt biến chưa gán.
false | echo hi — false lỗi nhưng echo (lệnh cuối) thành công, nên $? = 0. Cả set -e cũng bị lừa: nó thấy pipe "thành công" và không dừng, dù bước đầu đã hỏng. Đây là lý do một pipeline như curl ... | tar ... có thể nuốt lỗi tải xuống và bạn tưởng mọi thứ ổn.
Cứu tinh là set -o pipefail: nó khiến exit code của pipe là mã lỗi đầu tiên khác 0 trong chuỗi. Đo thật: cùng false | echo hi giờ cho $? = 1 — lỗi được bắt.
Bộ ba an toàn: set -euo pipefail
Đầu mỗi script bash nghiêm túc nên có:
set -euo pipefail
-e: thoát khi lệnh lỗi.-u: báo lỗi khi dùng biến chưa gán (đo thật:$CHUA_GANlàm script dừng vớiunbound variablethay vì chạy tiếp với chuỗi rỗng — đúng bài học bẫyrm -rf $var/*ở phần trước).-o pipefail: bắt lỗi giữa pipe.
Đánh đổi cần cân nhắc
set -e không thay được xử lý lỗi tường minh. Vì nhiều ngoại lệ, set -e là lưới an toàn, không phải cơ chế bắt lỗi đầy đủ. Với những chỗ quan trọng, hãy kiểm exit code rõ ràng: if ! cmd; then echo "loi"; exit 1; fi, hoặc dùng trap ... ERR để chạy dọn dẹp khi có lỗi. Đừng phó thác toàn bộ độ tin cậy cho một dòng set -e.
Khi bạn muốn một lệnh được phép lỗi, nói rõ. Dưới set -e, một lệnh có thể lỗi hợp lệ (ví dụ grep không khớp) sẽ làm script thoát ngoài ý muốn. Cách đúng: cmd || true (bỏ qua có chủ đích) hoặc bọc trong if. Viết rõ ý định để người đọc biết đây là lỗi được phép, không phải sơ suất.
set -e hành xử khác nhau giữa shell và phiên bản. Quy tắc chính xác của set -e (đặc biệt với hàm, subshell, lệnh trong $(...)) tinh vi và từng khác nhau giữa các bản bash/POSIX sh. Đừng viết logic dựa vào sắc thái của set -e; giữ nó như lưới an toàn và xử lý lỗi quan trọng một cách tường minh để script chạy nhất quán mọi nơi.
Ba ý mang về
- Exit code là ngôn ngữ thành–bại: đo thật
0(true) /1(false) /2(ls lỗi); nhưng mỗi lệnh có quy ước riêng —greptrả1khi không khớp (bình thường), nên đừng coi mọi giá trị khác 0 là hỏng. set -ecó nhiều ngoại lệ: đo thật nó dừng ở lệnh lỗi trực tiếp, nhưng không dừng khi lỗi nằm trongif/||/&&; nguy hiểm nhất là pipe —false | echocho$?=0và qua mặt cảset -e.- Dùng
set -euo pipefail:-o pipefailbắt lỗi giữa pipe (đo thật$?đổi từ 0 sang 1),-ubắt biến chưa gán (unbound variable) — nhưng vẫn xử lý lỗi quan trọng tường minh, đừng phó thác hết choset -e.
Nguồn
- Bash manual — The Set Builtin (-e, -u, -o pipefail): https://www.gnu.org/software/bash/manual/bash.html#The-Set-Builtin
- man7.org — Exit status conventions: https://man7.org/linux/man-pages/man3/exit.3.html
- Greg's Wiki (BashFAQ 105) — Why doesn't set -e do what I expected?: https://mywiki.wooledge.org/BashFAQ/105
Phần sau ta xem cách gói và nén file: tar (gộp nhiều file thành một) kết hợp gzip/zstd — vì sao tar czf khác tar cf, và cái bẫy thứ tự cờ khiến bạn tạo nhầm archive rỗng.