jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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 $?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 truefalse chỉ để trả 01:

if true; then echo "always runs"; fi
if false; then echo "never runs"; else echo "always the else"; fi
[[ -f file.txt ]] $? = 0 ? then (true / exists) exit code 0 = success else (false / missing) non-zero = failure
A test like [[ -f file.txt ]] runs as a command; exit code 0 branches to then, non-zero to else

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)
PortablePOSIX — works in shBash only
Tách từCó — rủi ro khi biến không quoteNo — safer with "$var"
&& / || inside testNot in POSIX [ ]Supported natively
Regex =~NoYes — [[ $s =~ ^[0-9]+$ ]]
Filename globsLiteral 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ả [ ][[ ]] trừ khi ghi chú.

Test file

OperatorÝ nghĩaExample
-etồn tại (mọi loại)[[ -e path ]]
-ffile thường[[ -f config.yml ]]
-dthư mục[[ -d /var/log ]]
-rđọc được[[ -r secrets.env ]]
-wghi được[[ -w . ]]
-xchạy được[[ -x deploy.sh ]]
-stồ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ĩaExample
-zchuỗi rỗng[[ -z "$reply" ]]
-nchuỗ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
-eqequal
-nenot equal
-ltless than
-leless or equal
-gtgreater than
-gegreater 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" ]]đúng< so sánh chuỗi theo thứ tự từ điển ("1" đứng trước "2"). [[ 10 -lt 2 ]]sai-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

&&|| — chuỗi short-circuit

Các toán tử này nối lệnh, không chỉ test:

  • chạy cmd2 chỉ khi cmd1 thành công (exit 0)
  • chạy cmd2 chỉ khi cmd1 thấ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 [[ ]], &&|| 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 -eq cho 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 $name rỗng, [ $name = admin ] thành [ = admin ] và lỗi. Luôn "$name".
  • Dùng == để gán trong [ ][ "$x" == "y" ] là test; x==y không có khoảng trắng không phải cú pháp test hợp lệ. Gán là x=y bên ngoài ngoặc.
  • Tưởng if [ -f $file ] chạy khi $file có 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.

  1. Viết check-port.sh nhận số port, dùng test số [[ ]] in valid nếu port từ 1 đến 65535, không thì invalid và exit 1.
  2. Viết script nhận đường dẫn file và in một dòng: missing, empty, hoặc readable (N bytes) bằng toán tử -e, -s, -r.
  3. Viết lại kiểm tra bài 1 bằng case: 80, 443, 8080 là “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"
    ;;
esac

Bà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.