Featured image of post Điều kiện trong Bash: if, case và xử lý logic cho DevOps

Điều kiện trong Bash: if, case và xử lý logic cho DevOps

Hướng dẫn dùng if/elif/else, case…esac, toán tử so sánh số, chuỗi và kiểm tra file trong Bash. Kèm ví dụ DevOps thực tế: kiểm tra service, ngưỡng disk và quyết định deploy.

Điều kiện trong Bash cho DevOps

bài đầu tiên chúng ta đã viết script tuyến tính — chạy lần lượt từ trên xuống dưới. Trong thực tế vận hành, script thường phải ra quyết định: nếu service đã chạy thì bỏ qua, nếu disk vượt ngưỡng thì cảnh báo, nếu môi trường là production thì cần xác nhận trước khi deploy.

Điều kiện trong Bash chính là công cụ giúp script phản ứng linh hoạt với trạng thái hệ thống. Bài này sẽ đi qua if/elif/else, các toán tử so sánh, kiểm tra file và case…esac — kèm các ví dụ DevOps mà bạn có thể đem dùng ngay.


if / elif / else

Cú pháp cơ bản:

1
2
3
4
5
6
7
if [[ <điều_kiện> ]]; then
  <lệnh khi đúng>
elif [[ <điều_kiện_khác> ]]; then
  <lệnh khi điều kiện khác đúng>
else
  <lệnh khi tất cả đều sai>
fi

Một vài lưu ý:

  • Trong Bash hiện đại, ưu tiên dùng [[ ... ]] thay cho [ ... ]. [[ ]]conditional expression của Bash, hỗ trợ thêm pattern matching và an toàn hơn với biến chứa khoảng trắng.
  • Mỗi if phải kết thúc bằng fi. Nếu quên, bash -n script.sh sẽ báo lỗi cú pháp ngay.
  • Khối lệnh thường thụt vào 2 khoảng trắng để dễ đọc.

Ví dụ rất nhỏ — kiểm tra script có được chạy với quyền root hay không:

1
2
3
4
5
6
7
#!/usr/bin/env bash

if [[ "${EUID}" -eq 0 ]]; then
  echo "Đang chạy với quyền root."
else
  echo "Không phải root, một số lệnh có thể bị từ chối."
fi

EUID là biến built-in của Bash, chứa effective user ID. User root luôn có EUID=0.


Toán tử so sánh số

Khi so sánh số nguyên, dùng các toán tử dạng -eq, -ne, -gt

Toán tửÝ nghĩa
-eqbằng (equal)
-nekhác (not equal)
-gtlớn hơn (greater)
-gelớn hơn hoặc bằng
-ltnhỏ hơn (less)
-lenhỏ hơn hoặc bằng

Ví dụ kiểm tra dung lượng ổ đĩa /:

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

DISK_USAGE_PERCENT="$(df -P / | awk 'NR==2 {gsub("%", "", $5); print $5}')"

if [[ "${DISK_USAGE_PERCENT}" -ge 90 ]]; then
  echo "CRITICAL: Disk / đã dùng ${DISK_USAGE_PERCENT}%"
  exit 2
elif [[ "${DISK_USAGE_PERCENT}" -ge 80 ]]; then
  echo "WARNING: Disk / đã dùng ${DISK_USAGE_PERCENT}%"
  exit 1
else
  echo "OK: Disk / ở mức ${DISK_USAGE_PERCENT}%"
fi

Script trả exit code 0/1/2 tương ứng OK/WARN/CRIT. Đây là quy ước rất quen với các hệ giám sát như Nagios/Icinga và cũng dễ dùng trong cron hoặc pipeline CI/CD.

Mẹo: Bạn có thể dùng cú pháp (( ... )) cho số học, ví dụ if (( DISK_USAGE_PERCENT >= 80 )); then. Khi nằm trong (( )), bạn không cần $ trước tên biến và có thể dùng >=, <=, == quen thuộc.


Toán tử so sánh chuỗi

Với chuỗi, dùng các toán tử khác:

Toán tửÝ nghĩa
= / ==bằng
!=khác
-z STRchuỗi rỗng
-n STRchuỗi không rỗng
< / >so sánh thứ tự (chỉ trong [[ ]])

Ví dụ — gate môi trường trước khi deploy:

 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

ENVIRONMENT="${1:-}"

if [[ -z "${ENVIRONMENT}" ]]; then
  echo "Usage: $0 <dev|staging|production>"
  exit 1
fi

if [[ "${ENVIRONMENT}" == "production" ]]; then
  read -r -p "Bạn chắc chắn deploy lên PRODUCTION? (yes/no): " CONFIRM
  if [[ "${CONFIRM}" != "yes" ]]; then
    echo "Đã huỷ deploy."
    exit 1
  fi
fi

echo "Bắt đầu deploy lên ${ENVIRONMENT}..."

Lưu ý quan trọng: luôn quote biến ("${ENVIRONMENT}"). Nếu để $ENVIRONMENT trần và biến rỗng, biểu thức [[ $ENVIRONMENT == "production" ]] vẫn chạy được, nhưng các script tương tự với [ ... ] có thể lỗi cú pháp khi biến rỗng.


Kiểm tra file và thư mục

Bash có sẵn các toán tử dùng để kiểm tra trạng thái file:

Toán tửÝ nghĩa
-etồn tại (file hoặc thư mục)
-ftồn tại và là file thường
-dtồn tại và là thư mục
-rđọc được
-wghi được
-xthực thi được
-sfile tồn tại và có kích thước > 0
-Llà symbolic link

Ví dụ — script đảm bảo thư mục log tồn tại và file cấu hình hợp lệ trước khi chạy app:

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

APP_NAME="manager-blog"
CONFIG_FILE="/etc/${APP_NAME}/app.env"
LOG_DIR="/var/log/${APP_NAME}"

if [[ ! -f "${CONFIG_FILE}" ]]; then
  echo "ERROR: Không tìm thấy file cấu hình ${CONFIG_FILE}"
  exit 1
fi

if [[ ! -r "${CONFIG_FILE}" ]]; then
  echo "ERROR: Không có quyền đọc ${CONFIG_FILE}"
  exit 1
fi

if [[ ! -d "${LOG_DIR}" ]]; then
  echo "Tạo thư mục log: ${LOG_DIR}"
  mkdir -p "${LOG_DIR}"
fi

echo "Tiền kiểm tra OK, sẵn sàng start ${APP_NAME}."

Dấu ! ở đầu biểu thức là phép phủ định — [[ ! -f ... ]] đọc là “nếu file không tồn tại”.


Kết hợp nhiều điều kiện (AND / OR)

Có hai cách phổ biến để ghép điều kiện:

Cách 1 — ghép trong cùng [[ ]] bằng &&||:

1
2
3
if [[ -f "/etc/nginx/nginx.conf" && -r "/etc/nginx/nginx.conf" ]]; then
  echo "Cấu hình Nginx tồn tại và đọc được."
fi

Cách 2 — nối hai lệnh test riêng bằng &&/|| ở mức shell:

1
2
command -v docker >/dev/null 2>&1 && echo "Docker đã được cài"
command -v docker >/dev/null 2>&1 || echo "Docker chưa được cài"

Trong script automation, cách 1 dễ đọc hơn khi điều kiện liên quan chặt với nhau. Cách 2 phù hợp khi bạn muốn “nếu lệnh này thành công thì làm tiếp”, ví dụ:

1
mkdir -p /backup/db && tar -czf /backup/db/dump.tar.gz /var/lib/db

Nếu mkdir thất bại, lệnh tar sẽ không chạy.


case…esac — khi if/elif quá dài

Khi cần so khớp biến với nhiều giá trị khác nhau, case thường gọn và dễ đọc hơn chuỗi if/elif.

Cú pháp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case <biến> in
  <pattern1>)
    <lệnh>
    ;;
  <pattern2>|<pattern3>)
    <lệnh>
    ;;
  *)
    <lệnh mặc định>
    ;;
esac

Ví dụ — script điều phối thao tác cho một service:

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

ACTION="${1:-}"
SERVICE_NAME="nginx"

case "${ACTION}" in
  start)
    echo "Khởi động ${SERVICE_NAME}..."
    sudo systemctl start "${SERVICE_NAME}"
    ;;
  stop)
    echo "Dừng ${SERVICE_NAME}..."
    sudo systemctl stop "${SERVICE_NAME}"
    ;;
  restart|reload)
    echo "Khởi động lại ${SERVICE_NAME}..."
    sudo systemctl restart "${SERVICE_NAME}"
    ;;
  status)
    sudo systemctl status "${SERVICE_NAME}" --no-pager
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|reload|status}"
    exit 1
    ;;
esac

case hỗ trợ pattern theo kiểu glob, nên bạn có thể viết dev*), *.log), [0-9]*) … rất tiện cho việc phân nhánh theo môi trường hoặc loại file.


Thực hành DevOps: check_service.sh

Ghép các kiến thức ở trên thành một script kiểm tra service và disk thực tế.

Tạo file check_service.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/env bash
set -euo pipefail

SERVICE_NAME="${1:-nginx}"
DISK_THRESHOLD="${2:-80}"

echo "========================================"
echo "  Service & Disk Check"
echo "  Service   : ${SERVICE_NAME}"
echo "  Threshold : ${DISK_THRESHOLD}%"
echo "========================================"

# 1) Kiểm tra systemctl có sẵn không
if ! command -v systemctl >/dev/null 2>&1; then
  echo "ERROR: Hệ thống không có systemctl, script dừng."
  exit 1
fi

# 2) Kiểm tra service đang chạy hay không
if systemctl is-active --quiet "${SERVICE_NAME}"; then
  echo "OK: Service ${SERVICE_NAME} đang chạy."
else
  echo "WARN: Service ${SERVICE_NAME} không chạy. Đang thử restart..."
  if sudo systemctl restart "${SERVICE_NAME}"; then
    echo "OK: Đã restart ${SERVICE_NAME}."
  else
    echo "ERROR: Restart ${SERVICE_NAME} thất bại."
    exit 2
  fi
fi

# 3) Kiểm tra dung lượng disk theo ngưỡng truyền vào
DISK_USAGE_PERCENT="$(df -P / | awk 'NR==2 {gsub("%", "", $5); print $5}')"

case 1 in
  $(( DISK_USAGE_PERCENT >= 90 )) )
    echo "CRITICAL: Disk / đã dùng ${DISK_USAGE_PERCENT}%."
    exit 2
    ;;
  $(( DISK_USAGE_PERCENT >= DISK_THRESHOLD )) )
    echo "WARNING: Disk / đã dùng ${DISK_USAGE_PERCENT}% (ngưỡng ${DISK_THRESHOLD}%)."
    exit 1
    ;;
  *)
    echo "OK: Disk / ở mức ${DISK_USAGE_PERCENT}%."
    ;;
esac

Chạy thử:

1
2
chmod +x check_service.sh
./check_service.sh nginx 80

Điểm đáng chú ý:

  • Dùng systemctl is-active --quiet để kiểm tra trạng thái — đây là cách “đúng chuẩn” thay vì parse output bằng grep.
  • Trả exit code có ý nghĩa (0/1/2) để cron, alertmanager hoặc pipeline CI/CD nhận biết mức độ.
  • Pattern case 1 in $(( ... )) ) là một kỹ thuật nhỏ: biểu thức số học trả về 1 khi điều kiện đúng, 0 khi sai — vì vậy nhánh nào “khớp” số 1 sẽ chạy. Nếu thấy khó đọc, bạn hoàn toàn có thể quay về if/elif/else cho rõ ràng.

Sai sót thường gặp

  • Quên dấu cách trong [[ ]]: Bash yêu cầu khoảng trắng quanh dấu [[, ]] và quanh toán tử. [[$A == "x"]] sẽ lỗi cú pháp.
  • Dùng = cho số nguyên: [[ "${COUNT}" = 1 ]] so sánh dạng chuỗi. Khi cần so sánh số, dùng -eq hoặc đưa vào (( )).
  • Không quote biến: Nếu biến rỗng, biểu thức có thể bị Bash phân tích sai trong [ ] (POSIX). [[ ]] an toàn hơn, nhưng vẫn nên giữ thói quen "${VAR}".
  • Quên ;; trong case: Mỗi nhánh case phải kết thúc bằng ;;. Quên là Bash sẽ tiếp tục chạy sang nhánh sau khi nhánh đó match.
  • Lẫn lộn &&/|| với pipeline: cmd1 && cmd2 chỉ chạy cmd2 khi cmd1 thành công (exit 0). Đừng nhầm với pipe | dùng để chuyển stdout.

Ghi chú triển khai

  • Khi áp dụng vào dự án của bạn, hãy nghĩ về exit code trước khi viết logic: script này khi nào trả 0, khi nào trả khác 0? Điều này ảnh hưởng trực tiếp tới cron và pipeline.
  • Best practices:
    • Ưu tiên [[ ]] thay cho [ ] trong script Bash thuần.
    • Luôn có nhánh *) mặc định trong case để bắt giá trị không hợp lệ và in usage.
    • Khi điều kiện vượt quá 4–5 nhánh if/elif, cân nhắc chuyển sang case hoặc tách thành function.
    • Đặt biến ngưỡng (threshold) lên đầu file hoặc nhận từ tham số CLI — không hardcode rải rác trong code.
  • Troubleshooting:
    • Script không vào đúng nhánh? → Chạy với bash -x script.sh để xem giá trị biến sau khi expand.
    • Báo lỗi unary operator expected? → Thường do biến rỗng trong [ -f $FILE ]. Đổi sang [[ -f "${FILE}" ]].
    • case không match? → Kiểm tra pattern có cần quote không. Trong case, phía bên phải in là pattern (glob), không phải chuỗi đơn thuần.

🎯 Lời kết

Điều kiện là bước đầu tiên giúp script Bash của bạn “có não” — biết phản ứng theo trạng thái thay vì chạy mù. Với if/elif/else, case…esac, các toán tử so sánh số, chuỗi và kiểm tra file, bạn đã đủ công cụ để viết những script vận hành thực tế như healthcheck, gate deploy, hay điều phối service.

Ở bài tiếp theo, chúng ta sẽ đi vào vòng lặp trong Bash với for, while, until, kèm các ví dụ tự động hoá lặp qua danh sách server và đọc file log theo dòng. 🚀


Tài liệu tham khảo