Gỡ lỗi Bash
(wizardzines.com)- 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 -xin 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.shsẽ cho hiệu quả tương đương với việc thêmset -xở đầuscript.sh - Dùng cùng bẫy
DEBUGvàreadcó 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 -xin 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 đầuscript.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")' DEBUGread -psẽ in thông điệp và chờ nhấn Enter$BASH_SOURCElà tên tệp script$LINENOlà số dòng$BASH_COMMANDlà 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
diecó 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ạidie() { 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
Các ý kiến trên Hacker News
Có hàm ghi log
zdebugnằ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
set -x,PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: 'rất hữu íchCá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
shellcheck. Dù không trực tiếp bắt được vấn đề, nó vẫn chỉ ra các vấn đề tiềm ẩnNgoà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
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ỉ địnhCó 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...
Tuy nhiên, tôi hơi tiếc về triết lý của
dieđó. Hàmdievề 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
diechuyên biệt hơn.diecủ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 đóMột ví dụ triển khai ở đây: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
Dùng kiểu
some-command || fail "message"thì khisome-commandtrả về trạng thái thoát khác 0, nó sẽ tạo stack trace và thoát khỏi shellNếu muốn tạo stack trace trong hàm rồi trả về, có thể dùng
some-command || softfail "message" || return $?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ủ ý
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
rccủ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
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/
shlà 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ảnTheo 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ó
Phần lớn script của FreeBSD được viết cho
sh, và vìshlà 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ếnNhư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ó vài hạn chế, nhưng nhìn chung có thể hữu ích: https://github.com/ketancmaheshwari/pd
die()thì hay, nhưng Bash có một đặc tính khó chịu. Nếu cốexitbê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ạyVí dụ nếu gọi
dietrong 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 raNhiề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 đềdietrong subshell cũng được giải quyếtNhư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ểudie() { 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
set -ethì khi subshell thoát với mã lỗi khác 0, script cũng sẽ kết thúc theoTôi khó nghĩ ra lý do nào để bỏ
set -etrong bất kỳ shell script nàoset -euxo pipefailNó khiến việc kiểm tra điều kiện khó hơn một chút, nhưng riêng
pipefailthôi cũng đã nhiều lần chứng minh giá trịset +xNếu cứ bật liên tục thì khá nhàm
Tuy nhiên
-xthì tôi để dành cho đến khi thật sự phải xem toàn bộ output debug lộn xộn