Handle exception in Bash
Published:
Giới thiệu
Trong các ngôn ngữ lập trình bậc cao phổ biến như Java khi có exception xảy ra chúng ta thường có hai cách giải quyết đó là dừng chương trình bằng việc ném ra ngoại lệ sử dụng throw/throws keyword hoặc bắt ngoại lệ để chương trình có thể tiếp tục thực thi bằng cách sử dụng try/catch block.
Môi trường
- Bash shell
Kiến thức cơ bản
- Terminallà môi trường cho phép nhận- input(text) từ bàn phím…, gửi- input(command) đến- shellvà nhận- outputtrả về từ- shell.
- Shellvừa là một trình thông dịch (- command line interpreter) giúp bạn thực thi câu lệnh mà người dùng gửi đến thông qua- terminal/terminal-emulator, vừa là một ngôn ngữ lập trình. Nếu như với Java, chúng ta sử dụng file với đuôi- .javathì với shell, mà trong phạm vi bài viết này là- bash(- Bourne-Again SHell), chúng ta sử dụng file với đuôi- .sh(stand for shell) hay còn được gọi là- bash script.
- Mỗi lần bạn gõ câu lệnh trên terminal và nhấn phím - Enter,- bashsẽ thực thi câu lệnh đó và trả về một- exit statushay còn được gọi là- return code, là một số nguyên nằm trong khoảng [0,255]. Theo quy ước thì khi một câu lệnh thực thi thành công sẽ trả về- 0 exit status, còn nếu có lỗi xảy ra thì- non-zero exit statussẽ được trả về tỉ như- command not foundlà- 127. Chúng ta có một biến đặc biệt lưu trữ- exit statuscủa câu lệnh đã được thực thi trước đó, và để kiểm tra bạn có thể làm như sau:- echo $?- Ex:ls logbasex echo $?
- Outputls: cannot access 'logbasex': No such file or directory 2
 
- Ex:
- File Descriptorlà một số nguyên dương đại diện cho một tập tin đang được mở trong một process. Một- terminal emulatorcó 3 file descriptors mặc định đó là- stdin,- stdout,- stderr.- ls -la /proc/$$/fd- Output - total 0 dr-x------ 2 logbasex logbasex 0 Thg 3 14 07:06 . dr-xr-xr-x 9 logbasex logbasex 0 Thg 3 14 07:06 .. lrwx------ 1 logbasex logbasex 64 Thg 3 14 07:06 0 -> /dev/pts/5 lrwx------ 1 logbasex logbasex 64 Thg 3 14 07:06 1 -> /dev/pts/5 lrwx------ 1 logbasex logbasex 64 Thg 3 14 07:06 2 -> /dev/pts/5 lrwx------ 1 logbasex logbasex 64 Thg 3 14 08:35 255 -> /dev/pts/5- stdin: Standard Input (File descriptor 0)
- stdout: Standard Output (File descriptor 1)
- stderr: Standard Error (File descriptor 2)
  
- Pipelinelà một chuỗi của 2 hay nhiều câu lệnh được phân cách bằng kí tự- |hoặc- |&cho phép chúng ta truyền dữ liệu từ process này sang process kia mà không cần lưu dữ liệu trực tiếp lên ổ cứng. Trong đó- |pipes redirect- STDOUTcủa process này sang- STDINcủa process kia còn- |&redirect cả- STDOUTvà- STDERRVí dụ:- ls $PWD | grep logbasex.sh- Đây là một - pipelinecơ bản có thể được hiểu như sau: Liệt kê các tập tin trong thư mục hiện tại rồi tìm kiếm những tập tin có tên là- logbasex.shvà hiển thị kết quả ra màn hình, tức là- output(- STDOUT) của câu lệnh thứ nhất- ls $PWDđược sử dụng làm- input(- STDIN) của câu lệnh thứ hai- grep logbasex.sh.
- Ở đây mình chỉ giới thiệu qua các khái niệm cơ bản đủ để hiểu được nội dung bài viết. Mọi người có thể xem thêm ở đây
Tạo bug
- Để xử lý ngoại lệ thì đầu tiên phải có chương trình ném ra ngoại lệ trước đã. Mình tạo một file có tên error.shvới nội dung như sau:#!/bin/bash ls logbasex good morning love
- Execute filebash error.sh
- Outputls: cannot access 'logbasex': No such file or directory error.sh: line 4: good: command not found error.sh: line 5: love: command not found
Xử lý ngoại lệ
I. Trường hợp cơ bản
Như đã thấy ở trên, cả 3 câu lệnh đều thực thi không thành công, chương trình kết thúc và chúng ta có 3 dòng báo lỗi hiển thị trên màn hình. Ok. Nhưng bây giờ nếu bạn muốn chương trình kết thúc ngay tại dòng lệnh đầu tiên mà thực thi không thành công thì làm thế nào?
Thật may mắn là bash có hỗ trợ một built-in command gọi là set với option -e.
Exit immediately if a pipeline (see Pipelines), which may consist of a single simple command (see Simple Commands), a list (see Lists), or a compound command (see Compound Commands) returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a
&&or||list except the command following the final&&or||, any command in a pipeline but the last, or if the command’s return status is being inverted with !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits.This option applies to the shell environment and each subshell environment separately (see Command Execution Environment), and may cause subshells to exit before executing all the commands in the subshell.
If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.
- Chúng ta tạo một script mới có tên error-e.shvới nội dung như sau#!/bin/bash set -e ls logbasex good morning love
- Thực thibash -e error.sh OR bash error-e.sh
- Outputls: cannot access 'logbasex': No such file or directory
II. Ngoại lệ của set -e option (any command in a pipeline but the last)
- Chúng ta tạo một script mới có tên error-pipeline.shvới nội dung như sau#!/bin/bash set -e logbasex | echo "hello" echo "good morning"
- Thực thibash error-pipeline.sh
- Outputhello error-pipeline.sh: line 4: logbasex: command not found good morning
Như bạn có thể thấy, ngoại lệ ném ra ở dòng thứ 4 nhưng câu lệnh ở dòng thứ 5 vẫn được thực thi là bởi vì câu lệnh logbasex không phải là câu lệnh cuối cùng trong một pipelines.
Và thật may mắn là câu lệnh set cung cấp cho chúng ta option -o pipefail
-o pipefail
If set, the return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands in the pipeline exit successfully. This option is disabled by default.
Nếu sử dụng thì exit status trả về của một pipeline sẽ là exit status của câu lệnh ngoài cùng bên phải mà có exist status khác
0hoặc là bằng0nếu tất cả câu lệnh trong một pipeline đều được thực thi thành công.
Ví dụ với script dưới đây
set -o pipefail
logbasex | grep some-string /non/existent/file | sort
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]}" $?
thì output sẽ là
grep: /non/existent/file: No such file or directory
logbasex: command not found
127 2 0 2
Như bạn có thể thấy, exit status của pipeline trên chính là bằng 2.
Và…
Việc bây giờ là sửa lại script và thực thi để thấy ngay điều kì diệu
#!/bin/bash
set -eo pipefail
logbasex | echo "hello"
echo "good morning"
Oops :(( Output nhìn có vẻ không đẹp lắm, sẽ thật tuyệt nếu bỏ được error log ra khỏi output. Và thật may mắn, bash hỗ trợ chúng ta redirect error ouput (STDERR) vào hố đen vô cực như sau
bash error-pipefail.sh 2> /dev/null
Mình sẽ giải thích câu lệnh trên ở một bài viết khác.
 
II. Bỏ qua ngoại lệ
Trước giờ chúng ta chỉ nói đến vấn đề ném ra ngoại lệ, nhưng bây giờ chúng ta muốn chương trình vẫn tiếp tục thực thi khi bắt gặp ngoại lệ thì làm thế nào?
Thú thật thì chả biết làm thế nào ngoài sử dụng toán tử OR cả. Toán tử này trong bash script được biểu diễn dưới kí tự || (double pipe) và cách dùng cũng tương tự như trong Java, có hỗ trợ short-circuit.
- Việc bây giờ là tạo một script mới với tên error-suppression.shcó nội dung như sau:#!/bin/bash set -eo pipefail logbasex | echo "hello" || true echo "good morning"
- Thực thibash error-suppression.sh 2> /dev/null
- Outputhello good morning
IV. Xử lí hậu kì
Giả sử trong quá trình thực thi một script bất kì, bạn tạo ra vô số tập tin tạm thời (temporary files) trên hệ thống và bạn định sẽ xóa hết chúng ở cuối script. Nhưng điều gì sẽ xảy ra nếu có một ngoại lệ được ném ra và script của bạn thực thi không thành công? Nhiều khả năng là không có gì cho đến một ngày đẹp trời nào đó :)
 
Đến đây không hẳn là hết cách, tuy nhiên thì bây giờ nếu bạn quyết định dọn dẹp hệ thống thì dọn dẹp cái gì bây giờ, nói chung về cơ bản là khá mất thời gian và cách tốt nhất là khi ngoại lệ xảy ra thì chúng ta cũng đồng thời xóa hết những tập tin đã tạo ra trong quá trình chạy script.
Vẫn may mắn như thường lệ là bash có hỗ trợ câu lệnh trap giúp chúng ta làm việc đó.
- Tạo một script mới với tên error-clean-up-file.shcó nội dung như sau#!/bin/bash TMP=$(mktemp /tmp/logbasex.XXXXXX) trap 'rm -f $TMP' ERR trap 'echo $TMP' ERR /usr/bin/false
- Output/tmp/logbasex.5toDp0
Khi bắt gặp một câu lệnh trong script trả về non-zero exit status trong quá trình thực thi mà ở đây là /usr/bin/false thì ERR trap sẽ được kích hoạt để xóa tập tin đã tạo ra và hiển thị tên tập tin đó ra màn hình. Tuy nhiên, chú ý rằng nếu ngoại lệ xảy ra trong function thì câu lệnh trap sẽ không được thực thi.
#!/bin/bash
function tempfile {
    mktemp /tmp/logbasex.XXXXXX
    false
}
TMP=$(tempfile)
trap 'rm -f $TMP' ERR
trap 'echo $TMP' ERR
Và đó là lý do mà chúng ta lại một lần nữa dùng đến câu lệnh set
set -o errtrace
Việc bây giờ là sửa lại script để thấy ngay điều kì diệu
#!/bin/bash
set -o errtrace
function tempfile {
    mktemp /tmp/logbasex.XXXXXX
    false
}
TMP=$(tempfile)
trap 'rm -f $TMP' ERR
trap 'echo $TMP' ERR
Thanks for the following precious resources
- https://stackoverflow.com/questions/25378845/what-does-set-o-errtrace-do-in-a-shell-script
- https://www.baeldung.com/linux/creating-temp-file
- https://www.shell-tips.com/bash/functions/
- https://linuxhint.com/bash_error_handling/
- https://linuxhint.com/bash_pipe_tutorial/
- https://gist.github.com/mohanpedala/1e2ff5661761d3abd0385e8223e16425
- https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
Thank for reading :blush:.

