This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.

Luồng điều khiển (control flow) cho phép script của bạn đưa ra quyết định và lặp lại các tác vụ. Đây là bước chuyển từ việc gõ lệnh đơn lẻ sang viết chương trình thực sự. Bài này bao gồm exit codes, điều kiện if/case, và tất cả các vòng lặp trong Bash.

1. Exit Codes & $?

Mọi lệnh trong Unix đều trả về một exit code (mã thoát) — một số nguyên từ 0 đến 255. Quy ước: 0 = thành công, khác 0 = lỗi.

exit_codes.sh
#!/bin/bash

# $? chứa exit code của lệnh vừa chạy
ls /tmp
echo $?         # 0 (thành công)

ls /nonexistent_dir
echo $?         # 2 (lỗi: không tìm thấy)

# Lệnh true và false
true
echo $?         # 0
false
echo $?         # 1

# && (AND) — chạy lệnh tiếp theo CHỈ KHI lệnh trước thành công
mkdir mydir && cd mydir && echo "Đã tạo và vào thư mục"

# || (OR) — chạy lệnh tiếp theo CHỈ KHI lệnh trước thất bại
cd /nonexistent || echo "Không thể vào thư mục!"

# Kết hợp && và ||  (pattern phổ biến)
ping -c 1 google.com &> /dev/null && echo "Online" || echo "Offline"

# Exit codes thường gặp:
# 0   — thành công
# 1   — lỗi chung
# 2   — sai cú pháp / argument
# 126 — file không thể thực thi (permission denied)
# 127 — command not found
# 128+N — bị kill bởi signal N (ví dụ: 130 = Ctrl+C = SIGINT)

2. Điều kiện if/elif/else

Bash có hai cách kiểm tra điều kiện: [ ] (test command truyền thống, POSIX) và [[ ]] (mở rộng của Bash, hỗ trợ regex, glob, và toán tử logic &&/||).

conditionals.sh
#!/bin/bash

# === Cú pháp if cơ bản ===
age=20

if [[ $age -ge 18 ]]; then
  echo "Đủ tuổi bầu cử"
elif [[ $age -ge 16 ]]; then
  echo "Gần đủ tuổi"
else
  echo "Chưa đủ tuổi"
fi

# === So sánh số (dùng trong [ ] và [[ ]]) ===
# -eq (equal), -ne (not equal), -lt (less than)
# -le (less or equal), -gt (greater than), -ge (greater or equal)
if [[ 10 -gt 5 ]]; then echo "10 > 5"; fi

# === So sánh chuỗi ===
name="Bash"
if [[ $name == "Bash" ]]; then echo "Đúng là Bash"; fi
if [[ $name != "Zsh" ]]; then echo "Không phải Zsh"; fi
if [[ -z "$empty_var" ]]; then echo "Biến rỗng hoặc chưa set"; fi   # -z: zero length
if [[ -n "$name" ]]; then echo "Biến có giá trị"; fi                # -n: non-zero length

# === Kiểm tra file (rất phổ biến trong scripting) ===
if [[ -f "/etc/passwd" ]]; then echo "File tồn tại"; fi     # -f: regular file
if [[ -d "$HOME" ]]; then echo "Thư mục tồn tại"; fi       # -d: directory
if [[ -e "/tmp" ]]; then echo "Path tồn tại"; fi            # -e: exists (file hoặc dir)
if [[ -r "file.txt" ]]; then echo "Có quyền đọc"; fi       # -r: readable
if [[ -w "file.txt" ]]; then echo "Có quyền ghi"; fi       # -w: writable
if [[ -x "script.sh" ]]; then echo "Có quyền thực thi"; fi # -x: executable
if [[ -s "file.txt" ]]; then echo "File không rỗng"; fi    # -s: size > 0

# === Toán tử logic trong [[ ]] ===
if [[ $age -ge 18 && $name == "Bash" ]]; then
  echo "Đủ tuổi và tên là Bash"
fi

if [[ $age -lt 10 || $age -gt 60 ]]; then
  echo "Quá nhỏ hoặc quá lớn"
fi

# === Regex matching (chỉ trong [[ ]]) ===
email="[email protected]"
if [[ $email =~ ^[a-zA-Z0-9]+@[a-zA-Z]+\.[a-zA-Z]+$ ]]; then
  echo "Email hợp lệ"
fi

3. case Statement

Lệnh case thay thế chuỗi if/elif dài dòng khi cần so sánh một biến với nhiều pattern:

case_statement.sh
#!/bin/bash

# === Menu xử lý input ===
echo "Chọn hành động: (start/stop/restart/status)"
read -r action

case $action in
  start)
    echo "Đang khởi động service..."
    ;;
  stop)
    echo "Đang dừng service..."
    ;;
  restart)
    echo "Đang khởi động lại..."
    ;;
  status)
    echo "Service đang chạy."
    ;;
  *)
    echo "Hành động không hợp lệ: $action"
    exit 1
    ;;
esac

# === Pattern matching với wildcards ===
filename="photo.jpg"
case $filename in
  *.jpg|*.jpeg|*.png|*.gif)
    echo "Đây là file ảnh"
    ;;
  *.mp4|*.avi|*.mkv)
    echo "Đây là file video"
    ;;
  *.sh)
    echo "Đây là shell script"
    ;;
  *.tar.gz|*.zip|*.rar)
    echo "Đây là file nén"
    ;;
  *)
    echo "Loại file không xác định"
    ;;
esac

# === Case với character class ===
read -rp "Tiếp tục? (y/n): " answer
case $answer in
  [Yy]|[Yy][Ee][Ss])
    echo "Tiếp tục..."
    ;;
  [Nn]|[Nn][Oo])
    echo "Dừng lại."
    exit 0
    ;;
esac

4. Vòng lặp for

for_loops.sh
#!/bin/bash

# === for in list ===
for color in red green blue; do
  echo "Màu: $color"
done

# === for với brace expansion ===
for i in {1..5}; do
  echo "Số: $i"        # 1, 2, 3, 4, 5
done

for i in {0..20..5}; do
  echo "Bước: $i"      # 0, 5, 10, 15, 20
done

# === C-style for loop ===
for (( i=0; i<10; i++ )); do
  echo "Index: $i"
done

# === Lặp qua file (rất phổ biến) ===
for file in *.txt; do
  echo "Đang xử lý: $file"
  wc -l "$file"         # đếm số dòng
done

# Lặp qua thư mục con
for dir in */; do
  echo "Thư mục: $dir"
done

# === Lặp qua output lệnh ===
for user in $(cut -d: -f1 /etc/passwd | head -5); do
  echo "User: $user"
done

# === Lặp qua mảng ===
servers=("web01" "web02" "db01" "cache01")
for server in "${servers[@]}"; do
  echo "Kiểm tra $server..."
  # ping -c 1 "$server" &> /dev/null && echo "  OK" || echo "  DOWN"
done

5. Vòng lặp while/until & break/continue

while_loops.sh
#!/bin/bash

# === while loop cơ bản ===
count=1
while [[ $count -le 5 ]]; do
  echo "Lần lặp: $count"
  (( count++ ))
done

# === until loop (ngược với while: lặp CHO ĐẾN KHI điều kiện đúng) ===
num=1
until [[ $num -gt 5 ]]; do
  echo "Until: $num"
  (( num++ ))
done

# === Infinite loop + break ===
while true; do
  read -rp "Nhập 'quit' để thoát: " input
  if [[ $input == "quit" ]]; then
    echo "Tạm biệt!"
    break
  fi
  echo "Bạn nhập: $input"
done

# === continue — bỏ qua iteration hiện tại ===
for i in {1..10}; do
  if (( i % 2 == 0 )); then
    continue    # bỏ qua số chẵn
  fi
  echo "Số lẻ: $i"   # 1, 3, 5, 7, 9
done

# === Đọc file line-by-line (best practice) ===
while IFS= read -r line; do
  echo "Dòng: $line"
done < /etc/hostname

# Đọc file và xử lý từng dòng
line_num=0
while IFS= read -r line; do
  (( line_num++ ))
  # Bỏ qua dòng trống và comment
  [[ -z "$line" || "$line" =~ ^# ]] && continue
  echo "$line_num: $line"
done < config.txt

# === Đọc từ pipe (chú ý: chạy trong subshell!) ===
echo -e "apple\nbanana\ncherry" | while read -r fruit; do
  echo "Quả: $fruit"
done

# === while với counter để retry ===
max_retries=3
attempt=1
while [[ $attempt -le $max_retries ]]; do
  echo "Lần thử $attempt/$max_retries..."
  # Giả sử lệnh thất bại:
  if ping -c 1 example.com &> /dev/null; then
    echo "Thành công!"
    break
  fi
  (( attempt++ ))
  sleep 2
done

if [[ $attempt -gt $max_retries ]]; then
  echo "Thất bại sau $max_retries lần thử."
fi

6. Câu hỏi trắc nghiệm ôn tập

Trắc nghiệm 1: Exit Codes

Lệnh false && echo "A" || echo "B" sẽ in ra gì?

Trắc nghiệm 2: Test operators

Toán tử nào kiểm tra xem file có tồn tại VÀ là regular file (không phải thư mục)?

Trắc nghiệm 3: Đọc file

Cách nào là best practice để đọc file line-by-line trong Bash?

Tải file code thực hành minh họa bài học

File script tổng hợp các ví dụ về exit codes, điều kiện, case, và vòng lặp:

Tải về control_flow.sh

Related Articles

Bài viết liên quan trong series

Lesson 4: Functions & Scripts: Writing Your First Professional Script Bài 4: Functions & Scripts — Viết Script Chuyên Nghiệp Đầu Tiên Lesson 2: Variables, Strings & Arrays: Parameter Expansion & Arithmetic Bài 2: Biến, Chuỗi & Mảng — Parameter Expansion & Arithmetic Back to Bash Series Overview Quay lại Lộ trình Bash Series