Suốt mười một phần của loạt Unix, ta đã gặp hết bẫy này tới bẫy khác: quên quote biến làm vỡ tên có dấu cách, rm -rf $var/* với biến rỗng thành rm -rf /*, set -e nuốt lỗi trong pipe, biến chưa gán. Điểm chung của chúng: đều là những lỗi tĩnh — nhìn vào mã là thấy, nhưng con người hay bỏ sót. Tin tốt: có một công cụ đọc script và bắt gần như toàn bộ lớp lỗi đó trước khi bạn chạy — shellcheck. Bài này (phần 12, khép lại loạt Unix) chạy shellcheck thật để thấy nó gói gọn mọi bài học của loạt thành cảnh báo tự động.

Một script "bình thường" đầy bẫy

Đây là kiểu script ai cũng từng viết — trông chạy được, nhưng ẩn ba quả bom:

#!/bin/bash
DIR=$1
rm -rf $DIR/*              # biến rỗng -> rm -rf /* ; lại không quote
for f in $(ls $DIR); do    # lặp qua output ls -> vỡ với tên có dấu cách
  echo "xu ly $f"
done
if [ $COUNT == 0 ]; then    # biến chưa gán + không quote
  echo "rong"
fi

Mỗi dòng là một bài học của loạt này: quoting (phần 7), rm -rf biến rỗng (phần 7), biến chưa gán (set -u, phần 8), và lặp qua ls (phản mẫu word splitting).

Ảnh chụp đoạn mã nền tối minh hoạ vì sao script shell hỏng và shellcheck bắt lỗi trước khi chạy, một script bình thường đầy bẫy DIR bằng đô 1 rm -rf đô DIR trên sao biến rỗng rm -rf trên sao không quote for f in đô ngoặc ls đô DIR lặp qua ls vỡ với dấu cách if ngoặc đô COUNT bằng bằng 0 biến chưa gán cộng không quote, shellcheck linter tĩnh cho shell bắt lỗi mà không cần chạy shellcheck backup.sh mỗi cảnh báo có mã SC kèm link giải thích cộng gợi ý sửa SC2086 quote biến để tránh word splitting globbing SC2115 dùng var hỏi để không lỡ thành trên sao SC2045 đừng lặp qua output của ls dùng glob, bản sửa gom mọi bài học của loạt Unix set -euo pipefail dừng khi lỗi biến chưa gán lỗi pipe DIR bằng đô 1 hỏi can duong dan bắt buộc có tham số rm -rf đô DIR hỏi trên sao không bao giờ thành trên sao for f in đô DIR trên sao glob thay vì ls ngoặc -e đô f hoặc hoặc continue xử lý thư mục rỗng, dùng trong CI shellcheck trả exit code shellcheck sao sh exit 1 nếu có lỗi CI đỏ chặn merge thêm vào pre-commit pipeline bug shell không lọt lên prod

Hình 1: Một script với các bẫy kinh điển; shellcheck là linter tĩnh bắt chúng qua mã SCxxxx kèm gợi ý; bản sửa gom mọi bài học của loạt (set -euo pipefail, quote, ${var:?}, glob thay ls); và dùng exit code trong CI.

Đo thật: shellcheck chỉ mặt từng lỗi

Chạy shellcheck trên script trên, nó không chỉ báo "có lỗi" mà chỉ đúng dòng, đúng mã, kèm gợi ý sửa:

Ảnh chụp bảng kết quả chạy thật shellcheck output thật, một shellcheck quét script lỗi 3 mã cảnh báo line 3 rm -rf đô DIR trên sao SC2115 Use var hỏi to ensure this never expands to trên sao SC2086 Double quote to prevent globbing and word splitting line 4 for f in đô ngoặc ls đô DIR SC2045 Iterating over ls output is fragile Use globs, hai bản sửa shellcheck sạch shellcheck backup_fixed.sh KHÔNG có cảnh báo nào sạch, ba lỗi thật DIR rỗng bản gốc rm -rf đô DIR trên sao lệnh chạy rm -rf trên sao thảm hoạ bản sửa rm -rf đô DIR hỏi trên sao DIR can duong dan dừng ngay, bốn exit code cho CI script lỗi exit đô hỏi bằng 1 CI đỏ chặn merge script sạch exit đô hỏi bằng 0 qua

Hình 2: Chạy thật — shellcheck báo SC2115/SC2086 ở dòng rm -rf $DIR/* và SC2045 ở for f in $(ls $DIR); bản sửa cho ra sạch (không cảnh báo); DIR rỗng khiến bản gốc thành rm -rf /* còn bản sửa dừng ngay; exit code 1 (lỗi) / 0 (sạch) cho CI.

Ba cảnh báo thật, mỗi cái là một lớp lỗi:

  • SC2115: rm -rf $DIR/* có thể nở thành /* nếu $DIR rỗng — gợi ý dùng ${var:?}.
  • SC2086: quote biến để tránh word splitting/globbing (bài phần 7).
  • SC2045: đừng lặp qua output của ls — dùng glob (for f in "$DIR"/*).

Mỗi mã kèm một link shellcheck.net/wiki/SCxxxx giải thích cặn kẽ vì sao và sửa thế nào — vừa sửa vừa học.

Bản sửa và bằng chứng lỗi thật

Áp mọi gợi ý, bản sửa dùng đúng những gì loạt bài đã dạy: set -euo pipefail, ${1:?...} bắt buộc tham số, ${DIR:?} chống rỗng, glob thay ls. Đo thật: shellcheck trên bản sửa cho ra sạch, không một cảnh báo. Và khác biệt không chỉ là "đẹp mã": chạy với DIR rỗng, bản gốc tạo lệnh rm -rf /* (thảm hoạ, mình chỉ in ra không chạy), còn bản sửa dừng ngay với thông báo DIR: can duong dan. shellcheck đã cảnh báo trước chính xác kịch bản này.

Dùng trong CI: biến cảnh báo thành rào chắn

shellcheck trả exit code — đo thật: script lỗi → 1, script sạch → 0. Nghĩa là bạn cắm được nó vào pipeline: shellcheck *.sh trong CI hoặc pre-commit hook sẽ làm build đỏ khi có lỗi, chặn merge. Cả một lớp bug shell — thứ chỉ lộ ra lúc chạy production lúc nửa đêm — bị bắt tự động ngay khi viết. Đây là cách rẻ nhất để nâng chất lượng script cả đội.

Đánh đổi cần cân nhắc

shellcheck bắt lỗi tĩnh, không bắt lỗi logic. Nó thấy quoting sai, biến chưa gán, cú pháp nguy hiểm — nhưng không biết thuật toán của bạn đúng hay sai, hay lệnh có làm đúng ý định không. Nó là lớp phòng thủ đầu, không thay được đọc mã và test. Một script "shellcheck sạch" vẫn có thể làm sai việc.

Đừng tắt cảnh báo bừa — hiểu rồi hãy bỏ qua. shellcheck cho phép tắt từng mã (# shellcheck disable=SC2086), nhưng lạm dụng là vô hiệu hoá chính công cụ. Chỉ tắt khi bạn thật sự hiểu vì sao trường hợp này an toàn (ví dụ cố ý muốn word splitting), và ghi chú lý do. Mỗi lần tắt là một lời hứa "tôi biết mình đang làm gì".

Cân nhắc chuyển sang ngôn ngữ khác khi script quá lớn. shellcheck giúp shell an toàn hơn nhiều, nhưng shell vẫn có những góc sắc. Khi một script vượt vài trăm dòng, có cấu trúc dữ liệu phức tạp, hay cần test kỹ, Python thường dễ bảo trì và ít bẫy hơn. shellcheck giúp bạn viết shell tốt; biết khi nào không nên dùng shell cũng quan trọng ngang vậy.

Ba ý mang về

  1. Hầu hết bug shell là lỗi tĩnh bắt được tự động: đo thật, shellcheck chỉ ra SC2086 (quote), SC2115 (rm -rf rỗng), SC2045 (lặp qua ls) — đúng các bẫy suốt loạt — kèm dòng, mã, và gợi ý sửa.
  2. Bản sửa gom mọi bài học và cho ra sạch: set -euo pipefail + quote + ${var:?} + glob → shellcheck không còn cảnh báo, và DIR rỗng khiến bản gốc thành rm -rf /* còn bản sửa dừng ngay.
  3. Cắm shellcheck vào CI: đo thật exit code 1 (lỗi) / 0 (sạch) — dùng trong pre-commit/pipeline để chặn bug shell trước production; nhưng nhớ nó bắt lỗi tĩnh, không thay đọc mã và test.

Nguồn

Đến đây khép lại loạt Unix — từ pipe và file descriptor, grep/sed/awk, find/xargs, sort/uniq/cut, quoting, exit code/set -e, tar, quyền chmod, cron, cho tới shellcheck ở bài này. Sợi chỉ xuyên suốt: dòng lệnh Unix không phải phép thuật mà là những công cụ nhỏ, làm một việc thật tốt, ghép lại bằng pipe — và mỗi hành vi "khó hiểu" của chúng đều có lý do đo được. Nắm được lớp này, bạn xử lý dữ liệu nhanh hơn, viết script an toàn hơn, và debug hệ thống với sự tự tin thay vì đoán mò.