Bài 02

Logic và điều khiển: if, case, loop, function, strict mode

06 tháng 5, 2026 · 60 phút đọc

Bài 02 — Logic và điều khiển: if, case, loop, function

Script đơn giản chạy thẳng 5 lệnh xong. Production script cần kiểm tra: file tồn tại? service đang chạy? user đúng quyền? → cần if/case/loop/function. Bài này.


Exit status — trạng thái thoát

Mỗi lệnh Linux trả về một trạng thái khi nó kết thúc bình thường hoặc bất thường. Có thể dùng giá trị này trong shell script để hiển thị thông báo lỗi hoặc thực hiện hành động.

Trong shell:

  • 0 = thành công (true).
  • Khác 0 (1-255) = lỗi (false).

Lưu ý ngược với toán: 0 = TRUE.

Dùng biến đặc biệt $? để lấy exit status của lệnh trước đó:

ls /tmp
echo $?     # 0 — thành công

ls /khong-ton-tai
echo $?     # 2 — file not found

Sử dụng exit command

Sử dụng exit để phát hiện lỗi và chỉ định script thành công. Đặt mã exit khác nhau cho mỗi loại lỗi để dễ debug:

#!/bin/bash

LOG_FILE="/var/log/script_errors.log"

if [ ! -f "/path/to/file" ]; then
    echo "$(date) - Lỗi: File không tồn tại!" >> "$LOG_FILE"
    exit 1
fi

if [ ! -d "/path/to/directory" ]; then
    echo "$(date) - Lỗi: Thư mục không tồn tại!" >> "$LOG_FILE"
    exit 2
fi

echo "Mọi thứ đều ổn!"
exit 0

Đọc log thấy "exit 2" → biết ngay là dir issue, không cần grep log.

Cron, monitoring tool đọc exit status để biết script success/fail.


Lệnh test — kiểm tra điều kiện

Lệnh test được sử dụng để kiểm tra các loại file và so sánh giá trị. Test được dùng trong thực thi có điều kiện. Nó được sử dụng cho:

  • So sánh thuộc tính file
  • Thực hiện so sánh chuỗi
  • So sánh số học cơ bản

Ví dụ:

test -f /etc/resolv.conf && echo "File tồn tại" || echo "File không tồn tại"

test[ ] là 2 cách viết cùng 1 thứ:

test -f /etc/resolv.conf
[ -f /etc/resolv.conf ]    # tương đương

Cả hai trả exit status 0 nếu điều kiện đúng.

Comparison file

[ -f file ]      # tồn tại, là regular file
[ -d dir ]       # tồn tại, là directory
[ -e path ]      # tồn tại (file/dir/link...)
[ -r file ]      # readable
[ -w file ]      # writable
[ -x file ]      # executable
[ -s file ]      # tồn tại và size > 0
[ file1 -nt file2 ]   # file1 newer than file2

Comparison số

[ $a -eq $b ]    # bằng
[ $a -ne $b ]    # khác
[ $a -gt $b ]    # >
[ $a -ge $b ]    # >=
[ $a -lt $b ]    # <
[ $a -le $b ]    # <=

Comparison string

[ "$s1" = "$s2" ]    # bằng (1 dấu = trong [ ])
[ "$s1" != "$s2" ]   # khác
[ -z "$s" ]          # rỗng
[ -n "$s" ]          # không rỗng

⚠️ Luôn quote biến trong [ ]: [ -z "$var" ], không [ -z $var ]. Biến rỗng + không quote = syntax error.

[[ ]] — bash modern, khuyên dùng

[[ -f /etc/passwd ]]
[[ "$name" = "admin" ]]
[[ "$count" -gt 5 ]]

[[ ]] (double bracket) là Bash extension:

  • Không cần quote biến nghiêm ngặt như [ ].
  • Hỗ trợ regex: [[ "$ip" =~ ^[0-9]+\.[0-9]+ ]].
  • Hỗ trợ pattern: [[ "$file" == *.txt ]].
  • Logic operators dùng &&, || thay -a, -o.

Habit: dùng [[ ]] trừ khi script cần POSIX compatible (Alpine container minimal).


if — cấu trúc thực thi mã dựa trên điều kiện

if là cấu trúc thực thi mã dựa trên điều kiện.

Cú pháp:

if condition
then
    command1
    command2
    ...
fi

Hoặc với [[ ]]:

if [[ condition ]]; then
    command1
    command2
fi

Ví dụ:

read -p "Enter password: " pass
if [[ "$pass" = "jerry" ]]; then
    echo "Password verified."
fi

if..else..fi

if..else..fi cho phép đưa ra lựa chọn dựa trên sự thành công hay thất bại của lệnh.

Cú pháp:

if condition; then
    # nếu điều kiện đúng
    command1
else
    # nếu điều kiện sai
    command2
fi

Ví dụ:

read -p "Enter password: " pass
if [[ "$pass" = "jerry" ]]; then
    echo "Password verified."
else
    echo "Access denied."
fi

if lồng nhau (nested if)

if [[ -f "$config" ]]; then
    if [[ -r "$config" ]]; then
        echo "OK đọc được"
    else
        echo "File tồn tại nhưng không có quyền đọc" >&2
        exit 1
    fi
else
    echo "File không tồn tại" >&2
    exit 1
fi

Lồng > 2 cấp = code khó đọc → tách function hoặc dùng early return:

[[ -f "$config" ]] || { echo "Missing" >&2; exit 1; }
[[ -r "$config" ]] || { echo "Not readable" >&2; exit 1; }
echo "OK"

if..elif..else..fi

if..elif..else..fi cho phép script có nhiều khả năng và điều kiện khác nhau.

read -p "Enter a number: " n
if [[ $n -gt 0 ]]; then
    echo "$n is positive."
elif [[ $n -lt 0 ]]; then
    echo "$n is negative."
elif [[ $n -eq 0 ]]; then
    echo "$n is zero."
else
    echo "Oops! $n is not a number."
fi

Inline 1 dòng

[[ -f "$file" ]] && echo "exists" || echo "missing"

⚠️ Pattern A && B || C KHÔNG đúng if/else thuần. Nếu B fail (return non-zero), C vẫn chạy. Production dùng if/else đầy đủ.


case statement

Câu lệnh case trong Bash là công cụ mạnh để thực hiện so sánh và xử lý các trường hợp khác nhau hiệu quả, đặc biệt khi muốn kiểm tra một biến hoặc giá trị với nhiều khả năng. Hoạt động tương tự chuỗi if lồng nhau nhưng dễ đọc và bảo trì hơn.

Cú pháp:

case <biến hoặc biểu thức> in
    <mẫu 1>)
        # hành động nếu khớp mẫu 1
        ;;
    <mẫu 2>)
        # hành động nếu khớp mẫu 2
        ;;
    *)
        # hành động nếu không khớp mẫu nào (tùy chọn)
        ;;
esac
  • <biến/biểu thức>: cái muốn so sánh.
  • <mẫu n>: pattern muốn so sánh với giá trị.
  • ;;: kết thúc khối lệnh của mỗi trường hợp.
  • *: đại diện cho tất cả trường hợp không khớp (như else).

Ví dụ kinh điển — init script:

case "$1" in
    start)
        echo "Starting..."
        ;;
    stop)
        echo "Stopping..."
        ;;
    restart|reload)
        echo "Restarting..."
        ;;
    status)
        systemctl status myapp
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

Pattern matching mạnh:

case "$file" in
    *.txt)              echo "text file" ;;
    *.jpg|*.png|*.gif)  echo "image" ;;
    *.tar.gz|*.tgz)     echo "archive" ;;
    /etc/*)             echo "config file" ;;
esac

Thực hiện có điều kiện — AND, OR, NOT

Dựa vào exit status, có thể chain command có điều kiện:

AND && — chỉ chạy nếu lệnh trước thành công

mkdir /tmp/backup && cp file.txt /tmp/backup/
# cp chỉ chạy nếu mkdir OK

grep "^vivek" /etc/passwd && echo "Vivek found"
# echo chỉ chạy nếu grep tìm thấy

OR || — chỉ chạy nếu lệnh trước thất bại

systemctl is-active --quiet nginx || systemctl start nginx
# start chỉ chạy nếu is-active fail

grep "^vivek" /etc/passwd || echo "User vivek not found"
# echo chỉ chạy nếu grep không tìm thấy

NOT ! — toán tử boolean

! là toán tử boolean phủ định trạng thái của lệnh.

Trong Bash, lệnh thường trả về:

  • 0: Thành công (true).
  • Khác 0: Thất bại (false).

! đảo ngược:

  • Lệnh trả về 0, ! làm thành false.
  • Lệnh trả về khác 0, ! làm thành true.
if ! systemctl is-active --quiet nginx; then
    echo "nginx is down"
    systemctl start nginx
fi

Đọc: "nếu lệnh fail thì làm X".


Positional parameters — tham số vị trí

Tham số vị trí (Positional Parameters) trong Bash là các tham số hoặc đối số được truyền cho một script hoặc hàm khi nó được gọi. Được tham chiếu bằng các ký hiệu đặc biệt như $1, $2, $3... tương ứng thứ tự đối số.

Tham số Ý nghĩa
$0 Tên script đang chạy (bao gồm path nếu có)
$1, $2, ... Các tham số truyền cho script/hàm, bắt đầu từ thứ nhất
$# Số lượng tham số được truyền
$* Chuỗi chứa tất cả tham số, ngăn cách bởi dấu cách
"$@" Tương tự $*, nhưng giữ nguyên dấu ngoặc kép của từng tham số
$? Mã trạng thái thoát của lệnh trước đó
$$ PID của script/shell đang chạy
$! PID của tiến trình background cuối cùng

$@ mở rộng thành "$1" "$2" "$3" ... "$n" (giữ riêng).
$* mở rộng thành "$1y$2y$3y...$n" (gộp 1 string).

Habit: dùng "$@" mặc định khi forward args.

Ví dụ trong function:

my_function() {
    echo "First argument: $1"
    echo "Second argument: $2"
}

my_function argA argB
# First argument: argA
# Second argument: argB

Pattern check arg trong script:

if [[ $# -lt 2 ]]; then
    echo "Usage: $0 <source> <dest>" >&2
    exit 1
fi

src="$1"
dst="$2"

Vòng lặp (Bash Loops)

Bash loops cho phép thực hiện một chuỗi lệnh nhiều lần. Có 3 loại vòng lặp chính: for, while, và until. Mỗi loại có cách sử dụng và mục đích khác nhau.

for — biết trước số lần

Vòng lặp for được sử dụng để lặp qua một tập hợp giá trị hoặc một phạm vi.

# Range
for i in {1..5}; do
    echo "Lần $i"
done

# List
for service in nginx mysql redis; do
    systemctl status "$service"
done

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

# Iterate file
for file in /var/log/*.log; do
    echo "Processing $file"
done

⚠️ Đừng dùng for i in $(ls *.txt) — vỡ với tên file có dấu cách. Dùng glob trực tiếp.

Nested for — for trong for

#!/bin/bash
# In số 1-5, mỗi số 5 lần (5x5 grid)
for (( i = 1; i <= 5; i++ )); do          ### Outer ###
    for (( j = 1; j <= 5; j++ )); do      ### Inner ###
        echo -n "$i "
    done
    echo ""    # newline cuối row
done

Output:

1 1 1 1 1
2 2 2 2 2
3 3 3 3 3
4 4 4 4 4
5 5 5 5 5

Outer chạy 1 lần → inner chạy đủ 5 lần → outer kế tiếp. Use case: iterate qua list of lists (mỗi server × mỗi service).

while

Vòng lặp while thực thi các lệnh trong khối do khi điều kiện đúng (true). Vòng lặp này kiểm tra điều kiện trước khi thực thi.

count=1
while [[ $count -le 5 ]]; do
    echo "Số: $count"
    (( count++ ))
done

# Đọc file dòng theo dòng (pattern phổ biến)
while IFS= read -r line; do
    echo "Got: $line"
done < /etc/passwd

until

Vòng lặp until ngược với while. Tiếp tục thực thi các lệnh trong khối do cho đến khi điều kiện trở thành đúng (true).

count=1
until [[ $count -gt 5 ]]; do
    echo "Số: $count"
    (( count++ ))
done

# Wait service ready
until curl -sf http://localhost:8080/health > /dev/null; do
    echo "Waiting service ready..."
    sleep 2
done

break và continue

break: Dùng để thoát khỏi vòng lặp hiện tại ngay lập tức.
continue: Dùng để bỏ qua phần còn lại của vòng lặp và tiếp tục với lần lặp tiếp theo.

for i in {1..10}; do
    if [[ $i -eq 5 ]]; then
        echo "Gặp số 5, thoát"
        break
    fi
    if (( i % 2 == 0 )); then
        continue       # bỏ qua số chẵn
    fi
    echo "$i"
done
# Output: 1 3

select — vòng lặp tạo menu

Vòng lặp select là công cụ hữu ích để tạo các menu cho người dùng lựa chọn từ một danh sách các tùy chọn. Cho phép user chọn một giá trị từ tập hợp các tùy chọn đã cho, sau đó thực hiện hành động tương ứng.

Cú pháp:

select var in list
do
    command(s)
done

Ví dụ:

#!/bin/bash

PS3="Please select an option: "

select option in "Option 1" "Option 2" "Option 3" "Exit"
do
    case $option in
        "Option 1") echo "You selected Option 1" ;;
        "Option 2") echo "You selected Option 2" ;;
        "Option 3") echo "You selected Option 3" ;;
        "Exit")     echo "Exiting"; break ;;
        *)          echo "Invalid option" ;;
    esac
done

Function

Trong Bash Script, function (hàm) là cách để tổ chức mã thành các khối logic dễ quản lý, giúp tăng khả năng tái sử dụng và cải thiện tính rõ ràng của script.

  • Function trong Bash là một khối mã được đặt tên, có thể được gọi ở bất kỳ đâu trong script.
  • Function có thể nhận tham số và trả về giá trị thông qua các biến đặc biệt hoặc return.

Cú pháp:

function_name() {
    # Mã lệnh thực thi
}

# Hoặc
function function_name {
    # Mã lệnh thực thi
}

Gọi function:

function_name

Ví dụ:

greet() {
    echo "Hello, $1!"
}

greet "World"  # Output: Hello, World!

Function trong Bash trả về giá trị exit code (0-255) bằng return. Giá trị phức tạp hơn có thể trả qua echo hoặc biến toàn cục.

Biến toàn cục (Global Variables)

Biến toàn cục là biến có thể truy cập và thay đổi từ bất kỳ đâu trong script, kể cả bên trong và ngoài function. Tất cả biến trong Bash mặc định là toàn cục.

Biến cục bộ (Local Variables)

Biến cục bộ chỉ có hiệu lực bên trong function mà nó được khai báo. Để khai báo biến cục bộ, dùng từ khóa local.

So sánh có và không có local:

# KHÔNG có local — biến leak ra ngoài
bad_func() {
    count=10        # ghi đè biến count của caller!
}

count=5
bad_func
echo $count     # 10 — leak

# CÓ local — giới hạn trong function
good_func() {
    local count=10
}

count=5
good_func
echo $count     # 5 — OK

Habit: luôn dùng local trừ khi cố tình share. Quên local là nguyên nhân của nhiều bug debug điên đầu.

Khi nào dùng function

  • Khi có công việc lặp lại 3+ lần.
  • Khi cần tái sử dụng logic.
  • Khi cần tổ chức code rõ ràng.
  • Khi cần xử lý input.
  • Khi muốn đơn giản hoá thao tác với file.
  • Khi cần tăng tính linh hoạt.
  • Khi muốn xử lý lỗi và debug.
  • Khi tự động hoá tác vụ hằng ngày.

So sánh: không function vs có function

Không function (lặp code):

#!/bin/bash

if [[ -f "/path/to/file1" ]]; then
    echo "File1 exists."
else
    echo "File1 does not exist."
fi

if [[ -f "/path/to/file2" ]]; then
    echo "File2 exists."
else
    echo "File2 does not exist."
fi

Có function (tổ chức gọn):

#!/bin/bash

check_file() {
    if [[ -f "$1" ]]; then
        echo "File $1 exists."
    else
        echo "File $1 does not exist."
    fi
}

check_file "/path/to/file1"
check_file "/path/to/file2"

Tổ chức mã script setup:

#!/bin/bash

update_system() {
    echo "Updating system..."
    sudo apt update && sudo apt upgrade -y
}

install_package() {
    local package="$1"
    echo "Installing $package..."
    sudo apt install -y "$package"
}

cleanup_temp() {
    echo "Cleanup..."
    rm -rf /tmp/*
}

update_system
install_package "git"
install_package "curl"
cleanup_temp

Mỗi function 1 nhiệm vụ rõ — đọc tên hiểu ngay. Thêm package mới = thêm 1 dòng. Thay logic install = sửa 1 chỗ.


set -euo pipefail — strict mode

Đầu mỗi script production phải có:

#!/bin/bash
set -euo pipefail
  • -e (errexit): script dừng ngay khi lệnh nào fail (return non-zero).
  • -u (nounset): dùng biến chưa set → script die. Catch typo $DBNAME vs $DB_NAME.
  • -o pipefail: pipe fail nếu BẤT KỲ lệnh nào trong pipe fail (default chỉ check lệnh cuối). Quan trọng vì bug như mysqldump | gzip > out.gz — mysqldump fail, gzip vẫn OK, file out.gz tồn tại nhưng rỗng.

Một số case cần tắt tạm:

set +e
risky_command
result=$?
set -e

Hoặc dùng || true:

grep "pattern" file || true   # grep return 1 nếu không match, không phải lỗi

ShellCheck — linter cho bash

Bash có nhiều footgun (quote, [ ] vs [[ ]], $@ vs $*...). ShellCheck là linter catch tự động.

sudo apt install -y shellcheck
shellcheck script.sh

Đưa vào CI:

find . -name '*.sh' -exec shellcheck {} \;

Habit: mọi script production qua ShellCheck trước khi commit.


Câu chuyện thực tế

Script monitoring chạy mỗi đêm 2h:

#!/bin/bash
DBNAME="prod"
mysqldump $DBANME > /backup/db.sql    # typo: DBANME thay DBNAME
gzip /backup/db.sql
upload_to_s3 /backup/db.sql.gz

Không có set -u$DBANME rỗng → mysqldump (không có DB name argument) trả lỗi nhưng vẫn tạo file rỗng → gzip OK → upload_to_s3 OK. Sáng dậy, cron log "OK", thực tế backup rỗng.

Phát hiện 2 tuần sau khi cần restore. Hết.

Sửa:

#!/bin/bash
set -euo pipefail

DBNAME="prod"
mysqldump "$DBANME" > /backup/db.sql    # set -u: lỗi NGAY tại đây

Với -u, script die ngay tại typo, exit code khác 0, cron alert fail, fix trong vài phút thay vì 2 tuần.

ShellCheck cũng catch trước commit:

script.sh:5:14: warning: DBANME is referenced but not assigned. [SC2154]

Bài học: set -euo pipefail + ShellCheck = combo cứu khỏi silent failure.


Lab

mkdir /tmp/logic-lab && cd /tmp/logic-lab

cat > deploy.sh <<'EOF'
#!/bin/bash
set -euo pipefail

if [[ $# -lt 1 ]]; then
    echo "Usage: $0 <env>" >&2
    exit 1
fi

ENV="$1"

case "$ENV" in
    dev|staging|prod)
        echo "Deploying to $ENV..."
        ;;
    *)
        echo "Invalid env: $ENV" >&2
        exit 1
        ;;
esac

check_service() {
    local svc="$1"
    if systemctl is-active --quiet "$svc"; then
        echo "  $svc: OK"
        return 0
    else
        echo "  $svc: DOWN" >&2
        return 1
    fi
}

echo "Pre-deploy check:"
failed=0
for svc in nginx mysql redis; do
    check_service "$svc" || (( failed++ )) || true
done

if (( failed > 0 )); then
    echo "Có $failed service down. Tiếp tục? (y/n)"
    read -r ans
    [[ "$ans" = "y" ]] || exit 1
fi

echo "Waiting deploy..."
count=0
until curl -sf "http://localhost:8080/health" > /dev/null 2>&1; do
    (( count++ ))
    if (( count > 30 )); then
        echo "Timeout sau 60s" >&2
        exit 1
    fi
    sleep 2
done

echo "Deploy OK"
EOF

chmod +x deploy.sh
./deploy.sh dev
./deploy.sh invalid
shellcheck deploy.sh

# Test nested for
cat > grid.sh <<'EOF'
#!/bin/bash
for (( i = 1; i <= 3; i++ )); do
    for (( j = 1; j <= 3; j++ )); do
        echo -n "($i,$j) "
    done
    echo ""
done
EOF
chmod +x grid.sh
./grid.sh

# Test local var leak
cat > leak.sh <<'EOF'
#!/bin/bash
bad() { count=999; }
good() { local count=999; }

count=5; bad; echo "After bad: $count"     # 999 — leak!
count=5; good; echo "After good: $count"   # 5 — OK
EOF
chmod +x leak.sh
./leak.sh

Pitfalls

  1. [ -z $var ] không quote → biến rỗng gây syntax error. Quote: [ -z "$var" ] hoặc dùng [[ -z $var ]].
  2. if [ $a = $b ] với string có dấu cách → fail. Quote: [ "$a" = "$b" ].
  3. Quên local trong function → biến leak, ghi đè biến caller. Habit: local luôn.
  4. Không có set -e → script chạy tiếp dù lệnh trước fail. Silent failure khi cron.
  5. A && B || C không đúng if/else thuần — nếu B fail, C cũng chạy. Production dùng if/else.
  6. return ngoài function → fail. Dùng exit cho script.
  7. for i in $(ls *.txt) vỡ với tên có dấu cách. Dùng glob for file in *.txt.
  8. Quote $* thay "$@" khi forward args → arg gộp 1 string.

Tóm tắt

  • Exit status: 0 = OK, khác 0 = lỗi. $? xem code lệnh trước.
  • [[ ]] thay [ ] trong bash modern.
  • set -euo pipefail + ShellCheck = đầu mọi script production.
  • Function: local luôn, return cho status, echo + $() cho value.
  • "$@" mặc định khi forward args.

Bài tiếp: 03 — Tương tác với hệ thống: redirect, pipe, signal, process, trap.

← Bài trước
Bài 01
Bài tiếp theo →
Bài 03
Zalo tư vấn