Featured image of post Vòng lặp trong Bash: for, while, until cho tự động hóa DevOps

Vòng lặp trong Bash: for, while, until cho tự động hóa DevOps

Hướng dẫn dùng vòng lặp for, while, until, break và continue trong Bash. Kèm ví dụ DevOps thực tế: lặp danh sách server, đọc log theo dòng và retry lệnh thất bại.

Vòng lặp trong Bash cho DevOps

bài trước chúng ta đã dùng điều kiện để script biết rẽ nhánh theo trạng thái hệ thống. Nhưng trong công việc DevOps, rất nhiều tác vụ không chỉ chạy một lần: kiểm tra nhiều server, đọc từng dòng log, retry một lệnh có thể lỗi tạm thời, hoặc xử lý hàng loạt file cấu hình.

Vòng lặp trong Bash giúp bạn lặp lại một khối lệnh theo danh sách, theo điều kiện, hoặc cho đến khi một điều kiện được thoả mãn. Bài này đi qua for, while, until, break, continue và các ví dụ thực hành gần với vận hành hằng ngày.


for in — lặp qua danh sách

Theo GNU Bash Manual, dạng for name in words; do ...; done sẽ mở rộng danh sách words, rồi chạy khối lệnh một lần cho từng phần tử. Ở mỗi lượt lặp, biến name nhận giá trị hiện tại.

Cú pháp phổ biến:

1
2
3
for item in item1 item2 item3; do
  echo "Đang xử lý: ${item}"
done

Ví dụ — kiểm tra nhanh nhiều host bằng ping:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env bash
set -euo pipefail

HOSTS=("web-01" "web-02" "db-01")

for HOST in "${HOSTS[@]}"; do
  echo "Checking ${HOST}..."
  if ping -c 1 -W 2 "${HOST}" >/dev/null 2>&1; then
    echo "OK: ${HOST} reachable"
  else
    echo "WARN: ${HOST} unreachable"
  fi
done

Điểm quan trọng là dùng "${HOSTS[@]}" thay vì ${HOSTS[@]} trần. Cách quote này giữ nguyên từng phần tử array, kể cả khi giá trị có khoảng trắng.


for kiểu C — lặp theo bộ đếm

Bash cũng hỗ trợ dạng for (( expr1; expr2; expr3 )), tương tự ngôn ngữ C. Dạng này phù hợp khi bạn cần bộ đếm rõ ràng: chạy đúng N lần, đánh số retry, hoặc tạo tên file theo index.

1
2
3
4
5
6
#!/usr/bin/env bash
set -euo pipefail

for (( i = 1; i <= 5; i++ )); do
  echo "Attempt ${i}/5"
done

Ví dụ — tạo 3 file log giả lập cho môi trường test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/usr/bin/env bash
set -euo pipefail

LOG_DIR="/tmp/bash-loop-demo"
mkdir -p "${LOG_DIR}"

for (( i = 1; i <= 3; i++ )); do
  LOG_FILE="${LOG_DIR}/app-${i}.log"
  echo "$(date -Is) demo log ${i}" > "${LOG_FILE}"
  echo "Created ${LOG_FILE}"
done

Trong (( ... )), bạn có thể dùng toán tử số học quen thuộc như <=, ++, +=. Tên biến không bắt buộc có $ ở phía trước.


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

while chạy khối lệnh miễn là lệnh kiểm tra trả exit code 0. Đây là lựa chọn tốt khi số lần lặp chưa biết trước: đọc file đến hết, chờ service sẵn sàng, hoặc polling một endpoint.

Cú pháp:

1
2
3
while <lệnh_kiểm_tra>; do
  <lệnh_cần_lặp>
done

Ví dụ — đọc file log theo từng dòng và lọc dòng lỗi:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="${1:-/var/log/app/app.log}"

if [[ ! -r "${LOG_FILE}" ]]; then
  echo "ERROR: Không đọc được log file: ${LOG_FILE}"
  exit 1
fi

while IFS= read -r LINE; do
  if [[ "${LINE}" == *"ERROR"* ]]; then
    echo "Found error: ${LINE}"
  fi
done < "${LOG_FILE}"

IFS= read -r LINE là pattern nên dùng khi đọc từng dòng:

  • IFS= tránh việc Bash tự cắt khoảng trắng đầu/cuối dòng.
  • read -r giữ nguyên dấu \, không coi nó là ký tự escape.
  • done < "${LOG_FILE}" đưa file vào stdin của vòng lặp, tránh cần gọi cat không cần thiết.

until — lặp cho đến khi điều kiện đúng

until gần giống while, nhưng logic ngược lại: nó chạy khối lệnh miễn là lệnh kiểm tra trả non-zero, và dừng khi điều kiện trả 0.

Cú pháp:

1
2
3
until <lệnh_kiểm_tra>; do
  <lệnh_cần_lặp>
done

Ví dụ — chờ một endpoint healthcheck sẵn sàng:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
set -euo pipefail

HEALTH_URL="${1:-http://localhost:8080/health}"
MAX_WAIT_SECONDS="30"
WAITED="0"

until curl -fsS "${HEALTH_URL}" >/dev/null; do
  if (( WAITED >= MAX_WAIT_SECONDS )); then
    echo "ERROR: Service is not healthy after ${MAX_WAIT_SECONDS}s"
    exit 1
  fi

  echo "Waiting for service healthcheck... (${WAITED}s)"
  sleep 2
  WAITED=$(( WAITED + 2 ))
done

echo "OK: Service is healthy"

until đọc rất tự nhiên trong các tình huống “lặp cho tới khi thành công”. Nếu team của bạn thấy while ! command; do ... dễ hiểu hơn, dùng while cũng hoàn toàn ổn.


break và continue

break thoát khỏi vòng lặp hiện tại. continue bỏ qua phần còn lại của lượt lặp hiện tại và chuyển sang lượt tiếp theo. Theo GNU Bash Manual, cả hai đều dùng được trong for, while, untilselect.

Ví dụ — bỏ qua file không phải .log, dừng khi gặp log quá lớn:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env bash
set -euo pipefail

for FILE in /var/log/*.log; do
  [[ -e "${FILE}" ]] || continue

  if [[ "${FILE}" != *.log ]]; then
    continue
  fi

  SIZE_MB="$(du -m "${FILE}" | awk '{print $1}')"

  if (( SIZE_MB > 1024 )); then
    echo "Stop: ${FILE} is larger than 1GB"
    break
  fi

  echo "Process ${FILE} (${SIZE_MB}MB)"
done

Dòng [[ -e "${FILE}" ]] || continue xử lý trường hợp glob /var/log/*.log không match file nào. Nếu không có file, Bash có thể giữ nguyên pattern thành chuỗi literal, nên cần kiểm tra tồn tại trước khi xử lý.


Thực hành DevOps: lặp qua danh sách server SSH

Ví dụ này đọc danh sách server từ file, SSH vào từng host và chạy một lệnh kiểm tra ngắn. Tên server, user và command đều dùng placeholder chung, không phụ thuộc môi trường thật.

Tạo file servers.txt:

1
2
3
webserver-01
webserver-02
worker-01

Tạo script check_servers.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env bash
set -euo pipefail

SERVER_FILE="${1:-servers.txt}"
SSH_USER="${SSH_USER:-deploy}"
REMOTE_COMMAND="uptime"

if [[ ! -r "${SERVER_FILE}" ]]; then
  echo "ERROR: Cannot read server file: ${SERVER_FILE}"
  exit 1
fi

while IFS= read -r SERVER; do
  [[ -n "${SERVER}" ]] || continue
  [[ "${SERVER}" != \#* ]] || continue

  echo "===== ${SERVER} ====="
  if ssh -o BatchMode=yes -o ConnectTimeout=5 "${SSH_USER}@${SERVER}" "${REMOTE_COMMAND}"; then
    echo "OK: ${SERVER}"
  else
    echo "WARN: command failed on ${SERVER}"
  fi
  echo
done < "${SERVER_FILE}"

Chạy thử:

1
2
chmod +x check_servers.sh
./check_servers.sh servers.txt

Một vài điểm đáng chú ý:

  • BatchMode=yes giúp SSH không hỏi password/passphrase trong automation. Nếu key chưa sẵn sàng, lệnh sẽ fail thay vì treo script.
  • ConnectTimeout=5 tránh chờ quá lâu khi một host không phản hồi.
  • Dòng rỗng và dòng comment bắt đầu bằng # được bỏ qua bằng continue.
  • Không hardcode user nhạy cảm: script lấy SSH_USER từ biến môi trường, mặc định là deploy.

Thực hành DevOps: retry lệnh thất bại

Trong vận hành, có những lỗi chỉ tạm thời: network chập chờn, registry chưa phản hồi, endpoint vừa restart xong. Vòng lặp giúp bạn retry có kiểm soát thay vì chạy lại thủ công.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bash
set -euo pipefail

URL="${1:-https://example.com/health}"
MAX_RETRIES="5"
SLEEP_SECONDS="3"

for (( attempt = 1; attempt <= MAX_RETRIES; attempt++ )); do
  echo "Attempt ${attempt}/${MAX_RETRIES}: ${URL}"

  if curl -fsS "${URL}" >/dev/null; then
    echo "OK: endpoint is reachable"
    exit 0
  fi

  if (( attempt == MAX_RETRIES )); then
    echo "ERROR: endpoint is still unreachable after ${MAX_RETRIES} attempts"
    exit 1
  fi

  sleep "${SLEEP_SECONDS}"
done

Ở đây, curl -f trả lỗi nếu HTTP status là 4xx/5xx, -sS giữ output gọn nhưng vẫn in lỗi khi thất bại. Exit code cuối cùng giúp pipeline biết nên tiếp tục hay dừng.


Sai sót thường gặp

  • Lặp qua output của ls: for FILE in $(ls) dễ vỡ khi tên file có khoảng trắng hoặc ký tự đặc biệt. Hãy dùng glob (for FILE in *.log) hoặc find ... -print0 cho case phức tạp.
  • Quên quote biến: Luôn dùng "${VAR}", đặc biệt khi biến là đường dẫn, hostname hoặc dữ liệu đọc từ file.
  • Dùng pipe làm mất biến ngoài vòng lặp: cat file | while read ... thường chạy vòng lặp trong subshell, thay đổi biến bên trong có thể không còn sau vòng lặp. Ưu tiên while ...; done < file.
  • Vòng lặp vô hạn không có điều kiện dừng: Khi dùng while true, luôn có break, timeout, hoặc giới hạn số lần thử.
  • Không xử lý dòng rỗng/comment: Khi đọc danh sách server hoặc config, nên bỏ qua dòng rỗng và comment để script bền hơn.

Ghi chú triển khai

  • Khi áp dụng vào dự án của bạn, hãy xác định rõ vòng lặp đang lặp theo danh sách hay theo trạng thái:
    • Có danh sách sẵn → thường dùng for.
    • Đọc từng dòng hoặc chờ điều kiện còn đúng → thường dùng while.
    • Chờ cho đến khi lệnh thành công → cân nhắc until.
  • Best practices:
    • Dùng IFS= read -r khi đọc file theo dòng.
    • Quote array bằng "${ARRAY[@]}" khi lặp qua danh sách.
    • Với retry, luôn có MAX_RETRIESSLEEP_SECONDS rõ ràng.
    • Với SSH automation, thêm timeout và tránh prompt tương tác.
  • Troubleshooting:
    • Vòng lặp không chạy? → Kiểm tra danh sách đầu vào có rỗng không, glob có match file không.
    • Script dừng giữa chừng khi dùng set -e? → Một lệnh trong vòng lặp trả non-zero nhưng chưa được bọc bằng if, ||, hoặc xử lý exit code.
    • Đọc file bị mất khoảng trắng? → Đảm bảo dùng IFS= read -r LINE.

🎯 Lời kết

Vòng lặp là nền tảng để Bash chuyển từ các script chạy một lần sang automation thật sự: xử lý nhiều server, nhiều file, nhiều dòng log và nhiều lần retry. Khi kết hợp for, while, until với điều kiện ở bài trước, bạn đã có thể viết các script DevOps nhỏ nhưng rất hữu dụng cho kiểm tra, triển khai và vận hành hằng ngày.

Ở bài tiếp theo, chúng ta sẽ đi vào xử lý file trong Bash: đọc, ghi, redirect output, dùng tee, find, xargs và các pattern quản lý log/config an toàn hơn. 🚀


Tài liệu tham khảo