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:

  1. 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.
  2. 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)

Ảnh chụp đoạn mã nền tối minh hoạ quoting vì sao quên dấu ngoặc kép quanh biến làm hỏng script, shell làm gì sau khi thay biến word splitting cộng globbing sau khi thay đô f shell còn tách theo khoảng trắng word splitting rồi nở ký tự đại diện sao hỏi ngoặc vuông globbing trước khi gọi lệnh f bằng file co dau cach.txt ls đô f thành ls file co dau cach.txt 4 đối số lỗi ls đô f có quote thành 1 đối số đúng, quy tắc quote mọi lần dùng biến đô var đô a còng cp đô src đô dst không đô là vỡ khi tên có dấu cách for x in đô a còng giữ từng đối số nguyên vẹn đô sao và đô a còng không quote đều bị word splitting luôn đô a còng, nháy đơn vs nháy kép nháy kép đô bien đô lenh gạch chéo được nở nháy đơn giữ nguyên văn mọi thứ kể cả đô echo Xin chao đô ten ra Xin chao An echo nháy đơn Xin chao đô ten ra Xin chao đô ten đô không nở, bẫy chí mạng biến rỗng cộng không quote dd bằng rỗng rm -rf đô dd trên sao thành rm -rf trên sao thảm hoạ rm -rf đô dd có quote trên sao cộng kiểm biến rỗng trước khi xoá

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:

Ảnh chụp bảng kết quả chạy thật bash quoting output thật, một ls đô f vs ls đô f có quote file tên file co dau cach.txt ls đô f ra ls cannot access file No such file và co dau cach.txt 4 lỗi riêng ls đô f có quote ra file co dau cach.txt đúng, hai đếm số đối số thực sự truyền vào đô thăng đô f không quote so doi so bằng 4 đô f có quote so doi so bằng 1, ba globbing biến chứa sao bị nở thành tên file star bằng sao echo đô star ra file co dau cach.txt sao nở ra echo đô star có quote ra sao giữ nguyên, bốn nháy đơn vs kép đô chỉ nở trong nháy kép Xin chao đô ten có quote kép ra Xin chao An Xin chao đô ten nháy đơn ra Xin chao đô ten nháy đơn giữ nguyên đô

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ành file, co, dau, cach.txt và ls cố 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="*", khi echo $star shell 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ề

  1. Shell tách và nở biến không quote: đo thật, ls $f vớ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ố.
  2. 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.
  3. Thiếu quote có thể là thảm hoạ: rm -rf $var/* với biến rỗng nở thành rm -rf /* — luôn quote, kiểm biến rỗng, dùng set -u và shellcheck. Quy tắc vàng: quote gần như mọi lần dùng biến, đặc biệt "$@".

Nguồn

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.