Bài 02
Logic và điều khiển: if, case, loop, function, strict mode
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 foundSử 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 và [ ] là 2 cách viết cùng 1 thứ:
test -f /etc/resolv.conf
[ -f /etc/resolv.conf ] # tương đươngCả 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 file2Comparison 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
...
fiHoặc với [[ ]]:
if [[ condition ]]; then
command1
command2
fiVí dụ:
read -p "Enter password: " pass
if [[ "$pass" = "jerry" ]]; then
echo "Password verified."
fiif..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
fiVí dụ:
read -p "Enter password: " pass
if [[ "$pass" = "jerry" ]]; then
echo "Password verified."
else
echo "Access denied."
fiif 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
fiLồ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."
fiInline 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
;;
esacPattern matching mạnh:
case "$file" in
*.txt) echo "text file" ;;
*.jpg|*.png|*.gif) echo "image" ;;
*.tar.gz|*.tgz) echo "archive" ;;
/etc/*) echo "config file" ;;
esacThự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ấyOR || — 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ấyNOT ! — 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: argBPattern 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
doneOutput:
1 1 1 1 1
2 2 2 2 2
3 3 3 3 3
4 4 4 4 4
5 5 5 5 5Outer 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/passwduntil
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
donebreak 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 3select — 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)
doneVí 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
doneFunction
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_nameVí 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 — OKHabit: 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."
fiCó 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_tempMỗ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$DBNAMEvs$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 -eHoặc dùng || true:
grep "pattern" file || true # grep return 1 nếu không match, không phải lỗiShellCheck — 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.gzKhô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 đâyVớ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.shPitfalls
[ -z $var ]không quote → biến rỗng gây syntax error. Quote:[ -z "$var" ]hoặc dùng[[ -z $var ]].if [ $a = $b ]với string có dấu cách → fail. Quote:[ "$a" = "$b" ].- Quên
localtrong function → biến leak, ghi đè biến caller. Habit:localluôn. - Không có
set -e→ script chạy tiếp dù lệnh trước fail. Silent failure khi cron. A && B || Ckhông đúng if/else thuần — nếu B fail, C cũng chạy. Production dùng if/else.returnngoài function → fail. Dùngexitcho script.for i in $(ls *.txt)vỡ với tên có dấu cách. Dùng globfor file in *.txt.- 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:
localluôn,returncho 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.