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.

Ảnh chụp đoạn mã nền tối minh hoạ exit code và set -e vì sao script an toàn vẫn nuốt lỗi, exit code mọi lệnh trả một số 0 tới 255 0 thành công khác 0 lỗi đọc lệnh vừa chạy bằng đô hỏi mỗi lệnh có quy ước riêng grep trả 1 khi không khớp không phải lỗi thật 2 khi lỗi thật đừng coi mọi khác 0 là hỏng cmd và và echo OK hoặc hoặc echo LOI rẽ nhánh theo exit code, set -e dừng script khi một lệnh lỗi nhưng có ngoại lệ set -e thoát ngay khi có lệnh trả khác 0 không dừng khi lệnh lỗi nằm trong if cmd then điều kiện của if cmd hoặc hoặc fallback vế trái của hoặc và cmd pipe cmd2 giữa pipe, bẫy pipe exit code bằng lệnh cuối lỗi giữa bị nuốt false pipe echo hi đô hỏi bằng 0 vì echo lệnh cuối thành công set -o pipefail đô hỏi bằng mã lỗi đầu tiên trong pipe false pipe echo hi đô hỏi bằng 1 giờ bắt được lỗi của false, bộ ba an toàn cho đầu script set -euo pipefail -e thoát khi lệnh lỗi -u lỗi khi dùng biến chưa gán -o pipefail bắt lỗi giữa pipe

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 ... — false lỗ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:

Ảnh chụp bảng kết quả chạy thật bash exit code và set -e output thật, một exit code của vài lệnh đô hỏi true 0 false 1 ls file lạ 2 grep không khớp 1 1 là không tìm thấy khác lỗi thật, hai set -e dừng ngay tại lệnh lỗi buoc 1 false lỗi dừng buoc 2 không bao giờ chạy exit script 1, ba ngoại lệ lỗi trong if hoặc hoặc không làm set -e dừng bat loi bang hoặc hoặc van chay tiep binh thuong script không thoát, bốn bẫy pipe cộng pipefail false pipe echo hi không pipefail đô hỏi 0 lỗi bị nuốt false pipe echo hi có pipefail đô hỏi 1 bắt được, năm set -euo pipefail -u bắt biến chưa gán line 3 CHUA_GAN unbound variable exit 1 dừng ngay thay vì chạy tiếp với giá trị rỗng

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_GAN làm script dừng với unbound variable thay vì chạy tiếp với chuỗi rỗng — đúng bài học bẫy rm -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ề

  1. 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 — grep trả 1 khi không khớp (bình thường), nên đừng coi mọi giá trị khác 0 là hỏng.
  2. set -e có 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 trong if/||/&&; nguy hiểm nhất là pipe — false | echo cho $?=0 và qua mặt cả set -e.
  3. Dùng set -euo pipefail: -o pipefail bắt lỗi giữa pipe (đo thật $? đổi từ 0 sang 1), -u bắ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 cho set -e.

Nguồn

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.