聊一聊shell中的條件控制

以前趕鴨子上架寫過一個不算太複雜的bash腳本,被bash中的條件控制噁心到了,如今抽絲剝繭深刻學習下,防止之後再掉坑裏html

基礎用法

if commands; then
     commands
[elif commands; then
     commands...]
[else
     commands]
fi
複製代碼

bash中的條件控制的基本形式如上, ; 能夠與換行互相替換,這篇文章主要來研究下if後面接着的commandslinux

退出狀態

當命令執行完畢後,命令(包括咱們編寫的腳本和 shell 函數)會給系統發送一個值,叫作退出狀態。 這個值是一個 0 到 255 之間的整數,說明命令執行成功或是失敗。按照慣例,一個零值說明成功,其它全部值說明失敗。 Shell 提供了一個參數 $? ,咱們能夠用它檢查退出狀態。git

[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2
複製代碼

這是很重要的概念,意味着咱們能夠不只僅使用test命令來進行條件控制,咱們可使用一個普通的命令來做爲判斷條件。github

## 下載某個文件,成功則作一些事情,不成功就不作
if wget xxx ;then;commands;fi
複製代碼

true 和 false 是什麼

在shell中true和false並不像其餘語言同樣是布爾值,而是兩個內建的命令
它們不作任何事情,除了以一個0或1退出狀態來終止執行。 True 命令老是執行成功,而 false 命令老是執行失敗:正則表達式

[me@linuxbox~]$ true
[me@linuxbox~]$ echo $?
0
[me@linuxbox~]$ false
[me@linuxbox~]$ echo $?
1
複製代碼

test命令

常常與 if 一塊使用的命令是 test。它有兩種等價形式shell

test expression
[ expression ]  ## expression先後的空格必不可少
複製代碼

這裏的 expression 是一個表達式,其執行結果是 true 或者是 false。當表達式爲真時,這個 test 命令返回一個零 退出狀態,當表達式爲假時,test 命令退出狀態爲1。這句話比較重要,在這裏踩了幾個坑,後面會詳細介紹。
test命令中的expression能夠對文件、字符串和整數的狀態進行判斷express

文件表達式

表達式 若是下列條件爲真則返回True
file1 -ef file2 file1 和 file2 擁有相同的索引號(經過硬連接兩個文件名指向相同的文件)。
file1 -nt file2 file1新於 file2。
file1 -ot file2 file1早於 file2。
-b file file 存在而且是一個塊(設備)文件。
-c file file 存在而且是一個字符(設備)文件。
-d file file 存在而且是一個目錄。
-e file file 存在。
-f file file 存在而且是一個普通文件。
-g file file 存在而且設置了組 ID。
-G file file 存在而且由有效組 ID 擁有。
-k file file 存在而且設置了它的「sticky bit」。
-L file file 存在而且是一個符號連接。
-O file file 存在而且由有效用戶 ID 擁有。
-p file file 存在而且是一個命名管道。
-r file file 存在而且可讀(有效用戶有可讀權限)。
-s file file 存在且其長度大於零。
-S file file 存在且是一個網絡 socket。
-t fd fd 是一個定向到終端/從終端定向的文件描述符 。
這能夠被用來決定是否重定向了標準輸入/輸出錯誤。
-u file file 存在而且設置了 setuid 位。
-w file file 存在而且可寫(有效用戶擁有可寫權限)。
-x file file 存在而且可執行(有效用戶有執行/搜索權限)。

一個簡單的例子bash

FILE=~/.bashrc
if [ -e "$FILE" ]; then
    if [ -f "$FILE" ]; then
        echo "$FILE is a regular file."
    fi
    if [ -d "$FILE" ]; then
        echo "$FILE is a directory."
    fi
    if [ -r "$FILE" ]; then
        echo "$FILE is readable."
    fi
    if [ -w "$FILE" ]; then
        echo "$FILE is writable."
    fi
    if [ -x "$FILE" ]; then
        echo "$FILE is executable/searchable."
    fi
else
    echo "$FILE does not exist"
    exit 1
fi
複製代碼

字符串表達式

表達式 若是下列條件爲真則返回True
string string 不爲 null。
-n string 字符串 string 的長度大於零。
-z string 字符串 string 的長度爲零。
string1 = string2
string1 == string2 string1 和 string2 相同。 單或雙等號均可以,不過雙等號更受歡迎。
string1 != string2 string1 和 string2 不相同。
string1 > string2 sting1 排列在 string2 以後。
string1 < string2 string1 排列在 string2 以前。

一個例子網絡

ANSWER=maybe
if [ -z "$ANSWER" ]; then
    echo "There is no answer." >&2
    exit 1
fi
if [ "$ANSWER" = "yes" ]; then
    echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
    echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
    echo "The answer is MAYBE."
else
    echo "The answer is UNKNOWN."
fi
複製代碼

整型表達式

表達式 若是爲真...
integer1 -eq integer2 integer1 等於 integer2。
integer1 -ne integer2 integer1 不等於 integer2。
integer1 -le integer2 integer1 小於或等於 integer2。
integer1 -lt integer2 integer1 小於 integer2。
integer1 -ge integer2 integer1 大於或等於 integer2。
integer1 -gt integer2 integer1 大於 integer2。

例子socket

INT=-5
if [ -z "$INT" ]; then
    echo "INT is empty." >&2
    exit 1
fi
if [ $INT -eq 0 ]; then
    echo "INT is zero."
else
    if [ $INT -lt 0 ]; then
        echo "INT is negative."
    else
        echo "INT is positive."
    fi
    if [ $((INT % 2)) -eq 0 ]; then
        echo "INT is even."
    else
        echo "INT is odd."
    fi
fi
複製代碼

結合表達式

AND					-a
OR					-o
NOT					!
複製代碼

加強版的[]——[[ ]]

[[ expression ]] 相似於test命令,可是它的功能更爲強大

支持正則表達式

string1 =~ regex
複製代碼

==操做符支持模式匹配

[me@linuxbox ~]$ FILE=foo.bar
[me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
> echo "$FILE matches pattern 'foo.*'"
> fi
foo.bar matches pattern 'foo.*'
複製代碼

結合表達式支持&& ||

能夠直接使用&& ||而不用-a -o
[[ ... && ... && ... ]] 和 [ ... -a ... -a ...] 不同,[[ ]] 是邏輯短路操做,而 [ ] 不會進行邏輯短路

支持數字運算

test命令只支持數字的比較而不支持 + - * / % ,[[ ... ]]能夠支持

ASCII比較

test命令中ASCII比較須要轉義,而[[ ]]中不須要

[ aaa \> bbbb ]
[[ aaa > bbbb ]]
複製代碼

這裏有個須要注意的點, > 或者 \> 比較的ASCII,在比較數字時很容易出錯
例如

[[ "a" != "b" && 10 > 2 ]] ## 10的第一位是1,ASCII值小於2,因此這個表達式的值是false
複製代碼

若是須要進行數字的比較須要使用 -le 等命令選項,或者使用(( ))

let expr (( ))

除了[[ ]]能夠進行整數運算以外還有幾種其餘的方式

## expr進行數學運算,注意空格,使用*須要轉義\*
expr 2 + 2
## 將運算結果賦值,使用反引號或者$() 
s=`expr 2 + 3` 
echo $s
5
## 等價爲
s=$[2+3] 	## 不用考慮空格。*也不用轉義
## 等價爲
let s=2+3
## 等價爲
s=$((2+3))

## ((expression))能夠用來進行整數的比較
[[ "a" != "b" ]] && ((10 > 2)) ## 整數比較正確的寫法
複製代碼

布爾型變量最佳實踐

假設一個變量 ENABLE 被賦值爲true/false,咱們應該怎麼去使用它呢

## 直接使用$ENABLE,通常狀況下沒有問題
## 可是若是ENABLE是個未定義的變量或者空字符串又或者是一個退出狀態爲true的命令,這個if都會判斷爲true
ENABLE=false
if $ENABLE
then
    echo true
else
    echo false
fi
## ENABLE是一個空字符串
➜  ~ ENABLE=""
➜  ~ if $ENABLE;then;echo true;else;echo false;fi
true
## ENABL爲1
➜  ~ ENABLE=1
➜  ~ if $ENABLE;then;echo true;else;echo false;fi
zsh: command not found: 1
false
## ENABLE=echo $aaaa
➜  user-1678701-1561966146 ENABLE=echo $aaaa
➜  user-1678701-1561966146 echo $ENABLE
echo
➜  user-1678701-1561966146 if $ENABLE;then;echo true;else;echo false;fi

true

複製代碼

爲了不變量未被定義仍被當作true執行,即便有一個變量是true/false,咱們仍須要將它當作字符串來處理

ENABLE=""
if [[ $ENABLE = "true" ]]
then
    echo true
else
    echo false
fi
複製代碼

若是不使用 [[ ]] 而是 [ ] 會發現當變量未定義時會發生異常

if [ $aaaaaaaa = "true" ]
then
    echo true
else
    echo false
fi

## 執行結果,這裏雖然也打印出了false,可是是由於$aaaaaaaa = "true"執行失敗,而不是test命令對錶達式的判斷
./hello_world.sh: line 5: [: =: unary operator expected
false
複製代碼

因此建議能用[[ ]] 的地方所有用[[ ]] ,而用 [ ] 時須要在引用變量時再套個雙引號 if [ "$aaaaaaaa" = "true" ]

關於test命令一些理解

變量非空判斷

以前說過在test命令這踩了幾個坑,我碰到的場景是有個布爾值變量,而後根據其餘條件,兩個條件&&操做以後進行判斷

ENABLE=false
TYPE=Debug
if [[ $ENABLE && $TYPE = "Debug" ]]
then
    echo true
else
    echo false
fi
複製代碼

這種寫法不管 ENABLE 是true仍是false最後都會打印true
嘗試修改下 TYPE 的值,發現打印出了false,因此問題出在前一個語句中
[[ $ENABLE ]] 單獨拿出來測試,發現只要ENABLE賦值爲非空值,該條件都爲true
在這裏產生了一個誤解,test命令並不能對true/false自己進行真值判斷。
而 [[ $ENABLE ]] 的真正含義是對變量ENABLE進行非空判斷
正確的用法應該是

## 將$ENABLE變爲test命令能夠正確支持的形式
## 這裏$ENABLE被當作字符串
if [[ $ENABLE = true && $TYPE = "Debug" ]] 

## 另外一種方式是將$ENABLE單獨做爲一個命令
## 這裏的$ENABLE是一個命令
if $ENABLE && [[ $TYPE = "Debug" ]] 
複製代碼

要不要空格這是個問題

不少教程上都說在 [[ ]] 要捨得加空格,簡單測試一下

[[ 1 == 2 ]] ## 結果爲false
[[ 1==2 ]] ## 結果爲真
複製代碼

這裏簡單談下本身的理解,未經查證,有誤歡迎指正

  • 這裏的 [ ] [[ ]] 均可以看作test命令,而其中的內容均可以看作test命令的參數
  • test命令沒有參數時退出狀態爲1,表示false
  • test命令有一個參數時退出狀態爲0,表示true
  • 當參數大於一個時,test只支持字符串/文件/整數判斷
  • test不支持執行一個命令獲取命令的退出狀態(這是if語句自己的功能)

[[ 1==2 ]]爲真是由於將1==2總體做爲了命令參數
而[[ 1 == 2 ]]則是三個參數,其中==是tes支持的操做符

再聯繫上面的 [[ false ]] 爲true,這裏也是將true/false做爲了一個參數,而不是執行true/false命令
因此建議在寫條件判斷的時候考慮成命令參數,分清楚比較對象和操做符

如何修改一個布爾值的變量

說回到以前ENABLE和其餘條件結合的例子,當時我想將結合的真值直接從新賦值給ENABLE
相似下面的代碼

ENABLE=true
TYPE=Debug
ENABLE= $($ENABLE && [[ $TYPE = "Debug" ]]) ## ENABLE爲空
複製代碼

失敗的緣由咱們來仔細探究一下

函數/命令的返回值

在shell中函數/命令實際是沒法將一個值帶回到調用方的,return的是函數執行的狀態,而命令展開等展開的實際是標準輸出的數據。而true/false/test命令都是沒有標準輸出的。這裏咱們指望的是$ENABLE執行true命令,在將true值輸出到標準輸出再與[[ $TYPE = "Debug" ]]的輸出結合,實際狀況並不是如此

如何符合預期的修改

寫了個函數

getBoolValue(){
## 將字符串當作命令執行
if eval $*
then
    echo true
else
    echo false
fi
}
ENABLE=true
TYPE=Debug
## &&須要轉義才能做爲getBoolValue函數的參數
## 不然會被看作getBoolValue $ENABLE做爲一個總體[[ $TYPE = "Debug" ]]做爲一個總體,而後&&操做
ENABLE=$(getBoolValue $ENABLE \&\& [[ $TYPE = "Debug" ]])
echo $ENABLE
複製代碼

固然這個函數只是寫來試驗下,沒有太多的實際價值

總結

  • if後面的條件能夠是普通的命令
  • true/false是內建的命令,不作任何事情,除了以一個0或1退出狀態來終止執行。
  • test命令只能處理它支持的字符串/文件/整數表達式,命令/函數做爲參數只會當作字符串不會獲得正確的結果
  • 儘可能使用[[ ]] 代替 [ ]
  • [[ ]]中的空格須要注意
  • 使用到數字運算和比較使用(( ))

參考資料

shell if [[ ]]和[ ]區別 || &&
流程控制:if 分支結構

相關文章
相關標籤/搜索