5 điểm bởi GN⁺ 2023-11-25 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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

 
GN⁺ 2023-11-25
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

    • Vấn đề với "${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 variable
      Dù 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[@]}}"
    • Cũng có lời khuyên phổ biến là đừng đưa các tùy chọn Bash/shell vào shebang. Lý do là nếu ai đó chạy script bằng cách chỉ định rõ trình thông dịch, chẳng hạn bash my_script.sh, thay vì ./my_script.sh, thì các tùy chọn đó sẽ không được áp dụng
      Khá 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 set như 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
    • Tôi thắc mắc có lý do đặc biệt nào để đưa -u và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=38397241
      Ví dụ bash -uc 'unset x; echo "=> ${x[@]}"'bash -uc 'x=(); echo "=> ${x[@]}"' đều chạy qua với giá trị rỗng, nhưng bash -uc 'x=(); echo "=> ${x[0]}"' thì để lại bash: 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}"'. -e khi đ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ó
    • Vấn đề của -e có 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ái
    • Khi dùng tùy chọn khiến script thoát sớm, nếu có file cần dọn dẹp thì thường phải dùng kèm trap
      trap là 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ều
  • Gầ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 shell

    • Nếu muốn bảo mật hơn thì nên dùng gì?
  • ShellCheck 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/

    • Hay thì hay, nhưng có lẽ chỉ hiệu quả khi dùng cho script của chính mình. Nếu không, bạn sẽ phải debug và sửa mọi script cần chạy mỗi lần
  • 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

    • Gần đây tôi đã thử dùng Turtle nhưng cuối cùng bỏ nó và chọn typed-process
      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ợ OverloadedStrings cũ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 được
      Tôi cũng dùng gói interpolateQuasiQuotes để làm cho raw string trong mã nguồn dễ nhìn hơn, nhưng vì không tương thích với hlint nê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

    • Giá mà có một dự án làm việc này theo cách tổng quát hơn
      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
    • High five. Tôi cũng vừa mới tạo một thứ tương tự: https://gitlab.com/engmark/shellcheck-gitlab-ci-scripts-hook
      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...
    • Thật sự rất hay. Trước đây tôi từng thử giải quyết vấn đề này từ một góc nhìn khác. Tôi muốn thêm một bước tiền xử lý nhận shell script “thông thường” rồi render nó thành phần script củ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 80

  • Nế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ì sh là một cơn ác mộng

    • Có thể làm kiểu này: đặt # shellcheck source=./deployment/deployment-example.env rồ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ở