重新編寫 sh 或 ash 的 bash 條件時應該檢查什麼?
有時,不可能在系統上擁有 bash 的奢侈,但與 sh 或 ash 相比,在 bash 上更容易創建條件,應該驗證以確保條件不會在重寫時與典型的“意外運算符”中斷bash 到 sh 還是 ash ?
我不同意使用
((...))
and[[...]]
(假設這就是您所指的內容;請注意,這些運算符並非特定於 bash 並且來自 ksh)比標准[
或test
命令更容易創建條件。[[ ... ]]
並且(( ... ))
有自己的幾個問題¹,並且比[
.如果您因意外的操作員
[
錯誤而失敗,則很可能您沒有正確使用shell(通常,您忘記引用副檔名)而不是命令。[
如何正確和便攜地使用
[
/test
,最好參考它的 POSIX 規範。使其安全的一些基本規則是:
在其參數中引用所有單詞擴展 (
$var
,$(cmd)
, )。$((arithmetic))
這適用於每個命令,不僅適用[
於dash
.[[ ... ]]
並且(( ... ))
它們本身是具有自己特定語法的特殊構造(從 shell 到 shell 不同),其中可能會或可能不會引用事物並不總是很明顯。
[
不要在and旁邊傳遞超過 4 個參數]
。也就是說,不要使用已棄用的-o
and-a
運算符,也不要使用(
or)
進行分組。所以通常你的[
表達應該是:
- 中的單個參數
[ "$string" ]
,儘管我更喜歡[ -n "$string" ]
變體來明確說明您檢查$string
是否為非空。- 一元運算符 (
-f
,-r
,-n
…) 及其操作數,可選地以!
for 否定開頭。- 二元運算符 (
=
,-gt
…) 及其 2 個操作數,前面可選!
- 算術運算符的操作數必須是帶有可選符號的十進制整數文字常量。
dash
, likebash
接受這些操作數中的前導和尾隨空格,但並非所有[
實現都這樣做。檢查 POSIX 標準以獲取可移植運算符列表。請注意,它
dash
還有一些對標準的擴展(-nt
,-ef
,-k
,-O
,<
,>
² …)對於模式匹配,使用
case
構造 (case $var in ($pattern)...
) 而不是if [[ $var = $pattern ]]...
.對於擴展的正則表達式匹配,您可以使用
awk
:ere_match() { awk -- 'BEGIN{exit !(ARGV[1] ~ ARGV[2])}' "$1" "$2"; } if ere_match "$string" "$regex"...
代替:
if [[ $string =~ $regex ]]...
對於 AND/OR,使用or shell 運算符(具有相同
[
的優先級)連結多個呼叫,並使用命令組進行分組,這與您對任何其他命令使用的相同,而不僅僅是.&&``||``[
if [ "$mode" = "$mode1" ] || { [ -f "$file" ] && [ -r "$file" ] } then...
代替:
if [[ $mode = "$mode1" || ( -f $file && -r $file ) ]]; then...
一些
bash
’s/dash
’s/zsh
’s/ksh
’s/yash
’stest
/[
非 POSIX 運算符的一些標準等效項:
-a file
🠖-e file
(但請注意,對於無法訪問的文件的符號連結,兩者都返回 false)-k file
🠖has_t_bit() (export LC_ALL=C ls -Lnd -- "$1" 2> /dev/null | { unset -v IFS read mode rest && case $mode in (*[Tt]) true;; (*) false;; esac } )
-O file
->is_mine() (export LC_ALL=C ls -Lnd -- "$1" 2> /dev/null | { unset -v IFS read mode links fuid rest && euid=$(id -u) && [ "$fuid" -eq "$euid" ] } )
-G file
🠖is_my_group() (export LC_ALL=C ls -Lnd -- "$1" 2> /dev/null | { unset -v IFS read mode links fuid fgid rest && egid=$(id -g) && [ "$fgid" -eq "$egid" ] } )
-N file
:沒有等效項,因為沒有 POSIX API 可以完全精確地檢索文件的修改或訪問時間。file1 -nt file2
/file1 -ot file2
🠖newer() (export LC_ALL=C case $1 in ([/.]*) ;; (*) set "./$1" "$2"; esac case $2 in ([/.]*) ;; (*) set "$1" "./$2"; esac [ -n "$(find -L "$1" -prune -newer "$2" 2> /dev/null)" ] ) older() { newer "$2" "$1"; } newer file1 file2
如果任一文件不可訪問,請注意
-nt
/的行為在實現之間會有所不同。-ot
在這裡newer
並older
在這些情況下返回 false。
file1 -ef file2
🠖same_file() (export LC_ALL=C [ "$1" = "$2" ] && [ -e "$1" ] && return inode1=$(ls -Lid -- "$1" | awk 'NR == 1 {print $1}') && inode2=$(ls -Lid -- "$2" | awk 'NR == 1 {print $1}') && [ -n "$inode1" ] && [ "$inode1" -eq "$inode2" ] && dev1=$(df -P -- "$1" | awk 'NR == 2 {print $1}') && dev2=$(df -P -- "$2" | awk 'NR == 2 {print $1}') && [ -n "$dev1" ] && [ "$dev1" = "$dev2" ] ) 2> /dev/null same_file file1 file2
(假設
df
輸出中的文件系統源不包含空格;如果操作數實際上是安裝了文件系統的設備,則文件系統比較也無法正常工作)。
-v varname
🠖[ -n "${varname+set}" ]
-o optname
🠖case $- in (*"$single_letter_opt_name"*)...
。對於長選項名稱,我想不出標準的等價物。-R varname
🠖is_readonly() (export LC_ALL=C _varname=$1 is_readonly_helper() { [ "${1%%=*}" = "$_varname" ] && exit } eval "$( readonly -p | sed 's/^[[:blank:]]*readonly/is_readonly_helper/' )" exit 1 )
雖然要注意它很危險,因為它
sed
可能會受到 LINE_MAX 限制的影響,而變數沒有長度限制。另請注意, in
ksh93
是-R name
檢查是否name
是 nameref 變數。
"$a" == "$b"
🠖"$a" = "$b"
"$a" '<' "$b"
,"$a" '>' "$b"
🠖collate() { awk -- 'BEGIN{exit !(ARGV[1] "" '"$2"' ARGV[2] "")}' "$1" "$3" } collate "$a" '<' "$b" collate "$a" '>' "$b"
(
collate
也支持<=
,>=
,==
,!=
)。
[
, 或awk
‘s是否<
使用逐字節比較或區域設置的排序順序來比較字元串取決於實現。dash
尚未國際化,因此它僅適用於字節值。對於yash
’s[
,它基於語言環境排序規則。對於bash
,[[ $a < $b ]]
適用於語言環境排序規則,而[ "$a" '<' "$b" ]
適用於字節值。POSIX 要求awk
’s<
使用區域設置的排序規則,但某些awk
實現(如mawk
尚未國際化)因此使用字節值。要強制進行字節值比較,請將語言環境修復為C
.對於
awk
’s==
和!=
運算符,POSIX 過去要求使用排序規則,但很少有實現這樣做,現在 POSIX 規範未指定它。[
’s=
並且!=
總是進行逐字節比較,但請參見下面yash
的 ’s===
/!==
。
"$a" '=~' "$b"
🠖 見上文(在bash
/ksh
中,=~
運算符僅在 中可用[[...]]
,不是這種情況zsh
或yash
誰[
支持它)。"$string" -pcre-match "$pcre"
🠖 沒有等價物,因為 POSIX 沒有指定 PCRE。"$a" === "$b"
/"$a" !== "$b"
(strcoll()
比較)🠖expr "z $a" = "z $b" > /dev/null
/expr "z $a" != "z $b"
(還要注意一些awk
實現==
/!=
運算符也strcoll()
用於比較)。-o "?$option_name"
(是有效的選項名稱)🠖 沒有標準等效項,我可以將其視為set -o
或未set +o
指定的輸出格式。"$version1" -veq "$version2"
(和-vne
////比較版本-vgt
號-vge
)-vlt
🠖-vle
version_compare() { awk -- ' function pad(s, r) { r = "" while (match(s, /[0123456789]+/)) { r = r substr(s, 1, RSTART - 1) \ sprintf("%020u", substr(s, RSTART, RLENGTH)) s = substr(s, RSTART + RLENGTH) } return r s } BEGIN {exit !(pad(ARGV[1]) '"$2"' pad(ARGV[2]))}' "$1" "$3" }
用作
version_compare "$v1" '<' "$v2"
(或<=
,==
,!=
,>=
, ),使用與’ 運算符(或’ 的數字順序)>
相同的含義,並假設沒有一個數字超過 20 位。yash``zsh
bosh
shell[
也有and-D file
來-P file
測試一個文件是一個door還是event port。這些是特定於 Solaris 的文件類型,因此 POSIX 未涵蓋,但您始終可以定義:is_of_type() (export LC_ALL=C case "$2" in ([./]*) file="$2";; (*) file="./$2" esac [ -n "$(find -H "$file" -prune -type "$1")" ] )
然後,
is_of_type D "$file"
即使 POSIX 可能無法在門相關的 Solaris 上執行,您也可以這樣做,或者在其他系統和它們自己特定類型的文件上等效。¹ 參見例如’s 算術運算符中的命令注入漏洞
[[...]]``((...))
和 bash3.2+ 、ksh93 或 yash中的運算符混亂。=~
[
² 其中一些在實現中非常普遍,可能會在未來版本的 POSIX 標準中添加