2 điểm bởi GN⁺ 2024-03-03 | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi script Bash hoạt động khác với dự kiến, chỉ riêng việc xem đúng các lệnh đang được thực thi cũng giúp việc lần theo nguyên nhân trở nên dễ dàng hơn
  • set -x in ra từng dòng sau khi mở rộng biến, giúp bạn kiểm tra script thực sự đang chạy lệnh nào
  • Chạy từ dòng lệnh bằng bash -x script.sh sẽ cho hiệu quả tương đương với việc thêm set -x ở đầu script.sh
  • Dùng cùng bẫy DEBUGread có thể dừng trước khi thực thi từng dòng để kiểm tra tên tệp, số dòng và lệnh kế tiếp
  • Hàm die() { echo $1 >&2; exit 1; } giúp đơn giản hóa luồng in thông báo lỗi ra standard error rồi thoát khi gắn sau một lệnh thất bại

Kiểm tra luồng thực thi bằng mắt

  • set -x in ra các dòng mà script thực thi, và các biến sẽ được hiển thị dưới dạng giá trị đã mở rộng
  • Có thể dùng bằng cách thêm set -x ở đầu script
  • Có thể thực hiện cùng hành vi này ngay từ dòng lệnh
    • $ bash -x script.sh
    • Điều này tương đương với việc thêm set -x ở ngay đầu script.sh

Dừng lại để kiểm tra từng dòng

  • Bẫy DEBUG được kích hoạt trước khi từng dòng mã được thực thi
  • Nếu thêm đoạn mã dưới đây vào phần đầu script, nó sẽ chờ bạn nhấn Enter trước khi chạy lệnh tiếp theo
    • trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUG
    • read -p sẽ in thông điệp và chờ nhấn Enter
    • $BASH_SOURCE là tên tệp script
    • $LINENO là số dòng
    • $BASH_COMMAND là lệnh sẽ được thực thi tiếp theo

In thông báo rồi thoát khi thất bại

  • Hàm die có thể được dùng để in thông báo và kết thúc chương trình khi một lệnh thất bại
    • die() { echo $1 >&2; exit 1; }
    • Chỉ cần gắn sau lệnh có thể thất bại như some_command || die "oh no!"
  • Hàm này gửi thông báo tới standard error và thoát bằng exit 1

1 bình luận

 
GN⁺ 2024-03-03
Các ý kiến trên Hacker News
  • Trong ZFSBootMenu, họ dùng vài hàm tự viết khá ổn để hỗ trợ gỡ lỗi
    Có hàm ghi log zdebug nằm rải rác trong mã: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...
    Khi bật debug logging, nhấn Ctrl-T ở menu chính sẽ hiện màn hình như thế này: https://i.imgur.com/Ge75zkP.png
    Ngoài ra còn có profiling bằng flame graph có thể bật qua https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme..., và nếu lắp ghép lại dữ liệu dump qua cổng serial thì có thể tạo ra đồ thị như https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master...
    Bash linh hoạt hơn người ta tưởng
  • Khi dùng set -x, PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: ' rất hữu ích
    Cách này hiển thị tên file, tên hàm, số dòng, nên khá giúp ích khi gỡ lỗi các script Bash lớn
  • Cũng khuyến nghị dùng shellcheck. Dù không trực tiếp bắt được vấn đề, nó vẫn chỉ ra các vấn đề tiềm ẩn
    Ngoài ra cũng khuyến nghị viết lại script bằng ngôn ngữ khác. Ở công ty, chúng tôi đang chuyển các script Bash sang Rust; chi phí nhập môn cao, nhưng mã kết quả dễ bảo trì hơn nhiều và đáng tin cậy hơn
    Bash vẫn tốt cho các script nhanh, nhưng khi vượt khoảng 100 dòng thì đáng cân nhắc dùng một ngôn ngữ cho bảo đảm mạnh hơn
    • Đồng ý, nhưng cũng cần nói chuyện đó với mảng kỹ thuật CI/CD và các pipeline YAML
  • Có thể cải thiện gỡ lỗi hơn nữa bằng cách dùng mã thoát như thế này
    die() là hàm trợ giúp in thông báo lỗi ra lỗi chuẩn rồi thoát với mã lỗi được chỉ định
    Có thêm nhiều mã thoát và hàm trợ giúp cho shell script ở đây: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
    • Danh sách hay, và tôi nghĩ người dùng thành thạo nào cũng có các hàm trợ giúp riêng
      Tuy nhiên, tôi hơi tiếc về triết lý của die đó. Hàm die về cơ bản nên truyền tiếp mã thoát của lệnh đã thất bại, và cũng không nên che giấu output lỗi của lệnh đó
      Nếu trong một script lớn tôi muốn tự gán ý nghĩa cho lỗi của lệnh, tôi sẽ dùng một die chuyên biệt hơn. die của tôi đại khái có dạng __errex "$?" "${LINENO}" "$0", in ra lỗi nghiêm trọng, số dòng, tên script và thông báo, rồi kết thúc bằng mã thoát đó
  • Nếu dùng nhiều hàm Bash, cũng có thể tạo một dạng stack trace
    Một ví dụ triển khai ở đây: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
    • Có một triển khai stack trace khác: https://github.com/runag/runag/blob/main/lib/fail.sh
      Dùng kiểu some-command || fail "message" thì khi some-command trả về trạng thái thoát khác 0, nó sẽ tạo stack trace và thoát khỏi shell
      Nếu muốn tạo stack trace trong hàm rồi trả về, có thể dùng some-command || softfail "message" || return $?
  • Tôi tò mò liệu ngoài di sản và quán tính ra, còn lý do nào khiến Bash vẫn là ngôn ngữ shell scripting trên thực tế không
    Nó làm được việc cần làm, nhưng thô và cú pháp cũng kinh khủng. Khi script đạt tới một mức kích thước hoặc độ phức tạp nào đó, nó buộc ta phải chuyển sang một ngôn ngữ tử tế hơn, nên có khi đó lại là thiết kế có chủ ý
    • Đúng là lượng sử dụng legacy chiếm phần lớn trong độ phổ biến
      Với các bản phân phối hiện đại thì thường có phiên bản Bash khá mới, và nếu không định dùng những thứ như array thì cũng không cần quá bận tâm về phiên bản
      Sức hấp dẫn của Bash nằm ở vị trí của nó giữa các ngôn ngữ và công cụ khác. Nó lý tưởng để nối các công cụ với nhau, đủ gần với hệ điều hành nên tiện lợi, và cũng không đòi cài thư viện như Python
      Người ta thường nói nên chuyển các script phức tạp hơn sang ngôn ngữ như Python, nhưng điều đó có thể thêm một lớp phức tạp không có lợi về lâu dài. Script Bash viết từ 20 năm trước vẫn chạy tốt, còn chương trình Python 20 năm trước rất có thể gặp vấn đề phiên bản
    • Scripting bằng Bourne shell đủ ổn, nên gần như không thể thay thế
      rc của Plan 9 gọn gàng hơn một chút, nhưng chỉ “tương tự mà gọn hơn” thì không đủ để khiến ai chuyển sang. Ngay bây giờ có thể cài những thứ tương tự nhưng tốt hơn ở https://pkgsrc.se/shells, vậy mà người ta vẫn không dùng, và cách chạy cũng không thay đổi đối với người khác
      Để thay thế một công nghệ đã bám rễ, nó phải tốt hơn gấp nhiều lần ở các khía cạnh cốt lõi. Plan 9 cũng tốt hơn họ UNIX, nhưng chưa đủ tốt để thay thế
      Rất khó tạo ra thứ gì đó đủ tốt để thay thế ngách của scripting bằng Bourne shell. Trước khi đạt được mức tốt đó, nó đã chuyển sang vị trí sinh thái hay vùng vấn đề của các ngôn ngữ scripting thực thụ như Perl, Python, Ruby
      Trong một vùng vấn đề hẹp, điểm tối ưu cục bộ hút hết không khí, khiến khó xuất hiện đối thủ cạnh tranh gần với tối ưu toàn cục trên lý thuyết
    • Tôi thực sự nghĩ là do di sản và quán tính

Các tính năng mới được thêm gần đây tuy đã được đặt lên trên sh/Bash khá ổn, nhưng rốt cuộc shell scripting chỉ là phương tiện để đạt mục đích, và phải tiến hóa chậm hơn nhiều so với các ngôn ngữ lập trình thông thường
Đặc điểm cốt lõi của Bash/sh là nó mang tính chống entropy. Gần như không có phát triển hay tiến hóa, nên ít có khả năng phải đau đầu vì dependency hay tính năng mới, và những thứ dùng được 20 năm trước vẫn tiếp tục là công cụ cơ bản
Theo thiết kế, nó trở thành một hệ thống kháng cự thay đổi, và khi chạm giới hạn thì tạo động lực để mọi người bước ra ngoài nó

  • Không chắc đó có thật sự là Bash không
    Phần lớn script của FreeBSD được viết cho sh, và vì sh là một phần của chuẩn POSIX nên có cảm giác được hỗ trợ rộng rãi hơn nhiều. Bash thì tôi xem là chỉ khá phổ biến
  • Điểm lớn là nó có ở khắp nơi
    Nhưng Bash quá tệ nên tôi đã tạo cả đống utility thu gọn namespace để dùng script Groovy. Có thể phát triển bằng IDE, hệ thống thư viện cũng an toàn, và Groovy đã mài nhẵn gần như mọi bất tiện của Java nên tốt hơn nhiều
  • Cũng có một debugger thực sự kiểu gdb khá mạnh: https://bashdb.sourceforge.net/
  • Quảng bá một chút liên quan: trước đây tôi từng làm một debugger pipeline cho Bash có giữ lại output trung gian
    Có vài hạn chế, nhưng nhìn chung có thể hữu ích: https://github.com/ketancmaheshwari/pd
  • Kỹ thuật die() thì hay, nhưng Bash có một đặc tính khó chịu. Nếu cố exit bên trong subshell thì chỉ subshell kết thúc, còn phần còn lại của script vẫn tiếp tục chạy
    Ví dụ nếu gọi die trong một pipeline như cat myfile | while read line; do ... die "Found match" ... done, thì echo "I don't want this line" phía sau vẫn sẽ được in ra
    Nhiều trường hợp có thể tránh subshell, và với ví dụ này shellcheck đúng khi chỉ ra UUOC; sửa lỗi đó thì vấn đề die trong subshell cũng được giải quyết
    Nhưng đôi khi không thể tránh subshell, hoặc muốn tránh thì script trở nên quá phức tạp. Khi đó có thể lưu PID ở đầu script bằng MYPID=$$, rồi kết liễu kiểu die() { echo "$1" >&2; kill -9 $MYPID; exit 1; }
    Tất nhiên cách này cũng có đánh đổi. Kiểu kết liễu này khá thô bạo, và không hiểu vì sao cũng không hoàn toàn đáng tin cậy
    • Chỉ cần thêm set -e thì khi subshell thoát với mã lỗi khác 0, script cũng sẽ kết thúc theo
      Tôi khó nghĩ ra lý do nào để bỏ set -e trong bất kỳ shell script nào
    • Nếu kill PID như vậy, liệu có thể tạo ra tiến trình zombie không?
  • Ở đầu script Bash, tôi luôn đặt set -euxo pipefail
    Nó khiến việc kiểm tra điều kiện khó hơn một chút, nhưng riêng pipefail thôi cũng đã nhiều lần chứng minh giá trị
    • https://mywiki.wooledge.org/BashPitfalls#set_-euo_pipefail
    • Cũng có thể bật cho vài dòng rồi tắt bằng set +x
      Nếu cứ bật liên tục thì khá nhàm
    • Đây là thiết lập như cứu mạng
      Tuy nhiên -x thì tôi để dành cho đến khi thật sự phải xem toàn bộ output debug lộn xộn