test, [, và [[ (2020)
(jmmv.dev)- Trên các hệ Unix, có thể có tệp thực thi
/bin/[với tên chỉ là một ký hiệu một ký tự; cú pháp trông giống biểu thức điều kiện của shell thực ra cũng được xây dựng trên việc thực thi lệnh và mã thoát testđánh giá biểu thức và trả về 0 nếu đúng, 1 nếu sai; khi được gọi là[, nó còn kiểm tra xem đối số cuối cùng có phải là]hay không- Nhiều shell cũng cung cấp
testvà[dưới dạng lệnh tích hợp, nên thông báo lỗi hoặc hành vi của/bin/testbên ngoài và phần triển khai tích hợp trong shell có thể khác nhau [[, phần mở rộng của Bash, không phải lệnh bên ngoài mà là cú pháp tích hợp, nên áp dụng các quy tắc khác với[, chẳng hạn như trong ví dụ, nó không mở rộng globlong*mà so sánh như chuỗi literal- Với script cần tính di động,
[là lựa chọn phù hợp; nếu chỉ dành cho Bash thì nên dùng nhất quán[[, nhưng cần hiểu khác biệt về quy tắc mở rộng giữa hai cách để chọn cho đúng
Bản chất của /bin/[ và /bin/test
- Trên hệ thống Unix có thể tồn tại tệp thực thi
/bin/[với tên chỉ là một ký hiệu một ký tự- Lệnh ví dụ
ls /bin/?sẽ hiển thị/bin/[
- Lệnh ví dụ
/bin/[và/bin/testcó thể trỏ tới cùng một binary- Trong ví dụ, hai đường dẫn xuất hiện như các tệp có cùng inode và kích thước
- Tuy nhiên không nhất thiết hệ thống nào cũng phải dùng hard link
testlà chương trình dùng để đánh giá biểu thức trong shell- So sánh chuỗi
- So sánh số
- Kiểm tra điều kiện tệp
- Nếu kết quả đánh giá là đúng, nó trả về mã thoát 0; nếu sai, trả về 1
Vì sao [ hoạt động như một lệnh
test a = bkhó trông giống biểu thức điều kiện, nhưng nếu viết cùng logic dưới dạng[ a = b ]thì sẽ quen thuộc hơn[trông như cú pháp riêng, nhưng thực tế là một lời gọi lệnhif [ a = b ]; then ... fichạy lệnh[rồi kiểm tra mã thoát- Khi được gọi là
[, chương trình kiểm tra xem đối số cuối cùng có phải là dấu ngoặc vuông đóng]hay không
- Câu lệnh
ifkhông trực tiếp diễn giải biểu thức điều kiện, mà rẽ nhánh dựa trên mã thoát của lệnh được truyền vàotest a = a; echo $?là0test a = b; echo $?là1[ a = a ]; echo $?là0[ a = b ]; echo $?là1
- Cùng ngữ cảnh đó,
truevàfalsecũng có thể được xem là các binary phụ trợ trả về mã thoát
Khác biệt giữa binary bên ngoài và lệnh tích hợp của shell
- Vì
testvà[được dùng thường xuyên trong shell script, hầu hết shell cũng triển khai chúng dưới dạng lệnh tích hợp - Với cùng một đầu vào, đầu ra của binary bên ngoài và lệnh tích hợp của shell có thể khác nhau
/bin/test a blàtest: a: unexpected operatortest a blàdash: 2: test: a: unexpected operator
- Những khác biệt như vậy có thể xảy ra không chỉ với
testvà[, mà cả với những lệnh trông đơn giản nhưecho - Mỗi shell có phần triển khai tích hợp khác nhau, nên hành vi của script có thể thay đổi tùy theo shell thực thi
Các quy tắc riêng do phần mở rộng Bash [[ áp dụng
[[là phần mở rộng của Bash và có thể dùng thay cho[- Khác biệt lớn nhất là
[[luôn là cú pháp tích hợp- Khác với
[có thể được chạy dưới dạng binary bên ngoài, với[[, Bash có thể thay đổi các quy tắc ngôn ngữ bên trong biểu thức
- Khác với
- Trong ví dụ về glob,
[và[[hoạt động khác nhau- Sau
touch long-name,[ long* = long-name ] && echo matchsẽ in ramatch - Các quy tắc mở rộng shell thông thường được áp dụng cho đối số của lệnh
[, nênlong*được mở rộng thànhlong-nametrong thư mục [[ long* = long-name ]] && echo matchkhông in gì[[xử lýlong*như chuỗi literal và so sánh nguyên trạng vớilong-name, nên thất bại
- Sau
- Nếu là script chỉ dành cho Bash, bạn cũng có thể dùng các tính năng như khớp biểu thức chính quy
=~với[[
Nên chọn gì trong script
- Với shell script cần tính di động, nên dùng
[ - Cũng có thể dùng
test, nhưng đây không phải lựa chọn phổ biến - Nếu script chỉ dành cho Bash, nên dùng nhất quán
[[ - Bản thân shell cũng có các toán tử biểu thức như
!,&&,||- Các toán tử này hoạt động dựa trên trạng thái thoát của lệnh
grep ^hello$ ... && grep ^bye$ ...sẽ có mã thoát tổng thể là0nếu cả hai lệnh đều thành công- Nếu lệnh
grepđầu tiên thất bại, lệnh sau&&sẽ không thành công theo, nên mã thoát tổng thể là1
- Vì vậy có thể kết hợp biểu thức
test/[và toán tử logic của shell trong cùng một câu điều kiện- Ví dụ:
[ a = b ] || grep -q ^hello$ /usr/share/dict/words
- Ví dụ:
- POSIX không yêu cầu
/bin/[và/bin/testphải là hard link- Trên NetBSD, chúng là hard link
- macOS Catalina cung cấp các bản sao riêng của cùng một binary
- Debian testing cung cấp hai binary khác nhau
- Đặc tả POSIX không yêu cầu hai tệp này phải là liên kết với nhau
1 bình luận
Các ý kiến trên Hacker News
Tôi là tác giả bài gốc. Cảm ơn đã chia sẻ, và rất vui vì bài đã lên tới trang nhất. Có lẽ tiêu đề nên có thêm (2020), và vì "test" thực ra là chỉ một lệnh nên tốt hơn là không viết hoa
Tôi cũng có một bài liên quan viết năm 2021, trong đó đề cập cả toán tử
[[của bash, nên có thể sẽ thú vị trong ngữ cảnh này: https://jmmv.dev/2021/08/useless-use-of-gnu.html[[không phải là lệnh dựng sẵn, mà về cơ bản gần với thành phần cú pháp hơn. Có lẽ bên trong nó dùng một lệnh dựng sẵn gần như không thể truy cập trực tiếp, nhưng điểm thú vị là]]cũng là từ dành riêng dù nó không thể xuất hiện ở vị trí mà từ dành riêng có ý nghĩaTrong một số shell không phải bash, để khai báo một số loại hàm nhất định cần có từ khóa
function.$(shell)củamakecó thể tạo ra khác biệt hiệu năng đo được khi build nhiều target. Dù vậy, nếu không làm gì cả thì vẫn là thiệt, nên thông thường nếu muốn kích hoạt việc tái sinh thì nên dùnginclude. Việc GNU phớt lờ POSIX là hoàn toàn hợp lý, vì POSIX không hữu ích lắm để giải quyết phần lớn vấn đề thực tế.cũng hữu ích, và việc thêm tùy chọn vào cuối lệnh vừa gõ thì thật sự tiện. Thấy lệnh nào không hỗ trợ những thứ đó là tôi luôn bực mìnhTrong script, nhìn chung nhắm tới POSIX
shlà hợp lý. Ít nhất bạn phải biết mình có đang dùng cú pháp riêng của Bash hay không--ignore-case,set -o pipefailsẽ làm giảm tính di động của script. Bản thân điều đó là đúngNhưng bài không giải thích vì sao người dùng Linux phải quá quan tâm đến tính di động. OpenBSD và FreeBSD vẫn sống tốt, nhưng số người dùng quá ít nên có vẻ không phải đối tượng cần đặc biệt lo lắng. Vì công bằng, có thể nói rằng cũng nên cân nhắc các hệ điều hành đó, nhưng tiêu chuẩn ấy nên dừng ở đâu? Có phải cân nhắc cả những thứ obscure như
vxWorkskhông? Phía BusyBox, Alpine thì thú vị hơn, nhưng các thay đổi quá lớn nên dù sao gần như lúc nào cũng cần port riêng. Có lý do thuyết phục nào khác để phải quan tâm đến các hệ sinh thái không phải GNU không?if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; fichẳng phải chỉ là cách dùng shell bình thường hằng ngày sao?make $(shell …) expansion, trong khi đáng ra phải làmake $(shell ...) expansionNhư trong phần nội dung viết đúng, đó là ba dấu chấm chứ không phải một ký tự dấu ba chấm, nên bản thân
mldrcũng không đúng. Có lẽ hai lỗi không liên quan đã cùng lúc gây ảnh hưởngNếu đẩy ý cuối đi thêm một bước nữa thì có thể loại bỏ luôn cả khối
if.if [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fitrở thành[ a = b ] && echo "Oops!" || echo "Expected; phew!"Tôi không biết nên làm vậy thường xuyên đến mức nào, nhưng đôi khi hữu ích khi in thông tin debug có điều kiện ra lỗi chuẩn, như
[ "$debug" ] && echo "what's going on" >&2. Việc khốiifkiểm tra các lệnh thông thường cũng cho phép viết những thứ nhưif grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fi. Điều tôi chưa tìm hiểu là nên viết[ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ], hay dùng phép AND logic dựng sẵn củatestlà[ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ]. Nếu hiệu năng không phải vấn đề thì cả hai phía đều có vẻ hợp lý vì những lý do tương tự[ a = b ] && echo "Oops!" || echo "Expected; phew!"như một quy tắc chung. Có lẽ bash sẽ diễn giải dòng này như([ a = b ] && echo "Oops!") || echo "Expected; phew!"Vì vậy nếu chuỗi lệnh sau
&&thất bại thì đoạn mã sau||vẫn sẽ chạy. Ví dụ nếu>/dev/full echo "strings match"thất bại do lỗi ghi, thì dù chuỗi bằng nhau,"strings don't match"vẫn được in ra. Điều này khác với ý nghĩa của khốiifset -e— thứ nên dùng — thìif [ a = b ]; then echo "Oops!"; fihoạt động như mong đợi, nhưng[ a = b ] && echo "Oops!"sẽ thoát với lỗi khi biểu thứcakhông bằngb-a,-ovà các toán tử(,)được đánh dấu là sẽ bị loại bỏ. Xem mục "Application Usage" tại https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t... để biết chi tiết-ahay hai phép kiểm tra với&&, nếu có thể dùng đánh giá số học của bash thì không cần chạyexpr:[ $((1+1)) -eq 2 ]Từ vài năm trước tôi đã bắt đầu không dùng
[.testcủng cố ý rằng đây không phải cú pháp, mà chỉ là một lệnh như các lệnh khác. Và traman testdễ chịu hơn nhiều so với lụcman bashman test, mà còn có cảman [Bash có
help testnhư một cheat sheet nhanh. Lệnh[đã rất lâu đời, và đã có trong Version 7 Unix từ năm 1979Đồng ý.
[và[[vốn chỉ dành cho bash thực sự gây ra nhiều nhầm lẫn về chuyện điều gì đang diễn ra, nên khó mà tự tin đượcTuy nhiên, việc
[[được bảo đảm là lệnh tích hợp chắc chắn từng có mục đích vào thời hiệu năng script shell còn có ý nghĩa, mà cũng chưa phải quá xa xưatest. Tôi không viết script bash thường xuyên nên lúc nào cũng vấp ở câu lệnhif, đặc biệt là quy tắc khoảng trắng. Sau khi thấy lý do thì mọi thứ trở nên quá hiển nhiên, và dùngtestcũng thể hiện rõ hơn rằng ta chỉ đang truyền đối số mà thôiCái bẫy lớn nhất trong
[vàtestlà hành vi với một đối số. Ví dụ, để kiểm tra biến không rỗng, bạn có thể viết[ -n $FOO ]Nhưng nếu
FOOchưa được đặt, nó sẽ được mở rộng thành không có gì, chứ không phải chuỗi rỗng, và trở thành tương đương[ -n ]. POSIX yêu cầu dạng một đối số của[phải thành công nếu đối số đó, ở đây là"-n", không rỗng. Vì vậy nó báo sai rằng$FOOkhông rỗng. Biến nhất định phải được đặt trong dấu nháytestkhông có bẫy; cái bẫy nằm ở chính shellHành vi được nhắc tới là hợp lý. Vì
[ "$FOO" ]là dạng luôn kiểm tra xem nội dung có không rỗng hay không, bất kể nội dung là gì, kể cả"-n"$FOOchứa khoảng trắng, nó sẽ được mở rộng thành nhiều đối số. Cứ luôn đặt biến trong dấu nháy[ x"$FOO" != x"" ][ -n "${FOO?}" ]để script dừng ngay nếu$FOOlà null hoặc chưa được đặtchubot đã viết một tài liệu thú vị đào sâu vào các phần tinh tế hơn của
test/[/[[. Các bài khác trên blog đó cũng giải thích khá thú vị về những điểm kỳ lạ của shell¹ https://www.oilshell.org/blog/2017/08/31.html
² https://www.oilshell.org/blog/2016/11/18.html
Tôi hoàn toàn không biết
[là một chương trình, và chuyện nó kiểm tra đối số cuối có phải dấu ngoặc vuông đóng hay không thì hơi buồn cườiDù vậy, điều đó giải thích vì sao cần khoảng trắng ở hai bên dấu ngoặc vuông
[[chỉ dành cho bash. Nếu biết chắc mình sẽ chỉ dùng bash thì cứ dùng. Bài viết đã trình bày tốt phần chi tiếthttps://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
[[là đượczsh và ksh cũng có, và thật ra tôi gần như chắc nó bắt đầu từ ksh vào năm 1988 hoặc trước đó
testvà[được POSIX quy định và thường tồn tại dưới dạng binary thực sự. Tuy nhiên, lệnh tích hợp của shell cũng có thể che chúngTrong khi đó
[[không được POSIX quy định và thường chỉ tồn tại dưới dạng lệnh tích hợp của shellViệc chỉ nhắm tới shell mẫu số chung nhỏ nhất nghe như một nỗi lo cổ lỗ
Tôi không hiểu lắm vì sao câu lệnh
ifcuối lại gây nhầm lẫn. Nếu là vì khi mới học shell script, người ta thường mặc định[là một phần của ngôn ngữ script bash chứ không chỉ là một chương trình khác, thì giờ tôi hiểu. Nếu không phải vậy, mong có ai giải thích vì sao nó đáng ngạc nhiên[là binary, tôi cũng không hiểu vì sao lại khó hiểu. Nó trông như bash rất bình thườngTôi có gu rất mạnh về shell, nhưng lại không hợp với đa số thế giới
Tôi cho rằng tuyệt đối không nên dùng
[, mà chỉ nên dùngtest.[khiến người ta lầm tưởng cơ chế đó là một phần của cú pháp ngôn ngữ, nhưng thực ra nó chỉ là một “chương trình” khác. Ở đây “chương trình” bao gồm cả lệnh tích hợp và hàm.if/||/&&nhìn vào trạng thái thoát, còn một chương trình thì sau khi mở rộng chỉ có thể nhìn trạng thái thoát của thứ khác thông qua biến ma thuật$?, vốn chỉ là chuỗi.casenhìn vào chuỗi, nhưng không hoạt động dựa trên trạng thái thoát và cũng không đặt trạng thái thoát như một phần của thao táccase ... esac. “Chương trình” mới đặt trạng thái thoát. Ngoài ra,[/testchỉ nên được dùng khi đánh giá cấu trúc hệ thống tập tin, chẳng hạntest -f /dev/null. Tôi cho rằng đánh giá chuỗi nên dùngcase. Đương nhiên, khi nhìn hầu hết script tôi đều thấy ngứa ngáy, còn script tôi viết thì người khác thấy kỳ quặcKhi dùng shell, tôi thích đặt chương trình sau
ifở một dòng riêng rồi mới tớithen. Là để nhấn mạnh rằng thứ sauiflà “xem trạng thái thoát của lệnh cuối cùng trước then”. Ví dụ với cấu trúc nhưif; ls /tmp/goober; test -d /tmp/goober; echo "I'll always execute the then clause because echo will always return a 0 return code"; then ... fi,ifrốt cuộc chỉ nhìn trạng thái thoát củaecho, nên nhánh then luôn được chạytestlà bằng chứng rằng tôi đồng ý. Đó là con đường cô độc... Tôi đổ lỗi cho Google shell style guideTôi hình thành thói quen thích dùng
testkhi viết script phải chạy được trên cảshlẫnbash, nhưng vẫn tiếp tục dùng vì về mặt ngữ nghĩa nó hợp lý hơn việc coi ký tự[như một lệnh. Việc]không phải binary riêng mà là đối số của[cũng kỳ lạ. Tôi hiểu lý do kỹ thuật, nhưng nó vẫn có cảm giác như một trò hacktest. Hàng chục người!Tôi được học là chỉ dùng
[[khi muốn khớp biểu thức chính quy. Ví dụ:if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fiNgoài ra thì chỉ dùng
"test"hoặc"[". Tôi đang ở dòng bash thứ 85.000. Không phải nói bash tuyệt vời, nhưng đến nay nó vẫn đáp ứng nhu cầu của tôi cho rất nhiều việcexprcó thể khớp biểu thức chính quy cơ bản và cũng có thể trả về nhóm bắt1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...
[ "$foo" = bar ] && echo Yeslà đượcVới so khớp chuỗi con, phần lớn trường hợp
[và glob*là đủ. Kiểu như[ "$bar" = extra* ] && echo '$bar began with extra'. Phương ngữ regex của Bash khá thô sơ nên hầu như không đáng để cố dùng. Với việc phức tạp thì nên dùng công cụ khác nhưgrep,awk,perl. Nếu cứ ám ảnh muốn làm mọi thứ bằng bash, kể cả các tác vụ phức tạp cần khả năng tái sử dụng cao hơn, tính mô-đun tốt hơn và các kiểu dữ liệu tích hợp, thì lợi ích thu được sẽ giảm rất nhanh.