Hướng dẫn

Bash script cho người mới: script đầu tiên, biến, điều kiện, vòng lặp và cron

Cập nhật: · Skillaria.Top

Bash script là tệp văn bản chứa các lệnh shell, bắt đầu bằng dòng #!/bin/bash, được cấp quyền chạy bằng chmod +x và thực thi bằng ./script.sh. Trong script người ta dùng biến, tham số $1 và $2, điều kiện if, vòng lặp for và while; còn chạy định kỳ được thiết lập bằng crontab -e.

Bash script là một tệp văn bản thông thường chứa các lệnh mà nếu không có nó bạn sẽ phải gõ từng cái trong terminal. Script bắt đầu bằng dòng shebang #!/bin/bash, được cấp quyền thực thi bằng chmod +x và chạy bằng ./script.sh. Hướng dẫn bash script cho người mới này đi từ dòng đầu tiên đến việc chạy theo lịch: biến, tham số, điều kiện, vòng lặp, xử lý lỗi và cron.

Bash script đầu tiên trong một phút

Tạo tệp bằng bất kỳ trình soạn thảo nào, ví dụ nano:

nano hello.sh
#!/bin/bash
# Script đầu tiên: lời chào và một chút thông tin về hệ thống
echo "Xin chào, $USER!"
echo "Hôm nay: $(date +%Y-%m-%d)"
echo "Bạn đang ở: $(pwd)"

Lưu lại (Ctrl+O, Enter, Ctrl+X), cấp quyền chạy và thực thi:

$ chmod +x hello.sh
$ ./hello.sh
Xin chào, alice!
Hôm nay: 2026-10-12
Bạn đang ở: /home/alice

Các dòng bắt đầu bằng # là chú thích. $(lệnh) chèn kết quả của lệnh trực tiếp vào chuỗi.

Shebang: vì sao cần dòng đầu tiên

Dòng #!/bin/bash cho hệ thống biết dùng trình thông dịch nào để chạy tệp. Không có nó, script sẽ được chạy bằng shell nào đang sẵn có; trên một số hệ thống /bin/sh không phải bash mà là dash đơn giản hơn, và một phần cú pháp sẽ không hoạt động.

Cũng hay gặp biến thể #!/usr/bin/env bash: nó tìm bash trong PATH, tiện khi bash không nằm ở /bin.

Cách chạy script và vì sao có ./

Có hai cách:

  • ./hello.sh — cần shebang và quyền x. Tiền tố ./ là bắt buộc, vì thư mục hiện tại không nằm trong PATH.
  • bash hello.sh — không cần quyền thực thi, vì trình thông dịch được chỉ định rõ ràng.

Nếu thiếu ./ bạn sẽ thấy command not found, còn thiếu chmod +x sẽ thấy Permission denied — chính là hai nguyên nhân này. Để gọi script của mình bằng tên từ bất kỳ đâu, hãy đặt chúng vào ~/bin hoặc /usr/local/bin.

Biến

Biến được gán không có khoảng trắng quanh dấu =, và được đọc bằng $:

#!/bin/bash
name="backup"
dir="/var/backups"
today=$(date +%F)

echo "Tệp: ${dir}/${name}-${today}.tar.gz"

Dấu ngoặc nhọn tách tên biến khỏi văn bản xung quanh. Dòng name = "backup" có khoảng trắng là lỗi: bash sẽ cố chạy lệnh tên là name.

Dấu nháy có ý nghĩa

Trong dấu nháy kép, biến được thay thế; trong dấu nháy đơn thì không. Hầu như luôn nên đặt biến trong dấu nháy kép, nếu không tên tệp có khoảng trắng sẽ bị tách thành hai tham số.

file="báo cáo.txt"
rm $file      # lỗi: sẽ cố xóa "báo" và "cáo.txt"
rm "$file"    # đúng
echo '$HOME'  # in ra đúng chuỗi $HOME
echo "$HOME"  # in ra /home/alice

Biến môi trường

Các biến như $HOME, $USER, $PATH do hệ thống thiết lập. Biến của riêng bạn có thể truyền cho tiến trình con bằng export. Cấu hình môi trường lâu dài đặt trong ~/.bashrc hoặc ~/.profile.

Tham số của script

Giá trị truyền khi chạy có sẵn dưới dạng $1, $2 và cứ thế tiếp tục.

BiếnChứa gì
$0tên script
$1, $2…tham số thứ nhất, thứ hai
$#số lượng tham số
"$@"tất cả tham số, tách riêng từng cái
$?mã thoát của lệnh vừa chạy
$$PID của script hiện tại
#!/bin/bash
echo "Script $0 nhận $# tham số"
echo "Tham số đầu tiên: $1"
$ ./args.sh nginx 80
Script ./args.sh nhận 2 tham số
Tham số đầu tiên: nginx

Dữ liệu nhập từ người dùng đọc bằng lệnh read: read -p "Nhập tên: " name.

Điều kiện if

Mỗi lệnh trả về mã thoát: 0 nghĩa là thành công, mọi giá trị khác là lỗi. Cấu trúc if kiểm tra chính mã này. Để so sánh, dùng [[ ... ]], và khoảng trắng bên trong dấu ngoặc là bắt buộc.

#!/bin/bash
if [[ $# -eq 0 ]]; then
    echo "Cách dùng: $0 <thư-mục>"
    exit 1
fi

if [[ -d "$1" ]]; then
    echo "Thư mục $1 tồn tại"
elif [[ -f "$1" ]]; then
    echo "$1 là tệp, không phải thư mục"
else
    echo "Không tìm thấy $1"
fi
Kiểm traÝ nghĩa
-f filetệp thông thường tồn tại
-d dirthư mục tồn tại
-e pathđường dẫn tồn tại
-x filetệp có thể thực thi
-z "$var" / -n "$var"chuỗi rỗng / không rỗng
"$a" == "$b", !=so sánh chuỗi
-eq -ne -lt -le -gt -geso sánh số

Cũng có thể kiểm tra trực tiếp kết quả của một lệnh:

if systemctl is-active --quiet nginx; then
    echo "nginx đang chạy"
else
    echo "nginx đã dừng"
fi

Khi có nhiều lựa chọn, case tiện hơn:

case "$1" in
    start)   echo "Khởi động" ;;
    stop)    echo "Dừng" ;;
    *)       echo "Lệnh không xác định: $1"; exit 1 ;;
esac

Vòng lặp for và while

for: duyệt qua một danh sách

for host in web1 web2 db1; do
    echo "Kiểm tra $host"
    ping -c 1 -W 1 "$host" > /dev/null && echo "  có thể truy cập" || echo "  không truy cập được"
done

for file in /var/log/*.log; do
    echo "$file: $(wc -l < "$file") dòng"
done

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

while: lặp khi điều kiện còn đúng

Ví dụ kinh điển là đọc tệp từng dòng:

while read -r user; do
    echo "Người dùng: $user"
done < users.txt

Và chờ cho dịch vụ khởi động xong:

count=0
until curl -sf http://localhost:8080/health > /dev/null; do
    count=$((count + 1))
    if [[ $count -ge 10 ]]; then
        echo "Dịch vụ không khởi động được"
        exit 1
    fi
    sleep 3
done
echo "Dịch vụ đã sẵn sàng"

Phép tính trong bash được viết trong dấu ngoặc kép: $((a + b)). Chỉ làm việc với số nguyên.

Hàm

Đoạn code lặp lại được đưa vào hàm. Bên trong hàm, tham số vẫn là $1, $2, còn local ngăn biến thoát ra bên ngoài.

log() {
    local level="$1"; shift
    echo "$(date '+%F %T') [$level] $*"
}

log INFO "Bắt đầu sao lưu"
log ERROR "Hết dung lượng đĩa"

Xử lý lỗi: set -euo pipefail

Mặc định, bash tiếp tục chạy sau khi một lệnh thất bại — và script sao lưu có thể kết thúc "thành công" mà không sao chép được gì. Người ta thường thêm chế độ nghiêm ngặt vào đầu script:

#!/bin/bash
set -euo pipefail
  • -e — dừng ngay ở lỗi đầu tiên.
  • -u — coi việc dùng biến chưa khai báo là lỗi (bảo vệ khỏi rm -rf "$DIR/" khi DIR rỗng).
  • -o pipefail — đường ống bị coi là thất bại nếu bất kỳ phần nào của nó thất bại.

Để gỡ lỗi, chạy script bằng bash -x script.sh: bash sẽ in từng lệnh trước khi thực thi. Để tìm lỗi thường gặp trước khi chạy, có thể dùng công cụ shellcheck.

Ví dụ thực tế: script sao lưu

#!/bin/bash
set -euo pipefail

SRC="${1:-/var/www}"
DST="/var/backups/site"
KEEP_DAYS=7
STAMP=$(date +%F_%H-%M)

mkdir -p "$DST"
tar -czf "$DST/site-$STAMP.tar.gz" -C "$(dirname "$SRC")" "$(basename "$SRC")"
echo "Đã tạo bản lưu $DST/site-$STAMP.tar.gz"

# xóa các bản lưu cũ hơn KEEP_DAYS ngày
find "$DST" -name "site-*.tar.gz" -mtime +"$KEEP_DAYS" -delete

Cú pháp ${1:-/var/www} nghĩa là "tham số đầu tiên, nếu không có thì dùng /var/www". Lưu script thành /usr/local/bin/backup-site.sh và cấp quyền thực thi: sudo chmod +x /usr/local/bin/backup-site.sh.

Chạy theo lịch: cron

Cron là dịch vụ chạy lệnh theo lịch. Tác vụ của người dùng được sửa bằng crontab -e, xem bằng crontab -l, còn tác vụ của root dùng sudo crontab -e.

Mỗi dòng tác vụ gồm năm trường thời gian và lệnh:

# phút giờ ngày_trong_tháng tháng thứ_trong_tuần lệnh
30 2 * * * /usr/local/bin/backup-site.sh >> /var/log/backup.log 2>&1
LịchThời điểm chạy
*/5 * * * *mỗi 5 phút
0 * * * *đầu mỗi giờ
30 2 * * *mỗi ngày lúc 02:30
0 9 * * 1-5các ngày trong tuần lúc 09:00
0 3 1 * *03:00 ngày 1 hằng tháng
@rebootmột lần sau khi khởi động

Vì sao script chạy bằng tay nhưng không chạy trong cron

  • Môi trường khác. Cron chạy tác vụ với PATH tối thiểu và không đọc ~/.bashrc. Hãy ghi đường dẫn đầy đủ tới lệnh và tệp, hoặc đặt PATH ở đầu crontab.
  • Thư mục hiện tại khác. Tác vụ bắt đầu ở thư mục cá nhân, không phải nơi chứa script. Dùng đường dẫn tuyệt đối.
  • Ký tự %. Trong crontab, ký tự % có nghĩa là xuống dòng, nên date +%F viết trực tiếp trong tác vụ phải được escape thành \%. Đơn giản hơn là đưa lệnh vào script.
  • Đầu ra bị mất. Chuyển hướng đầu ra vào nhật ký (>> file 2>&1), nếu không sẽ không thấy lỗi. Các lần chạy cron xem được qua journalctl -u cron (trên họ RHEL, dịch vụ tên là crond).

Trên hệ thống dùng systemd, có thể thay cron bằng systemd timer: timer ghi đầu ra vào nhật ký và có thể bù các lần chạy bị bỏ lỡ.

Tóm lại

Bash script cho người mới là một tệp có shebang #!/bin/bash, quyền thực thi và chuỗi lệnh. Hãy thêm biến trong dấu nháy kép, kiểm tra tham số bằng if, dùng vòng lặp for và while, và chế độ nghiêm ngặt set -euo pipefail — bạn sẽ có một công cụ tự động hóa đáng tin cậy để cron chạy theo lịch. Bash script, môi trường và cron đều được hướng dẫn thực hành trong khóa học Linux.

Câu hỏi thường gặp

bash khác sh ở điểm nào?

sh là shell POSIX tiêu chuẩn, còn bash là phiên bản mở rộng với mảng, dấu ngoặc kép đôi và nhiều tiện ích khác. Script dùng cú pháp của bash phải chạy bằng bash, không phải sh.

Bash script cần đuôi mở rộng gì?

Linux không quan tâm đuôi tệp; trình thông dịch được xác định qua shebang. Đuôi .sh chỉ là quy ước để người đọc biết đây là script.

Làm sao truyền mật khẩu vào script an toàn?

Đừng truyền qua tham số — các tham số hiển thị cho người dùng khác qua ps. Hãy dùng biến môi trường lấy từ tệp có quyền 600, hoặc read -s để nhập mà không hiển thị.

Làm sao kiểm tra script có đang chạy với quyền root không?

So sánh ID người dùng với số 0: if [[ $EUID -ne 0 ]]; then echo "Cần quyền root"; exit 1; fi.

Làm sao chạy script ở nền để nó không dừng khi thoát SSH?

Dùng nohup ./script.sh &, một phiên tmux hoặc screen; còn để chạy lâu dài, hãy đóng gói script thành dịch vụ systemd.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Linux: с нуля до уверенного администрирования”.

Đến khóa học