This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Viết script Bash có vẻ rất đơn giản, nhưng vì Shell chạy trực tiếp với quyền hệ thống, một lỗi nhỏ—chẳng hạn như truy cập biến chưa định nghĩa hay phân tách từ sai—có thể dẫn đến việc mất mát dữ liệu nghiêm trọng. Áp dụng các nguyên tắc Lập trình Phòng thủ (Defensive Programming) sẽ giúp mã nguồn của bạn tự bảo vệ, gỡ lỗi chính xác và dừng hoạt động an toàn (fail-safe) khi gặp sự cố.
1. Trình cấu hình nghiêm ngặt: set -euo pipefail
Mặc định, shell hoạt động với mục tiêu duy trì hoạt động bằng mọi giá. Lỗi ở một câu lệnh sẽ bị bỏ qua và câu lệnh tiếp theo vẫn được chạy bình thường. Để buộc shell báo lỗi và dừng lập tức khi có sự cố, chúng ta sử dụng Unofficial Strict Mode:
set -euo pipefail
A. Phân tích bản chất các cờ cấu hình và cạm bẫy
-
-e(errexit): Thoát script ngay lập tức nếu có lệnh đơn lẻ trả về exit code khác 0.
Cạm bẫy: Theo đặc tả POSIX, cờ-esẽ không hoạt động nếu lệnh lỗi nằm trong câu lệnh điều kiện củaif,while,until, hoặc các phép kiểm tra logic ngắn mạch (ví dụ:grep "user" /etc/passwd || true). -
-u(nounset): Trả về lỗi và dừng script lập tức nếu gọi một biến chưa được định nghĩa (uninitialized variable). Điều này triệt tiêu hoàn toàn hiểm họa gõ nhầm tên biến dẫn đến việc xóa hoặc thao tác sai thư mục hệ thống.
Cách giải quyết với biến tùy chọn: Dùng cơ chế gán mặc định:VAR="${1:-default_value}". -
-o pipefail: Mặc định trong Linux, một pipeline nhưcommand1 | command2 | command3chỉ trả về exit code của câu lệnh cuối cùng (command3). Nếucommand1crash nghiêm trọng nhưngcommand3thành công, cả chuỗi vẫn được coi là thành công!pipefailép toàn bộ đường ống dẫn trả về mã lỗi của câu lệnh lỗi đầu tiên trong chuỗi.
So sánh set -e với Kiểm lỗi thủ công
| Phương pháp | Ưu điểm | Nhược điểm & Cạm bẫy | Hiệu quả thực tế |
|---|---|---|---|
Bật set -e
|
Đơn giản, tự động bảo vệ toàn bộ script mà không cần viết điều kiện kiểm tra cho từng dòng. | Có thể thoát đột ngột ở các câu lệnh không mong muốn (ví dụ lệnh kiểm tra status bình thường có exit code khác 0). Không cho phép xử lý logic lỗi tinh tế. | Khuyên dùng cho các script tự động hóa tuyến tính, deploy hệ thống. |
Kiểm tra thủ công (|| exit 1)
|
Kiểm soát hoàn toàn luồng nghiệp vụ. Cho phép ghi log lỗi chi tiết hoặc thực hiện cleanup cục bộ. | Code phình to, dễ bỏ sót lỗi ở các dòng lệnh nhỏ nếu lập trình viên quên viết kiểm tra status. | Khuyên dùng cho các script phức tạp, cần quản lý giao dịch hoặc khôi phục dữ liệu lỗi. |
2. Bảo mật biến, Word Splitting và biến IFS
Một trong những cạm bẫy lớn nhất trong Bash là cơ chế Word Splitting (Phân tách từ).
Khi shell biên dịch một biến không nằm trong dấu nháy kép, nó sẽ phân tách giá trị của biến đó thành
nhiều đối số dựa trên biến môi trường đặc biệt IFS (Internal Field Separator).
Mặc định, IFS chứa các ký tự: khoảng trắng (Space), phím Tab, và phím xuống dòng
(Newline).
TARGET_DIR="/data/backup files"
# Nếu không bao bọc nháy kép:
rm -rf $TARGET_DIR
# Hệ thống sẽ dịch thành: rm -rf "/data/backup" "files" -> Xóa nhầm thư mục /data/backup!
# Khai báo an toàn bắt buộc:
rm -rf "$TARGET_DIR"
Kỹ thuật thay đổi IFS an toàn
Khi cần lặp qua danh sách các dòng được phân tách bằng dấu phẩy hoặc ký tự xuống dòng (không muốn bị
khoảng trắng làm gián đoạn), ta có thể đổi tạm thời IFS và khôi phục lại:
# Lưu IFS mặc định
OLD_IFS="$IFS"
# Đổi IFS chỉ nhận ký tự xuống dòng
IFS=$'\n'
for line in $(cat csv_data.txt); do
echo "Dòng dữ liệu hoàn chỉnh: $line"
done
# Khôi phục IFS mặc định
IFS="$OLD_IFS"
3. Tín hiệu POSIX (Signals) & Cơ chế hoạt động của trap
Khi một script đang chạy, hệ thống hoặc người dùng có thể gửi các tín hiệu không đồng bộ (Signals)
thông qua lệnh kill hoặc phím tắt terminal. Lệnh built-in trap cho phép
script đăng ký các hàm callback để đón nhận và xử lý các tín hiệu này trước khi bị dừng.
Bảng danh sách tín hiệu hệ thống quan trọng
| Số hiệu tín hiệu | Tên tín hiệu | Tác nhân kích hoạt | Khả năng bắt (Trap) | Hành vi xử lý khuyến nghị |
|---|---|---|---|---|
1 |
SIGHUP (Hangup) |
Terminal đóng đột ngột hoặc mất kết nối SSH. | Có thể | Reload file cấu hình hoặc ghi nhận tiến độ rồi dừng an toàn. |
2 |
SIGINT (Interrupt) |
Người dùng nhấn tổ hợp phím Ctrl + C từ bàn phím. |
Có thể | Dọn dẹp file tạm, đóng file descriptors và thoát lập tức. |
9 |
SIGKILL (Kill) |
Lệnh kill -9 ép tiến trình dừng ngay lập tức ở mức Kernel. |
Không thể bắt | Không thể xử lý. Tiến trình bị buộc dừng lập tức mà không giải phóng tài nguyên. |
15 |
SIGTERM (Terminate) |
Tín hiệu yêu cầu dừng mặc định từ lệnh kill hoặc shutdown OS. |
Có thể | Được ưu tiên tối đa để thực hiện quá trình tắt máy an toàn (Graceful Shutdown). |
| - | EXIT |
Sự kiện giả lập của Bash khi script kết thúc bình thường hoặc lỗi. | Có thể | Luôn luôn dùng để dọn dẹp file tạm (Cleanup routine). |
Cơ chế dọn dẹp tài nguyên tự động
Sử dụng EXIT trong trap để bảo đảm tài nguyên rác luôn được dọn dẹp sạch sẽ:
# Tạo thư mục tạm an toàn
TEMP_DIR=$(mktemp -d -t "sys_deploy_XXXXXX")
cleanup() {
echo "Bắt được sự kiện kết thúc! Đang xóa thư mục tạm: $TEMP_DIR"
rm -rf "$TEMP_DIR"
}
# Đăng ký bẫy tín hiệu EXIT
trap cleanup EXIT
4. Ngăn chặn trùng lặp tiến trình (Single Instance Locks)
Khi cấu hình cronjob chạy script định kỳ, nếu chu kỳ tiếp theo kích hoạt trong khi tiến trình trước đó chưa xử lý xong (do nghẽn mạng, ổ cứng ghi chậm), hệ thống sẽ bị chồng lấn tiến trình gây thắt nút cổ chai (race conditions) hoặc hỏng dữ liệu.
Bảng so sánh cơ chế Khóa tiến trình (Locking Mechanism)
| Cơ chế Khóa | Cách triển khai | Ưu điểm | Nhược điểm & Rủi ro |
|---|---|---|---|
| Tạo file khóa thường | Kiểm tra [ -f lock.pid ] rồi tạo file ghi PID. |
Dễ viết, tương thích với tất cả mọi hệ thống UNIX. | Không an toàn (Race Condition): Phép kiểm tra sự tồn tại và phép ghi file là hai bước tách biệt. Tiến trình khác có thể chen vào giữa hai bước này. Dễ để lại file rác nếu hệ thống mất điện đột ngột. |
| Khóa thư mục (mkdir) | Gọi lệnh mkdir lock_dir trực tiếp. |
An toàn nguyên tử (Atomic): Thao tác mkdir diễn ra ở mức nhân Kernel, chỉ duy nhất 1 tiến trình được tạo thư mục thành công tại một thời điểm. |
Vẫn để lại thư mục khóa nếu script crash đột ngột mà không kịp gọi dọn dẹp trong
trap.
|
Hệ thống flock
|
Sử dụng lệnh flock -n 9 liên kết với file descriptor. |
Hoàn hảo nhất: Khóa được giải phóng tự động bởi Kernel ngay khi tiến trình hoặc file descriptor bị đóng (dù máy chủ mất điện đột ngột). |
Cần hệ thống cài đặt sẵn tiện ích flock (không có sẵn trong một số bản phân phối
tối giản).
|
Triển khai Single Instance an toàn tuyệt đối với flock
# Mở FD 9 trỏ tới file khóa
exec 9>/var/run/my_process.lock
# Thử lấy khóa độc quyền trên FD 9, thoát ngay lập tức (-n) nếu đã bị khóa
if ! flock -n 9; then
echo "Lỗi: Một tiến trình khác đang thực thi script này!" >&2
exit 1
fi
echo "Đã lấy khóa thành công! Đang xử lý công việc chính..."
sleep 10 # Giả lập công việc dài
5. Trắc nghiệm ôn tập
Trắc nghiệm 1: Cấu hình set -u
Điều gì xảy ra khi bạn chạy lệnh echo $UNDEFINED trong một script có bật cấu hình
set -u?
Trắc nghiệm 2: Lệnh trap
Sự kiện EXIT trong lệnh trap sẽ được kích hoạt khi nào?
Tải file code thực hành minh họa bài học
Tập tin script tổng hợp các kỹ thuật phòng thủ, bẫy lỗi và validate tham số an toàn trong bài học:
Tải về defensive_scripting.sh
Comments
Bình luận