Nếu phải chọn một lỗi shell gây nhiều bug (và tai nạn) nhất, đó chắc chắn là quên quote biến: viết $f thay vì "$f". Script chạy hoàn hảo trên máy bạn — cho tới khi gặp một tên file có dấu cách, một biến rỗng, hay một giá trị chứa dấu *, và rồi nó hỏng theo những cách khó hiểu, đôi khi phá huỷ. Nguyên nhân nằm ở cách shell xử lý một dòng lệnh sau khi thay biến. Bài này (phần 7 loạt Unix) chạy thật để bạn thấy chính xác điều gì xảy ra và vì sao dấu ngoặc kép lại quan trọng đến vậy.
Shell làm gì sau khi thay biến
Đây là mấu chốt mà nhiều người bỏ qua. Khi shell gặp $f, nó không chỉ thay giá trị vào — nó còn làm thêm hai bước trước khi gọi lệnh:
- Word splitting: tách kết quả theo khoảng trắng (thực ra theo biến
IFS) thành nhiều từ riêng biệt. - Globbing: nở các ký tự đại diện
* ? [ ]thành danh sách tên file khớp.
Dấu ngoặc kép "..." tắt cả hai bước này cho phần bên trong. Đó là lý do "$f" giữ giá trị nguyên vẹn thành đúng một đối số, còn $f thì không.
f="file co dau cach.txt"
ls $f # -> ls file co dau cach.txt (shell tách thành 4 đối số!)
ls "$f" # -> ls "file co dau cach.txt" (1 đối số, đúng)

Hình 1: Sau khi thay biến, shell còn word splitting (tách theo khoảng trắng) và globbing (nở *); dấu ngoặc kép tắt cả hai. Quy tắc: quote mọi lần dùng biến ("$var", "$@"); phân biệt nháy đơn/kép; và bẫy rm -rf $var/*.
Đo thật: một tên file thành bốn đối số
Với file thật tên "file co dau cach.txt", kết quả đo được:

Hình 2: Chạy thật — ls $f báo 4 lỗi riêng (file, co, dau, cach.txt), ls "$f" in đúng tên; $# đếm 4 đối số vs 1; biến star="*" khi echo $star nở thành tên file, "$star" giữ nguyên *; $ten chỉ nở trong nháy kép.
ls $f→ 4 lỗi: shell tách tên thànhfile,co,dau,cach.txtvàlscố mở từng cái. Đếm bằng$#xác nhận: không quote = 4 đối số, có quote = 1 đối số. Đây chính là lý do script "chạy tốt" bỗng hỏng khi ai đó đặt tên file có dấu cách.- Globbing: biến
star="*", khiecho $starshell nở dấu*thành danh sách file trong thư mục;echo "$star"giữ nguyên*. Nếu bạn lưu một mẫu vào biến rồi dùng không quote, nó có thể bị nở ngoài ý muốn.
Nháy đơn vs nháy kép
Hai loại nháy làm việc khác nhau:
- Nháy kép
"...": cho phép nở$bien,$(lenh), và\— nhưng tắt word splitting và globbing. Dùng khi bạn muốn thay biến nhưng giữ giá trị nguyên. - Nháy đơn
'...': giữ nguyên văn mọi thứ, kể cả$. Không gì được nở.
Đo thật: echo "Xin chao $ten" → Xin chao An (nở $ten); echo 'Xin chao $ten' → Xin chao $ten (giữ nguyên). Dùng nháy đơn khi muốn một chuỗi đúng như viết (regex, mật khẩu tạm, chuỗi có $).
Cái bẫy chí mạng: biến rỗng không quote
Đây là nơi thiếu quote chuyển từ "bug" thành "thảm hoạ". Xét:
duong_dan="" # biến chưa gán hoặc lỗi logic
rm -rf $duong_dan/* # -> rm -rf /* (!!!)
Vì $duong_dan rỗng và không quote, dòng trên nở thành rm -rf /* — xoá sạch từ thư mục gốc. (Trong demo mình chỉ in ra, tuyệt đối không chạy.) Đã có những sự cố mất dữ liệu thật xảy ra đúng theo kiểu này. Phòng thủ: luôn quote (rm -rf "$duong_dan"/*) và kiểm biến rỗng trước ([ -n "$duong_dan" ] || exit 1), thậm chí dùng set -u để shell báo lỗi khi dùng biến chưa gán.
Đánh đổi cần cân nhắc
Quy tắc đơn giản: quote gần như mọi lần dùng biến. "$var", "$@", "$(lenh)". Trường hợp cố ý không quote (muốn word splitting hoặc globbing xảy ra) là ngoại lệ hiếm và nên có chú thích. Khi nghi ngờ, cứ quote — thừa quote hầu như không bao giờ gây hại, thiếu quote thì có.
"$@" khác "$*" và khác $@. Truyền lại đối số cho lệnh khác thì luôn dùng "$@" (giữ từng đối số nguyên vẹn). "$*" gộp tất cả thành một chuỗi; $@ không quote bị word splitting. Sai chỗ này là script wrapper hỏng khi đối số có dấu cách.
Dùng shellcheck để bắt lỗi tự động. Con người quên quote là chuyện thường; công cụ shellcheck quét script và cảnh báo đúng những chỗ thiếu quote, biến chưa gán, và các bẫy khác. Chạy nó trong CI là cách rẻ nhất để chặn cả một lớp bug shell trước khi lên production.
Ba ý mang về
- Shell tách và nở biến không quote: đo thật,
ls $fvới tên có dấu cách thành 4 đối số lỗi (word splitting), và biến chứa*bị nở thành tên file (globbing). Dấu ngoặc kép"$f"tắt cả hai — giữ giá trị thành đúng một đối số. - Nháy đơn giữ nguyên văn, nháy kép cho nở biến: đo thật
"$ten"→An,'$ten'→$ten; chọn loại nháy theo việc bạn có muốn thay biến hay không. - Thiếu quote có thể là thảm hoạ:
rm -rf $var/*với biến rỗng nở thànhrm -rf /*— luôn quote, kiểm biến rỗng, dùngset -uvàshellcheck. Quy tắc vàng: quote gần như mọi lần dùng biến, đặc biệt"$@".
Nguồn
- Bash manual — Quoting / Word Splitting: https://www.gnu.org/software/bash/manual/bash.html#Quoting
- ShellCheck — SC2086: Double quote to prevent globbing and word splitting: https://www.shellcheck.net/wiki/SC2086
- Greg's Wiki (BashFAQ) — Quotes: https://mywiki.wooledge.org/Quotes
Phần sau ta xem cách shell báo thành công/thất bại: exit code, và vì sao set -e cùng set -o pipefail không "bắt mọi lỗi" như nhiều người tưởng — cùng cách viết script dừng đúng lúc khi có lỗi.