ShellCheck: Công cụ phân tích giúp tìm lỗi trong shell script
(shellcheck.net)- ShellCheck là công cụ cho phép kiểm tra ngay trên web các lỗi thường gặp trong shell script; bạn có thể xem vấn đề trong đoạn mã đã dán ở phần đầu ra bên dưới trình soạn thảo
- Trong môi trường cục bộ, có thể cài đặt bằng
cabal,apt,dnf,pkg,brew install, v.v., nên có thể sử dụng dựa trên trình quản lý gói - Các ví dụ cho thấy những điểm thường bị bỏ sót trong script thực tế, như cảnh báo về tính di động của shebang
sh, vấn đề ngữ nghĩa và lỗi xử lý dấu ngoặc kép - Đây là phần mềm tự do theo giấy phép GPLv3, đồng thời cũng được cung cấp trên GitHub, Wiki và dưới dạng linter tích hợp cho các trình soạn thảo phổ biến
- Có thể dùng với CodeClimate, Codacy, CodeFactor để tự động kiểm tra kho GitHub; bản thân ShellCheck được viết bằng Haskell
Chức năng phân tích shell script
- ShellCheck là công cụ phân tích tìm lỗi trong shell script
- Khi dán script lên trang web, bạn có thể xem ngay kết quả kiểm tra trong cửa sổ đầu ra bên dưới trình soạn thảo
- Các ví dụ đi kèm cho thấy công cụ có thể phát hiện nhiều loại vấn đề
- Nhiều vấn đề ẩn trong các script thông thường
- Cảnh báo về tính di động phát sinh khi shebang là
sh - Các vấn đề ngữ nghĩa ở mức cao hơn
- Nhiều vấn đề xử lý dấu ngoặc kép khác nhau
Cài đặt, giấy phép và tích hợp
- Có thể cài đặt cục bộ bằng
cabal,apt,dnf,pkg,brew install - Đây là phần mềm tự do theo giấy phép GPLv3
- Có thể xem tài liệu tại ShellCheck Wiki
- Dự án được công khai trên GitHub, và mã nguồn website cũng được công khai
- Các gói dành cho bản phân phối hoặc trình quản lý gói đã được cung cấp sẵn
- Có thể dùng như linter tích hợp trong các trình soạn thảo phổ biến
- Có thể dùng CodeClimate, Codacy, CodeFactor để tự động kiểm tra kho GitHub
- ShellCheck được viết bằng Haskell
1 bình luận
Các ý kiến trên Hacker News
Mẹo tôi dùng là thế này. Trong shebang, hầu như lúc nào cũng nên thêm
-u(nounset) để biến chưa được khai báo trở thành lỗi; ngoại lệ thường gặp là trường hợp cú pháp"${arr[@]}"khi mở rộng mảng rỗng bị coi là unbound-n(noexec) ngăn việc thực thi lệnh, nên có thể dùng như kiểu dry-run của người nghèo.-e(errexit) cũng hữu ích, nhưng cần cẩn thận vì trên thực tế nó chỉ khiến thoát khi lệnh “chính nó” thất bại; cá nhân tôi tránh dùng và thích thường xuyên thêm|| fail "..."sau lệnh hơn"${arr[@]}"chỉ có ở bash 3 trở xuống; từ bash 4, ngay cả khi biến thật sự chưa được định nghĩa,[@]cũng không ném lỗi unbound variableDù vậy, macOS vẫn cài bash v3 làm mặc định và cũng không tự động cập nhật, nên đây vẫn còn là vấn đề. Việc bản phát hành cuối cùng của bash 3 đã từ 20 năm trước thật sự điên rồ. Lỗi unbound khi mở rộng mảng rỗng có thể tránh bằng mở rộng
${var+alter}:echo "${arr+${arr[@]}}"bash my_script.sh, thay vì./my_script.sh, thì các tùy chọn đó sẽ không được áp dụngKhá nhiều người làm vậy để khỏi phải đặt bit thực thi, và đôi khi cũng do chưa hiểu rõ. Vì thế lời khuyên thường gặp là đặt
setnhưset -euo pipefailở dòng đầu ngay sau shebang; cách này cũng hữu ích trong những trường hợp shebang khó xử lý đối số bổ sung, như#!/usr/bin/env bash-uvào shebang không. Vì sao dùng shebang thay vìset -u?Trong Bash,
"${arr[@]}"có vẻ hoạt động tốt. Như các bình luận khác cũng đã nhắc, Bash mới đã tốt hơn, và vấn đề dường như chỉ có ở<= 4.3: https://news.ycombinator.com/item?id=38397241Ví dụ
bash -uc 'unset x; echo "=> ${x[@]}"'vàbash -uc 'x=(); echo "=> ${x[@]}"'đều chạy qua với giá trị rỗng, nhưngbash -uc 'x=(); echo "=> ${x[0]}"'thì để lạibash: x[0]: unbound variable. Zsh không thích ví dụ đầu tiên, nhưng cả hai đều nên hỗ trợ mở rộng giá trị mặc định nhưbash -uc 'unset x; echo "=> ${x[@]:-null}"'.-ekhi đi với hàm rất dễ gây rối, nên càng về sau tôi càng bớt thích nó-ecó thể xử lý tốt bằng-o pipefail, và tùy chọn này đã được đưa vào POSIX từ năm ngoáitraptraplà một tính năng scripting tuyệt vời, nhưng theo cảm nhận của tôi thì chưa được nói đến đủ nhiềuGần đây tôi đã tìm thấy một lỗ hổng leo thang đặc quyền trong shell script do mở rộng số học. Nó thuộc kiểu tương tự như bài ở https://research.nccgroup.com/2020/05/12/shell-arithmetic-ex...
Ví dụ,
$((1 + ENV_VAR))có thể bị chèn mã nếu bạn kiểm soát được$ENV_VAR. Đáng tiếc là ShellCheck, ít nhất với cấu hình mặc định, không bắt được lỗi này. Nhưng nếu triển khai bất cứ thứ gì quan trọng về bảo mật dù chỉ một chút, thì ngay từ đầu không nên dùng shellShellCheck thật sự là cứu tinh. Trước đây tôi có làm một wrapper nhỏ https://github.com/jamespwilliams/strictbash, có thể dùng làm shebang cho script
Trước khi chạy script, nó chạy ShellCheck; nếu có lỗi thì script hoàn toàn không được thực thi, đồng thời cũng đặt tất cả các cờ “strict mode” của bash. Tham khảo: http://redsymbol.net/articles/unofficial-bash-strict-mode/
Chủ đề này đã xuất hiện nhiều lần: https://news.ycombinator.com/from?site=shellcheck.net
Lần thảo luận lớn gần nhất là vào năm 2021, với 301 điểm và 54 bình luận: https://news.ycombinator.com/item?id=27030504
Cách đây không lâu, tôi đã chuyển các script build và deploy, cùng vài bash script cho một máy chủ production đơn lẻ sang Turtle của Haskell
Việc này rất hay vì giảm được đáng kể phần trùng lặp, và mã kết quả cũng ngắn hơn nhiều. https://hackage.haskell.org/package/turtle
Theo tôi biết, chương trình Turtle chỉ có một thư mục hiện tại duy nhất, nên gặp khó khi chạy các tác vụ đồng thời cần được thực thi trong những thư mục cụ thể. Tôi đã giải quyết phần nào bằng cơ chế khóa, hàng đợi và worker, nhưng khi thư mục hiện tại của Turtle bị xóa và bắt đầu gây lỗi thì trở nên khó kiểm soát
Ngược lại, typed-process khởi chạy các tiến trình riêng biệt và có thể chạy bên trong thư mục làm việc mà không cần
cd, nên rất phù hợp với các workflow lớn và phức tạp. Hỗ trợOverloadedStringscũng tốt, nên thường chỉ cần sao chép rồi dán nguyên nội dung lẽ ra nhập vào bash là chạy đượcTôi cũng dùng gói
interpolatevàQuasiQuotesđể làm cho raw string trong mã nguồn dễ nhìn hơn, nhưng vì không tương thích vớihlintnên đang định tìm một gói khác để xử lý chuỗiĐây rõ ràng là tự quảng bá, nhưng tôi đưa ShellCheck và nhiều linter vào cấu hình pre-commit với nguyên tắc sửa mọi cảnh báo trước khi commit, hoặc ít nhất là trước khi merge
Tuy nhiên, phần lớn shell trong các dự án của tôi nằm trong file
.gitlab-ci.yml, nên khó kiểm tra. Vì vậy tôi đã tạo một wrapper để tự động xử lý: https://pypi.org/project/glscpc/Nó dùng dự án ShellCheck cùng một chút “ma thuật” để hiển thị các góp ý của ShellCheck với số dòng gần như chính xác
Tôi không dùng GitLab CI, nhưng dùng khá nhiều định dạng file về bản chất là nhúng inline shell script, như Dockerfile, GitHub Actions, Justfile
Thường thì chỉ riêng vì ShellCheck, tôi cũng tách bất cứ thứ gì phức tạp hơn vài lệnh ra thành shell script riêng, rồi gọi nó từ script inline trong Dockerfile. Mẫu này cũng giúp CI không bị buộc quá chặt vào GitHub Actions
Ví dụ sử dụng là một pre-commit hook nhắm tới
.gitlab-ci.yml, còn ví dụ cấu hình ở đây: https://gitlab.com/engmark/root/-/blob/9f7d9b93c2297d0b170e5...scriptcủa job đó tại thời điểm buildƯu điểm là mọi thứ vẫn tự chứa bên trong job gitlab-ci. Nhưng việc xử lý đủ kiểu kỳ quặc của shell trong môi trường GitLab CI runner quá đau đầu nên tôi đã dừng lại, và giờ đang chuyển mọi job sang Python script
Cũng có bash language server: https://github.com/bash-lsp/bash-language-server/
Hay. Ngay trong script production
/bin/shđầu tiên đem chạy thử, tôi đã học được vài điều, dù tôi đã đụng tới các script kiểu này từ thập niên 80Nếu nó trở nên quá dài để viết bằng Bash, đến mức thực ra không nên làm vậy nữa, thì tôi cũng khuyên dùng https://github.com/bach-sh/bach
ShellCheck rất tuyệt, nhưng xử lý source/import thì thật sự khổ sở. Không phải lỗi của ShellCheck, mà vì
shlà một cơn ác mộng# shellcheck source=./deployment/deployment-example.envrồi dùng. "${1}"Tuy nhiên, khi có nhiều shell script con và nhiều file cần source hơn, bạn sẽ hiểu vì sao nó khổ sở