Bash

重新編寫 sh 或 ash 的 bash 條件時應該檢查什麼?

  • January 11, 2021

有時,不可能在系統上擁有 bash 的奢侈,但與 sh 或 ash 相比,在 bash 上更容易創建條件,應該驗證以確保條件不會在重寫時與典型的“意外運算符”中斷bash 到 sh 還是 ash ?

我不同意使用((...))and [[...]](假設這就是您所指的內容;請注意,這些運算符並非特定於 bash 並且來自 ksh)比標准[test命令更容易創建條件。[[ ... ]]並且(( ... ))有自己的幾個問題¹,並且比[.

如果您因意外的操作員[錯誤而失敗,則很可能您沒有正確使用shell(通常,您忘記引用副檔名)而不是命令。[

如何正確和便攜地使用[/ test,最好參考它的 POSIX 規範

使其安全的一些基本規則是:

  • 在其參數中引用所有單詞擴展 ( $var, $(cmd), )。$((arithmetic))這適用於每個命令,不僅適用[dash. [[ ... ]]並且(( ... ))它們本身是具有自己特定語法的特殊構造(從 shell 到 shell 不同),其中可能會或可能不會引用事物並不總是很明顯。

  • [不要在and旁邊傳遞超過 4 個參數]。也就是說,不要使用已棄用的-oand-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’s test/[非 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在這裡newerolder在這些情況下返回 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 限制的影響,而變數沒有長度限制。

另請注意, inksh93-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中,=~運算符僅在 中可用[[...]],不是這種情況zshyash[支持它)。
  • "$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

  • boshshell[也有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 標準中添加

引用自:https://unix.stackexchange.com/questions/628428