Bash & Shell Scripting · Part 3 — Conditionals: if, test, [[ ]] & Exit Codes
Master bash conditionals: if runs on exit codes (0 = true), test vs [[ ]], file/string/numeric operators, && || short-circuit, and case patterns — bilingual, with exercises.
Đây là Phần 3 của series 10 bài về viết shell script chắc chắn. Ở Phần 1 bạn học exit code; ở Phần 2 bạn nắm biến và quoting. Giờ ta nối các ý đó vào logic rẽ nhánh — câu lệnh if và mọi thứ nuôi nó.
Quy tắc cốt lõi: if kiểm exit code, không phải boolean
Ngôn ngữ khác dùng true / false. Bash không có kiểu boolean. Thay vào đó, mọi lệnh trả về exit code khi kết thúc:
0→ thành công → điều kiện là đúng- Khác 0 → thất bại → điều kiện là sai
Khi bạn viết if some_command; then, bash chạy some_command rồi nhìn $?. Nếu $? là 0, khối then chạy; không thì khối else (nếu có) chạy.
#!/usr/bin/env bash
# A command that succeeds → exit 0 → "true"
if ls /tmp > /dev/null 2>&1; then
echo " /tmp exists and is readable"
else
echo " /tmp is missing or unreadable"
fi
# A command that fails → non-zero → "false"
if grep -q "ROOT" /etc/shadow 2>/dev/null; then
echo "found ROOT in shadow" # unlikely without root
else
echo "could not read /etc/shadow (expected for normal users)"
fi
Lệnh built-in true và false chỉ để trả 0 và 1:
if true; then echo "always runs"; fi
if false; then echo "never runs"; else echo "always the else"; fi
Cú pháp if / elif / else / fi
Hình dạng đầy đủ luôn đóng bằng fi (if viết ngược):
if command_or_test; then
# runs when exit code is 0
elif another_command_or_test; then
# runs when the first failed but this one succeeded
else
# runs when every preceding test failed
fi
Một kiểm tra deploy thực tế:
#!/usr/bin/env bash
set -euo pipefail
ENV="${1:-staging}"
if [[ "$ENV" == "production" ]]; then
echo "⚠ production deploy — extra checks"
if ! command -v docker &>/dev/null; then
echo "docker not found" >&2
exit 1
fi
elif [[ "$ENV" == "staging" ]]; then
echo "staging deploy — lighter checks"
else
echo "unknown env: $ENV" >&2
exit 2
fi
command -v docker là lệnh thật, exit code cho biết docker có trên PATH hay không. ! trong if ! command đảo exit code: thành công thành thất bại và ngược lại.
[ ] (test) vs [[ ]] (bash)
Cả hai đều đánh giá điều kiện, nhưng không thay thế được nhau.
| Tính năng | [ ] / test | [[ ]] (bash only) |
|---|---|---|
| Portable | POSIX — works in sh | Bash only |
| Tách từ | Có — rủi ro khi biến không quote | No — safer with "$var" |
&& / || inside test | Not in POSIX [ ] | Supported natively |
Regex =~ | No | Yes — [[ $s =~ ^[0-9]+$ ]] |
| Filename globs | Literal in [ ] | Patterns work in [[ ]] |
[ thực chất là lệnh test.
# These are equivalent:
test -f report.csv
[ -f report.csv ]
[[ là keyword bash — không phải lệnh — nên xử lý quoting và toán tử an toàn hơn:
file="my data.csv"
# [[ ]] — safe: no word-splitting on spaces
if [[ -f "$file" ]]; then
echo "found: $file"
fi
# [ ] with an empty variable can break or behave oddly
name=""
# [ $name = "admin" ] # ← ERROR: missing operand (word-splitting ate the empty token)
if [[ "$name" == "admin" ]]; then
echo "admin"
fi
Khuyến nghị: trong script bash có
#!/usr/bin/env bash, ưu tiên[[ ]]cho test. Chỉ dùng[ ]khi bắt buộc portable POSIX.
Toán tử test — file, chuỗi, số
Các toán tử dưới dùng được trong cả [ ] và [[ ]] trừ khi ghi chú.
Test file
| Operator | Ý nghĩa | Example |
|---|---|---|
-e | tồn tại (mọi loại) | [[ -e path ]] |
-f | file thường | [[ -f config.yml ]] |
-d | thư mục | [[ -d /var/log ]] |
-r | đọc được | [[ -r secrets.env ]] |
-w | ghi được | [[ -w . ]] |
-x | chạy được | [[ -x deploy.sh ]] |
-s | tồn tại và không rỗng | [[ -s error.log ]] |
#!/usr/bin/env bash
LOG="app.log"
if [[ ! -e "$LOG" ]]; then
touch "$LOG"
echo "created empty $LOG"
elif [[ -s "$LOG" ]]; then
lines=$(wc -l < "$LOG")
echo "$LOG has $lines lines"
else
echo "$LOG exists but is empty"
fi
Test chuỗi
| Operator | Ý nghĩa | Example |
|---|---|---|
-z | chuỗi rỗng | [[ -z "$reply" ]] |
-n | chuỗi không rỗng | [[ -n "$USER" ]] |
= / == | chuỗi bằng | [[ "$a" == "$b" ]] (== in [[ ]] only) |
!= | chuỗi khác | [[ "$branch" != "main" ]] |
< | nhỏ hơn theo thứ tự từ điển | [[ "$a" < "$b" ]] (bash [[ ]] only) |
read -r -p "Continue? [y/N] " answer
if [[ -z "$answer" || "$answer" =~ ^[Nn] ]]; then
echo "aborted"
exit 0
fi
echo "continuing…"
Test số
Dùng -eq, -ne, -lt, -le, -gt, -ge — không dùng = hay == cho số:
| Operator | Ý nghĩa |
|---|---|
-eq | equal |
-ne | not equal |
-lt | less than |
-le | less or equal |
-gt | greater than |
-ge | greater or equal |
count=12
if [[ $count -lt 10 ]]; then
echo "too few"
elif [[ $count -ge 10 && $count -le 100 ]]; then
echo "just right"
else
echo "too many"
fi
Vì sao so sánh số và chuỗi khác nhau
[[ "10" < "2" ]] là đúng vì < so sánh chuỗi theo thứ tự từ điển ("1" đứng trước "2"). [[ 10 -lt 2 ]] là sai vì -lt so sánh số nguyên. Với số học trong bash 4+, $(( )) thường rõ hơn:
a=10
b=2
if (( a > b )); then echo "10 is greater than 2"; fi
&& và || — chuỗi short-circuit
Các toán tử này nối lệnh, không chỉ test:
- chạy
cmd2chỉ khicmd1thành công (exit0) - chạy
cmd2chỉ khicmd1thất bại (khác 0)
# Create parent dir only if mkdir for the leaf fails
mkdir -p ./data/cache || { echo "cannot create cache dir" >&2; exit 1; }
# Only run tests when the config file exists
[[ -f Makefile ]] && make test
# Default value pattern (from Part 2)
: "${PORT:=3000}" # assign 3000 only if PORT was unset or empty
Trong [[ ]], && và || gộp test mà không cần lồng thêm if:
if [[ -n "$1" && -f "$1" && -r "$1" ]]; then
echo "processing readable file: $1"
fi
case — khớp mẫu
Khi rẽ nhánh theo nhiều giá trị rời rạc, case gọn hơn chuỗi elif:
#!/usr/bin/env bash
action="${1:-help}"
case "$action" in
start|up)
echo "starting services…"
;;
stop|down)
echo "stopping services…"
;;
restart)
echo "restarting…"
;;
help|--help|-h)
echo "usage: $0 {start|stop|restart}"
;;
*)
echo "unknown action: $action" >&2
exit 1
;;
esac
Mẫu là glob, không phải regex: * khớp bất kỳ, ? khớp một ký tự, [abc] khớp một lớp ký tự. Mỗi nhánh kết thúc bằng ;;. Nhánh *) là mặc định (như else).
# Match file extensions
filename="report.tar.gz"
case "$filename" in
*.tar.gz) echo "compressed tarball" ;;
*.tar) echo "tar archive" ;;
*.zip) echo "zip archive" ;;
*) echo "unknown type" ;;
esac
Gộp lại: script health-check nhỏ
#!/usr/bin/env bash
set -euo pipefail
URL="${1:-http://localhost:3000/health}"
TIMEOUT=5
if ! command -v curl &>/dev/null; then
echo "curl is required" >&2
exit 1
fi
http_code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time "$TIMEOUT" "$URL" || echo "000")
case "$http_code" in
200)
echo "OK $URL → $http_code"
exit 0
;;
000)
echo "FAIL $URL → timeout or connection error" >&2
exit 1
;;
*)
echo "FAIL $URL → HTTP $http_code" >&2
exit 1
;;
esac
Mọi kiểm tra ở đây đều dựa exit code: command -v, curl, case, và exit cuối.
Lỗi người mới hay mắc
- Dùng
=hoặc==cho số —[ "$a" = "$b" ]so sánh chuỗi; dùng-eqcho số nguyên.[[ $a = $b ]]với số không quote tình cờ chạy nhưng[[ $a -eq $b ]]rõ ràng hơn. - Thiếu khoảng trắng trong
[ ]—[ -f file]sai;[ -f file ]đúng.[ "$x"="y" ]là lỗi cú pháp; cần khoảng trắng quanh=. - Biến không quote trong
[ ]— nếu$namerỗng,[ $name = admin ]thành[ = admin ]và lỗi. Luôn"$name". - Dùng
==để gán trong[ ]—[ "$x" == "y" ]là test;x==ykhông có khoảng trắng không phải cú pháp test hợp lệ. Gán làx=ybên ngoài ngoặc. - Tưởng
if [ -f $file ]chạy khi$filecó khoảng trắng — phải quote:[[ -f "$file" ]].
Bài tập
Thử từng bài trước khi mở lời giải.
- Viết
check-port.shnhận số port, dùng test số[[]]invalidnếu port từ 1 đến 65535, không thìinvalidvà exit1. - Viết script nhận đường dẫn file và in một dòng:
missing,empty, hoặcreadable (N bytes)bằng toán tử-e,-s,-r. - Viết lại kiểm tra bài 1 bằng
case:80,443,8080là “well-known”, còn lại trong khoảng là “custom”.
Lời giải
#!/usr/bin/env bash
# check-port.sh
port="${1:-}"
if [[ -z "$port" || ! "$port" =~ ^[0-9]+$ ]]; then
echo "invalid"
exit 1
fi
if [[ "$port" -ge 1 && "$port" -le 65535 ]]; then
echo "valid"
else
echo "invalid"
exit 1
fi#!/usr/bin/env bash
# file-status.sh
path="${1:-}"
if [[ ! -e "$path" ]]; then
echo "missing"
elif [[ ! -s "$path" ]]; then
echo "empty"
elif [[ -r "$path" ]]; then
bytes=$(wc -c < "$path" | tr -d ' ')
echo "readable ($bytes bytes)"
else
echo "missing" # exists but not readable — treat as inaccessible
exit 1
fi#!/usr/bin/env bash
# port-classify.sh
port="${1:-}"
if [[ -z "$port" || ! "$port" =~ ^[0-9]+$ || "$port" -lt 1 || "$port" -gt 65535 ]]; then
echo "invalid"
exit 1
fi
case "$port" in
80|443|8080)
echo "well-known"
;;
*)
echo "custom"
;;
esacBài 1 dùng -ge / -le số trong [[ ]] sau khi xác nhận input là chữ số. Bài 2 xâu -e, -s, và -r theo thứ tự. Bài 3 dùng case cho port “well-known” rời rạc với nhánh * mặc định.
Điều cốt lõi
if không hỏi “cái này đúng không?” — nó hỏi “lệnh vừa rồi thành công chưa?”. Nắm [[ ]] cho test bash, chọn đúng toán tử (file -f, chuỗi ==, số -eq), và dùng case khi khớp mẫu. Short-circuit bằng && / || để script phẳng và dễ đọc.