Featured image of post Xử lý file trong Bash: đọc, ghi và quản lý an toàn cho DevOps

Xử lý file trong Bash: đọc, ghi và quản lý an toàn cho DevOps

Hướng dẫn xử lý file trong Bash: cat, head, tail, redirect, here-doc, tee, đọc từng dòng, kiểm tra quyền, find và xargs. Kèm ví dụ DevOps về rotate log thủ công và backup config.

Xử lý file trong Bash cho DevOps

bài trước chúng ta đã dùng vòng lặp để xử lý nhiều server, nhiều dòng log và retry các lệnh có thể lỗi tạm thời. Bước tiếp theo rất tự nhiên là xử lý file: đọc log, ghi report, append output, tạo file cấu hình, tìm file cũ và truyền danh sách file cho lệnh khác.

Trong DevOps, phần lớn automation nhỏ đều chạm tới file: log của service, file .env, config Nginx, manifest YAML, danh sách host, artifact build hoặc backup. Bài này tập trung vào các pattern Bash thực dụng: cat, head, tail, redirect, here-doc, tee, đọc từng dòng, kiểm tra file, find, xargs và hai ví dụ thực hành.


Đọc nhanh nội dung file với cat, tac, head, tail

cat đọc một hoặc nhiều file rồi ghi ra standard output. Theo GNU Coreutils, nếu không truyền file, cat đọc từ standard input.

1
2
cat /etc/os-release
cat app.log deploy.log

Một vài lệnh hay dùng khi xem log hoặc config:

1
2
3
4
5
6
head -n 20 app.log        # 20 dòng đầu
head -c 1K app.log        # 1 KiB đầu tiên
tail -n 50 app.log        # 50 dòng cuối
tail -f app.log           # theo dõi log đang tăng
tail -F app.log           # phù hợp hơn khi log có thể bị rotate
tac app.log | head -n 20  # xem 20 dòng cuối theo thứ tự đảo ngược

tail -f theo dõi file descriptor hiện tại. Khi log bị rotate, file cũ có thể bị rename và app mở file mới. GNU tail -F tương đương --follow=name --retry, thường tiện hơn cho log vận hành vì nó cố mở lại file theo tên.


Redirect output: ghi mới, append và tách stdout/stderr

Bash xử lý redirect từ trái sang phải. Một số dạng phổ biến:

1
2
3
4
5
6
command > output.log        # ghi stdout, overwrite file
command >> output.log       # append stdout
command 2> error.log        # ghi stderr
command > output.log 2>&1   # ghi stdout + stderr vào cùng file
command &> output.log       # Bash shorthand cho stdout + stderr
command &>> output.log      # append stdout + stderr

Ví dụ ghi log cho một bước deploy:

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

LOG_DIR="./logs"
LOG_FILE="${LOG_DIR}/deploy.log"
mkdir -p "${LOG_DIR}"

{
  echo "===== Deploy started at $(date -Is) ====="
  echo "Running migration..."
  ./migrate.sh
  echo "Restarting service..."
  ./restart-service.sh
  echo "===== Deploy finished at $(date -Is) ====="
} >> "${LOG_FILE}" 2>&1

Dùng block { ...; } >> "${LOG_FILE}" 2>&1 giúp gom log của nhiều lệnh vào cùng một file mà không phải lặp redirect ở từng dòng.

Lưu ý: > sẽ ghi đè file. Nếu muốn giảm rủi ro overwrite nhầm trong shell hiện tại, có thể bật set -o noclobber; khi cần ghi đè có chủ đích, dùng >| file.


Here-doc: tạo file cấu hình từ script

Here-doc (<<EOF) đưa nhiều dòng text vào stdin của một lệnh. Nó rất hữu ích khi cần tạo config mẫu, unit file, hoặc payload JSON nhỏ.

1
2
3
4
5
cat > app.env <<EOF
APP_ENV=production
APP_PORT=8080
LOG_LEVEL=info
EOF

Nếu delimiter không được quote, Bash sẽ expand biến bên trong here-doc:

1
2
3
4
5
APP_PORT="8080"

cat > app.env <<EOF
APP_PORT=${APP_PORT}
EOF

Nếu muốn giữ nguyên $, backtick hoặc ${VAR} trong file output, quote delimiter:

1
2
3
4
cat > template.env <<'EOF'
APP_PORT=${APP_PORT}
DATABASE_URL=${DATABASE_URL}
EOF

Theo Bash Manual, khi dùng <<-EOF, Bash sẽ bỏ các tab ở đầu dòng trong here-doc. Cách này giúp script dễ đọc hơn, nhưng chỉ strip tab, không strip space.


tee: vừa hiển thị vừa lưu file

Redirect > sẽ đưa output vào file và thường không còn hiển thị trên terminal. tee copy standard input ra standard output và đồng thời ghi vào file.

1
2
./healthcheck.sh | tee healthcheck.log
./healthcheck.sh | tee -a healthcheck.log

Theo GNU Coreutils, tee sẽ overwrite file nếu không dùng -a; tee -a append vào file.

Ví dụ vừa xem log build vừa lưu lại artifact log:

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

LOG_DIR="./logs"
mkdir -p "${LOG_DIR}"

./build.sh 2>&1 | tee -a "${LOG_DIR}/build.log"

Ở đây 2>&1 đặt trước pipe để stderr cũng đi qua tee. Nếu chỉ viết ./build.sh | tee ..., nhiều lỗi trên stderr vẫn hiện ra terminal nhưng không được lưu vào file log.


Đọc file theo từng dòng

Pattern an toàn khi đọc file line-by-line là while IFS= read -r LINE; do ...; done < file.

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

SERVER_FILE="${1:-servers.txt}"

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

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

  echo "Checking ${SERVER}"
done < "${SERVER_FILE}"

Giải thích nhanh:

  • IFS= giữ nguyên khoảng trắng đầu/cuối dòng.
  • read -r không coi \ là escape character.
  • done < "${SERVER_FILE}" tránh pattern cat file | while ..., vốn có thể làm vòng lặp chạy trong subshell ở một số shell và làm mất biến sau vòng lặp.
  • Quote "${SERVER_FILE}" để đường dẫn có khoảng trắng vẫn hoạt động.

Kiểm tra file, thư mục và quyền trước khi thao tác

Trước khi đọc/ghi/xóa file trong automation, hãy kiểm tra điều kiện rõ ràng. Một số test hay dùng trong [[ ... ]]:

1
2
3
4
5
6
7
[[ -e "${PATH_NAME}" ]]  # tồn tại
[[ -f "${PATH_NAME}" ]]  # regular file
[[ -d "${PATH_NAME}" ]]  # directory
[[ -r "${PATH_NAME}" ]]  # readable
[[ -w "${PATH_NAME}" ]]  # writable
[[ -x "${PATH_NAME}" ]]  # executable/searchable
[[ -s "${PATH_NAME}" ]]  # tồn tại và size > 0

Ví dụ backup config chỉ khi file tồn tại và đọc được:

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

CONFIG_FILE="${1:-/etc/example/app.conf}"
BACKUP_DIR="${BACKUP_DIR:-./backup}"

if [[ ! -f "${CONFIG_FILE}" ]]; then
  echo "ERROR: Not a regular file: ${CONFIG_FILE}"
  exit 1
fi

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

mkdir -p "${BACKUP_DIR}"
cp -- "${CONFIG_FILE}" "${BACKUP_DIR}/$(basename "${CONFIG_FILE}").$(date +%Y%m%d%H%M%S).bak"

Dấu -- sau cp giúp kết thúc danh sách option. Đây là thói quen tốt khi biến có thể bắt đầu bằng -.


find: tìm file theo điều kiện

find phù hợp khi cần tìm file theo tên, loại, thời gian, kích thước hoặc thư mục con.

1
2
3
find /var/log -type f -name "*.log"
find /var/log -type f -name "*.log" -mtime +7
find ./backup -type f -name "*.bak" -size +100M

Một số điều kiện thường gặp:

  • -type f: chỉ file thường.
  • -type d: chỉ thư mục.
  • -name "*.log": match theo tên.
  • -mtime +7: file có modification time hơn 7 ngày.
  • -size +100M: file lớn hơn 100 MiB theo cú pháp GNU find.
  • -maxdepth 1: không đi quá sâu khỏi thư mục hiện tại.

Ví dụ xem log cũ hơn 14 ngày nhưng chưa xóa:

1
find /var/log/my-app -type f -name "*.log" -mtime +14 -print

Khi viết script cleanup, nên chạy -print trước để review danh sách, sau đó mới đổi sang hành động xóa hoặc archive.


xargs: truyền danh sách file cho lệnh khác

xargs đọc dữ liệu từ stdin, gom thành argument và chạy command. Nó hữu ích khi danh sách file dài hoặc cần truyền kết quả của find cho lệnh khác.

Không nên dùng dạng mặc định cho file name tùy ý:

1
find ./logs -type f -name "*.log" | xargs gzip

Mặc định xargs tách input theo whitespace, nên tên file có space, tab hoặc newline có thể bị hiểu sai. GNU findutils khuyến nghị dùng find -print0 kết hợp xargs -0 để phân tách bằng NUL character:

1
find ./logs -type f -name "*.log" -print0 | xargs -0 gzip

Nếu dùng GNU xargs, thêm -r để không chạy command khi input rỗng:

1
find ./logs -type f -name "*.log" -print0 | xargs -r -0 gzip

Một lựa chọn khác là dùng find -exec ... {} +, portable và tránh pipe:

1
find ./logs -type f -name "*.log" -exec gzip -- {} +

Thực hành DevOps: rotate log thủ công

Trong production, log rotation thường nên giao cho logrotate hoặc logging stack. Nhưng hiểu một script rotate nhỏ giúp bạn nắm rõ các thao tác file: kiểm tra size, rename, tạo file mới, compress và cleanup.

 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
#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="${1:-./logs/app.log}"
MAX_SIZE_MB="${MAX_SIZE_MB:-100}"
KEEP_DAYS="${KEEP_DAYS:-14}"

if [[ ! -f "${LOG_FILE}" ]]; then
  echo "ERROR: Log file not found: ${LOG_FILE}"
  exit 1
fi

LOG_DIR="$(dirname "${LOG_FILE}")"
LOG_NAME="$(basename "${LOG_FILE}")"
SIZE_MB="$(du -m "${LOG_FILE}" | awk '{print $1}')"

if (( SIZE_MB < MAX_SIZE_MB )); then
  echo "OK: ${LOG_FILE} is ${SIZE_MB}MB, no rotation needed"
  exit 0
fi

TIMESTAMP="$(date +%Y%m%d%H%M%S)"
ROTATED_FILE="${LOG_DIR}/${LOG_NAME}.${TIMESTAMP}"

mv -- "${LOG_FILE}" "${ROTATED_FILE}"
: > "${LOG_FILE}"
gzip -- "${ROTATED_FILE}"

find "${LOG_DIR}" -type f -name "${LOG_NAME}.*.gz" -mtime +"${KEEP_DAYS}" -print -delete

echo "Rotated ${LOG_FILE} -> ${ROTATED_FILE}.gz"

Một vài lưu ý:

  • : > "${LOG_FILE}" tạo file rỗng mới bằng shell builtin : và redirect output rỗng.
  • Script này phù hợp để học hoặc dùng cho service nhỏ. Với app đang ghi log liên tục, rotate thủ công có thể cần signal/reopen log tùy ứng dụng.
  • Dùng find ... -print -delete để vừa thấy file nào bị xóa vừa cleanup.

Thực hành DevOps: backup config trước khi deploy

Trước khi deploy, một bước an toàn là backup các file config quan trọng vào thư mục riêng theo timestamp.

 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
#!/usr/bin/env bash
set -euo pipefail

BACKUP_ROOT="${BACKUP_ROOT:-./config-backups}"
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
BACKUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"

CONFIG_FILES=(
  "/etc/example/app.conf"
  "/etc/example/worker.conf"
  "/etc/nginx/conf.d/example.conf"
)

mkdir -p "${BACKUP_DIR}"

for CONFIG_FILE in "${CONFIG_FILES[@]}"; do
  if [[ ! -r "${CONFIG_FILE}" ]]; then
    echo "WARN: Skip unreadable config: ${CONFIG_FILE}"
    continue
  fi

  TARGET="${BACKUP_DIR}${CONFIG_FILE}"
  mkdir -p "$(dirname "${TARGET}")"
  cp -p -- "${CONFIG_FILE}" "${TARGET}"
  echo "Backed up ${CONFIG_FILE}"
done

find "${BACKUP_ROOT}" -mindepth 1 -maxdepth 1 -type d -mtime +30 -print -exec rm -rf -- {} +

cp -p giữ lại mode, ownership và timestamp nếu quyền hệ thống cho phép. TARGET="${BACKUP_DIR}${CONFIG_FILE}" giữ nguyên cấu trúc đường dẫn gốc bên trong thư mục backup, giúp restore dễ hơn.


Sai sót thường gặp

  • Dùng > khi muốn append: > overwrite file; dùng >> hoặc tee -a nếu muốn ghi nối tiếp.
  • Đặt sai thứ tự redirect: command 2>&1 >file khác command >file 2>&1. Bash xử lý redirect từ trái sang phải.
  • Quên quote đường dẫn: Luôn dùng "${FILE}", đặc biệt với path đọc từ input.
  • Dùng xargs mặc định với file name tùy ý: Ưu tiên find -print0 | xargs -0 hoặc find -exec ... {} +.
  • Xóa file ngay khi chưa review: Với cleanup, chạy find ... -print trước, sau đó mới thêm -delete hoặc rm.
  • Tạo here-doc bị expand ngoài ý muốn: Quote delimiter (<<'EOF') nếu muốn giữ nguyên ${VAR} trong file output.

Ghi chú triển khai

  • Khi áp dụng vào dự án của bạn, hãy phân biệt rõ thao tác:
    • Xem nhanh file/log → cat, head, tail, tail -F.
    • Ghi log script → redirect block hoặc tee -a.
    • Tạo config nhiều dòng → here-doc, quote delimiter nếu cần template literal.
    • Tìm/xử lý nhiều file → find, find -exec ... {} +, hoặc find -print0 | xargs -0.
  • Best practices:
    • Bắt đầu script bằng set -euo pipefail khi phù hợp.
    • Kiểm tra -r, -w, -f, -d trước khi thao tác nguy hiểm.
    • Dùng -- trước biến path trong các lệnh như cp, mv, rm, gzip.
    • Với cleanup, log lại file bị tác động bằng -print hoặc echo.
  • Troubleshooting:
    • File log không ghi đủ stderr? → Đảm bảo có 2>&1 trước pipe hoặc redirect đúng thứ tự.
    • tail -f không thấy log mới sau rotate? → Thử tail -F.
    • Script lỗi với tên file có space? → Kiểm tra quote biến và cách dùng xargs.

🎯 Lời kết

Xử lý file là kỹ năng cốt lõi khi viết Bash cho DevOps. Khi nắm chắc redirect, here-doc, tee, đọc từng dòng, kiểm tra quyền, findxargs, bạn có thể viết các script nhỏ nhưng an toàn hơn cho log, config, backup và cleanup.

Ở bài tiếp theo, chúng ta sẽ đi vào text processing với grep, awk và sed: lọc log, trích cột, thay đổi config và đếm dữ liệu từ file text hiệu quả hơn. 🚀


Tài liệu tham khảo