怎樣用 Bash 編程:邏輯操作符和 shell 擴展
Bash 是一種強大的編程語言,完美契合命令行和 shell 腳本。本系列(三篇文章,基於我的 三集 Linux 自學課程)講解如何在 CLI 使用 Bash 編程。
第一篇文章 講解了 Bash 的一些簡單命令行操作,包括如何使用變數和控制操作符。第二篇文章探討文件、字元串、數字等類型和各種各樣在執行流中提供控制邏輯的的邏輯運算符,還有 Bash 中的各類 shell 擴展。本系列第三篇也是最後一篇文章,將會探索能重複執行操作的 for
、while
和 until
循環。
邏輯操作符是程序中進行判斷的根本要素,也是執行不同的語句組合的依據。有時這也被稱為流控制。
邏輯操作符
Bash 中有大量的用於不同條件表達式的邏輯操作符。最基本的是 if
控制結構,它判斷一個條件,如果條件為真,就執行一些程序語句。操作符共有三類:文件、數字和非數字操作符。如果條件為真,所有的操作符返回真值(0
),如果條件為假,返回假值(1
)。
這些比較操作符的函數語法是,一個操作符加一個或兩個參數放在中括弧內,後面跟一系列程序語句,如果條件為真,程序語句執行,可能會有另一個程序語句列表,該列表在條件為假時執行:
if [ arg1 operator arg2 ] ; then list
或
if [ arg1 operator arg2 ] ; then list ; else list ; fi
像例子中那樣,在比較表達式中,空格不能省略。中括弧的每部分,[
和 ]
,是跟 test
命令一樣的傳統的 Bash 符號:
if test arg1 operator arg2 ; then list
還有一個更新的語法能提供一點點便利,一些系統管理員比較喜歡用。這種格式對於不同版本的 Bash 和一些 shell 如 ksh(Korn shell)兼容性稍差。格式如下:
if [[ arg1 operator arg2 ]] ; then list
文件操作符
文件操作符是 Bash 中一系列強大的邏輯操作符。圖表 1 列出了 20 多種不同的 Bash 處理文件的操作符。在我的腳本中使用頻率很高。
操作符 | 描述 |
---|---|
-a filename |
如果文件存在,返回真值;文件可以為空也可以有內容,但是只要它存在,就返回真值 |
-b filename |
如果文件存在且是一個塊設備,如 /dev/sda 或 /dev/sda1 ,則返回真值 |
-c filename |
如果文件存在且是一個字元設備,如 /dev/TTY1 ,則返回真值 |
-d filename |
如果文件存在且是一個目錄,返回真值 |
-e filename |
如果文件存在,返回真值;與上面的 -a 相同 |
-f filename |
如果文件存在且是一個一般文件,不是目錄、設備文件或鏈接等的其他的文件,則返回 真值 |
-g filename |
如果文件存在且 SETGID 標記被設置在其上,返回真值 |
-h filename |
如果文件存在且是一個符號鏈接,則返回真值 |
-k filename |
如果文件存在且粘滯位已設置,則返回真值 |
-p filename |
如果文件存在且是一個命名的管道(FIFO),返回真值 |
-r filename |
如果文件存在且有可讀許可權(它的可讀位被設置),返回真值 |
-s filename |
如果文件存在且大小大於 0,返回真值;如果一個文件存在但大小為 0,則返回假值 |
-t fd |
如果文件描述符 fd 被打開且被關聯到一個終端設備上,返回真值 |
-u filename |
如果文件存在且它的 SETUID 位被設置,返回真值 |
-w filename |
如果文件存在且有可寫許可權,返回真值 |
-x filename |
如果文件存在且有可執行許可權,返回真值 |
-G filename |
如果文件存在且文件的組 ID 與當前用戶相同,返回真值 |
-L filename |
如果文件存在且是一個符號鏈接,返回真值(同 -h ) |
-N filename |
如果文件存在且從文件上一次被讀取後文件被修改過,返回真值 |
-O filename |
如果文件存在且你是文件的擁有者,返回真值 |
-S filename |
如果文件存在且文件是套接字,返回真值 |
file1 -ef file2 |
如果文件 file1 和文件 file2 指向同一設備的同一 INODE 號,返回真值(即硬鏈接) |
file1 -nt file2 |
如果文件 file1 比 file2 新(根據修改日期),或 file1 存在而 file2 不存在,返回真值 |
file1 -ot file2 |
如果文件 file1 比 file2 舊(根據修改日期),或 file1 不存在而 file2 存在 |
圖表 1:Bash 文件操作符
以測試一個文件存在與否來舉例:
[student@studentvm1 testdir]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 does not exist.
[student@studentvm1 testdir]$
創建一個用來測試的文件,命名為 TestFile1
。目前它不需要包含任何數據:
[student@studentvm1 testdir]$ touch TestFile1
在這個簡短的 CLI 程序中,修改 $File
變數的值相比於在多個地方修改表示文件名的字元串的值要容易:
[student@studentvm1 testdir]$ File="TestFile1" ; if [ -e $File ] ; then echo "The file $File exists." ; else echo "The file $File does not exist." ; fi
The file TestFile1 exists.
[student@studentvm1 testdir]$
現在,運行一個測試來判斷一個文件是否存在且長度不為 0(表示它包含數據)。假設你想判斷三種情況:
- 文件不存在;
- 文件存在且為空;
- 文件存在且包含數據。
因此,你需要一組更複雜的測試代碼 — 為了測試所有的情況,使用 if-elif-else
結構中的 elif
語句:
[student@studentvm1 testdir]$ File="TestFile1" ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
[student@studentvm1 testdir]$
在這個情況中,文件存在但不包含任何數據。向文件添加一些數據再運行一次:
[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; fi
TestFile1 exists and contains data.
[student@studentvm1 testdir]$
這組語句能返回正常的結果,但是僅僅是在我們已知三種可能的情況下測試某種確切的條件。添加一段 else
語句,這樣你就可以更精確地測試。把文件刪掉,你就可以完整地測試這段新代碼:
[student@studentvm1 testdir]$ File="TestFile1" ; rm $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 does not exist or is empty.
現在創建一個空文件用來測試:
[student@studentvm1 testdir]$ File="TestFile1" ; touch $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 does not exist or is empty.
向文件添加一些內容,然後再測試一次:
[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is file $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; else echo "$File does not exist or is empty." ; fi
TestFile1 exists and contains data.
現在加入 elif
語句來辨別是文件不存在還是文件為空:
[student@studentvm1 testdir]$ File="TestFile1" ; touch $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and is empty.
[student@studentvm1 testdir]$ File="TestFile1" ; echo "This is $File" > $File ; if [ -s $File ] ; then echo "$File exists and contains data." ; elif [ -e $File ] ; then echo "$File exists and is empty." ; else echo "$File does not exist." ; fi
TestFile1 exists and contains data.
[student@studentvm1 testdir]$
現在你有一個可以測試這三種情況的 Bash CLI 程序,但是可能的情況是無限的。
如果你能像保存在文件中的腳本那樣組織程序語句,那麼即使對於更複雜的命令組合也會很容易看出它們的邏輯結構。圖表 2 就是一個示例。 if-elif-else
結構中每一部分的程序語句的縮進讓邏輯更變得清晰。
File="TestFile1"
echo "This is $File" > $File
if [ -s $File ]
then
echo "$File exists and contains data."
elif [ -e $File ]
then
echo "$File exists and is empty."
else
echo "$File does not exist."
fi
圖表 2: 像在腳本里一樣重寫書寫命令行程序
對於大多數 CLI 程序來說,讓這些複雜的命令變得有邏輯需要寫很長的代碼。雖然 CLI 可能是用 Linux 或 Bash 內置的命令,但是當 CLI 程序很長或很複雜時,創建一個保存在文件中的腳本將更有效,保存到文件中後,可以隨時運行。
字元串比較操作符
字元串比較操作符使我們可以對字元串中的字元按字母順序進行比較。圖表 3 列出了僅有的幾個字元串比較操作符。
操作符 | 描述 |
---|---|
-z string |
如果字元串的長度為 0 ,返回真值 |
-n string |
如果字元串的長度不為 0 ,返回真值 |
string1 == string2 或 string1 = string2 |
如果兩個字元串相等,返回真值。處於遵從 POSIX 一致性,在測試命令中應使用一個等號 = 。與命令 [[ 一起使用時,會進行如上描述的模式匹配(混合命令)。 |
string1 != string2 |
兩個字元串不相等,返回真值 |
string1 < string2 |
如果對 string1 和 string2 按字母順序進行排序,string1 排在 string2 前面(即基於地區設定的對所有字母和特殊字元的排列順序) |
string1 > string2 |
如果對 string1 和 string2 按字母順序進行排序,string1 排在 string2 後面 |
圖表 3: Bash 字元串邏輯操作符
首先,檢查字元串長度。比較表達式中 $MyVar
兩邊的雙引號不能省略(你仍應該在目錄 ~/testdir
下 )。
[student@studentvm1 testdir]$ MyVar="" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
[student@studentvm1 testdir]$ MyVar="Random text" ; if [ -z "" ] ; then echo "MyVar is zero length." ; else echo "MyVar contains data" ; fi
MyVar is zero length.
你也可以這樣做:
[student@studentvm1 testdir]$ MyVar="Random text" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar contains data.
[student@studentvm1 testdir]$ MyVar="" ; if [ -n "$MyVar" ] ; then echo "MyVar contains data." ; else echo "MyVar is zero length" ; fi
MyVar is zero length
有時候你需要知道一個字元串確切的長度。這雖然不是比較,但是也與比較相關。不幸的是,計算字元串的長度沒有簡單的方法。有很多種方法可以計算,但是我認為使用 expr
(求值表達式)命令是相對最簡單的一種。閱讀 expr
的手冊頁可以了解更多相關知識。注意表達式中你檢測的字元串或變數兩邊的引號不要省略。
[student@studentvm1 testdir]$ MyVar="" ; expr length "$MyVar"
0
[student@studentvm1 testdir]$ MyVar="How long is this?" ; expr length "$MyVar"
17
[student@studentvm1 testdir]$ expr length "We can also find the length of a literal string as well as a variable."
70
關於比較操作符,在我們的腳本中使用了大量的檢測兩個字元串是否相等(例如,兩個字元串是否實際上是同一個字元串)的操作。我使用的是非 POSIX 版本的比較表達式:
[student@studentvm1 testdir]$ Var1="Hello World" ; Var2="Hello World" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 matches Var2
[student@studentvm1 testdir]$ Var1="Hello World" ; Var2="Hello world" ; if [ "$Var1" == "$Var2" ] ; then echo "Var1 matches Var2" ; else echo "Var1 and Var2 do not match." ; fi
Var1 and Var2 do not match.
在你自己的腳本中去試一下這些操作符。
數字比較操作符
數字操作符用於兩個數字參數之間的比較。像其他類操作符一樣,大部分都很容易理解。
操作符 | 描述 |
---|---|
arg1 -eq arg2 |
如果 arg1 等於 arg2 ,返回真值 |
arg1 -ne arg2 |
如果 arg1 不等於 arg2 ,返回真值 |
arg1 -lt arg2 |
如果 arg1 小於 arg2 ,返回真值 |
arg1 -le arg2 |
如果 arg1 小於或等於 arg2 ,返回真值 |
arg1 -gt arg2 |
如果 arg1 大於 arg2 ,返回真值 |
arg1 -ge arg2 |
如果 arg1 大於或等於 arg2 ,返回真值 |
圖表 4: Bash 數字比較邏輯操作符
來看幾個簡單的例子。第一個示例設置變數 $X
的值為 1,然後檢測 $X
是否等於 1。第二個示例中,$X
被設置為 0,所以比較表達式返回結果不為真值。
[student@studentvm1 testdir]$ X=1 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X equals 1
[student@studentvm1 testdir]$ X=0 ; if [ $X -eq 1 ] ; then echo "X equals 1" ; else echo "X does not equal 1" ; fi
X does not equal 1
[student@studentvm1 testdir]$
自己來多嘗試一下其他的。
雜項操作符
這些雜項操作符展示一個 shell 選項是否被設置,或一個 shell 變數是否有值,但是它不顯示變數的值,只顯示它是否有值。
操作符 | 描述 |
---|---|
-o optname |
如果一個 shell 選項 optname 是啟用的(查看內建在 Bash 手冊頁中的 set -o 選項描述下面的選項列表),則返回真值 |
-v varname |
如果 shell 變數 varname 被設置了值(被賦予了值),則返回真值 |
-R varname |
如果一個 shell 變數 varname 被設置了值且是一個名字引用,則返回真值 |
圖表 5: 雜項 Bash 邏輯操作符
自己來使用這些操作符實踐下。
擴展
Bash 支持非常有用的幾種類型的擴展和命令替換。根據 Bash 手冊頁,Bash 有七種擴展格式。本文只介紹其中五種:~
擴展、算術擴展、路徑名稱擴展、大括弧擴展和命令替換。
大括弧擴展
大括弧擴展是生成任意字元串的一種方法。(下面的例子是用特定模式的字元創建大量的文件。)大括弧擴展可以用於產生任意字元串的列表,並把它們插入一個用靜態字元串包圍的特定位置或靜態字元串的兩端。這可能不太好想像,所以還是來實踐一下。
首先,看一下大括弧擴展的作用:
[student@studentvm1 testdir]$ echo {string1,string2,string3}
string1 string2 string3
看起來不是很有用,對吧?但是用其他方式使用它,再來看看:
[student@studentvm1 testdir]$ echo "Hello "{David,Jen,Rikki,Jason}.
Hello David. Hello Jen. Hello Rikki. Hello Jason.
這看起來貌似有點用了 — 我們可以少打很多字。現在試一下這個:
[student@studentvm1 testdir]$ echo b{ed,olt,ar}s
beds bolts bars
我可以繼續舉例,但是你應該已經理解了它的用處。
~ 擴展
資料顯示,使用最多的擴展是波浪字元(~
)擴展。當你在命令中使用它(如 cd ~/Documents
)時,Bash shell 把這個快捷方式展開成用戶的完整的家目錄。
使用這個 Bash 程序觀察 ~
擴展的作用:
[student@studentvm1 testdir]$ echo ~
/home/student
[student@studentvm1 testdir]$ echo ~/Documents
/home/student/Documents
[student@studentvm1 testdir]$ Var1=~/Documents ; echo $Var1 ; cd $Var1
/home/student/Documents
[student@studentvm1 Documents]$
路徑名稱擴展
路徑名稱擴展是展開文件通配模式為匹配該模式的完整路徑名稱的另一種說法,匹配字元使用 ?
和 *
。文件通配指的是在大量操作中匹配文件名、路徑和其他字元串時用特定的模式字元產生極大的靈活性。這些特定的模式字元允許匹配字元串中的一個、多個或特定字元。
?
— 匹配字元串中特定位置的一個任意字元*
— 匹配字元串中特定位置的 0 個或多個任意字元
這個擴展用於匹配路徑名稱。為了弄清它的用法,請確保 testdir
是當前工作目錄(PWD
),先執行基本的列出清單命令 ls
(我家目錄下的內容跟你的不一樣)。
[student@studentvm1 testdir]$ ls
chapter6 cpuHog.dos dmesg1.txt Documents Music softlink1 testdir6 Videos
chapter7 cpuHog.Linux dmesg2.txt Downloads Pictures Templates testdir
testdir cpuHog.mac dmesg3.txt file005 Public testdir tmp
cpuHog Desktop dmesg.txt link3 random.txt testdir1 umask.test
[student@studentvm1 testdir]$
現在列出以 Do
、testdir/Documents
和 testdir/Downloads
開頭的目錄:
Documents:
Directory01 file07 file15 test02 test10 test20 testfile13 TextFiles
Directory02 file08 file16 test03 test11 testfile01 testfile14
file01 file09 file17 test04 test12 testfile04 testfile15
file02 file10 file18 test05 test13 testfile05 testfile16
file03 file11 file19 test06 test14 testfile09 testfile17
file04 file12 file20 test07 test15 testfile10 testfile18
file05 file13 Student1.txt test08 test16 testfile11 testfile19
file06 file14 test01 test09 test18 testfile12 testfile20
Downloads:
[student@studentvm1 testdir]$
然而,並沒有得到你期望的結果。它列出了以 Do
開頭的目錄下的內容。使用 -d
選項,僅列出目錄而不列出它們的內容。
[student@studentvm1 testdir]$ ls -d Do*
Documents Downloads
[student@studentvm1 testdir]$
在兩個例子中,Bash shell 都把 Do*
模式展開成了匹配該模式的目錄名稱。但是如果有文件也匹配這個模式,會發生什麼?
[student@studentvm1 testdir]$ touch Downtown ; ls -d Do*
Documents Downloads Downtown
[student@studentvm1 testdir]$
因此所有匹配這個模式的文件也被展開成了完整名字。
命令替換
命令替換是讓一個命令的標準輸出數據流被當做參數傳給另一個命令的擴展形式,例如,在一個循環中作為一系列被處理的項目。Bash 手冊頁顯示:「命令替換可以讓你用一個命令的輸出替換為命令的名字。」這可能不太好理解。
命令替換有兩種格式:command
和
$(command)。在更早的格式中使用反引號(
),在命令中使用反斜杠(
`)來保持它轉義之前的文本含義。然而,當用在新版本的括弧格式中時,反斜杠被當做一個特殊字元處理。也請注意帶括弧的格式打開個關閉命令語句都是用一個括弧。
我經常在命令行程序和腳本中使用這種能力,一個命令的結果能被用作另一個命令的參數。
來看一個非常簡單的示例,這個示例使用了這個擴展的兩種格式(再一次提醒,確保 testdir
是當前工作目錄):
[student@studentvm1 testdir]$ echo "Todays date is `date`"
Todays date is Sun Apr 7 14:42:46 EDT 2019
[student@studentvm1 testdir]$ echo "Todays date is $(date)"
Todays date is Sun Apr 7 14:42:59 EDT 2019
[student@studentvm1 testdir]$
-seq
工具用於一個數字序列:
[student@studentvm1 testdir]$ seq 5
1
2
3
4
5
[student@studentvm1 testdir]$ echo `seq 5`
1 2 3 4 5
[student@studentvm1 testdir]$
現在你可以做一些更有用處的操作,比如創建大量用於測試的空文件。
[student@studentvm1 testdir]$ for I in $(seq -w 5000) ; do touch file-$I ; done
seq
工具加上 -w
選項後,在生成的數字前面會用 0 補全,這樣所有的結果都等寬,例如,忽略數字的值,它們的位數一樣。這樣在對它們按數字順序進行排列時很容易。
seq -w 5000
語句生成了 1 到 5000 的數字序列。通過把命令替換用於 for
語句,for
語句就可以使用該數字序列來生成文件名的數字部分。
算術擴展
Bash 可以進行整型的數學計算,但是比較繁瑣(你一會兒將看到)。數字擴展的語法是 $((arithmetic-expression))
,分別用兩個括弧來打開和關閉表達式。算術擴展在 shell 程序或腳本中類似命令替換;表達式結算後的結果替換了表達式,用於 shell 後續的計算。
我們再用一個簡單的用法來開始:
[student@studentvm1 testdir]$ echo $((1+1))
2
[student@studentvm1 testdir]$ Var1=5 ; Var2=7 ; Var3=$((Var1*Var2)) ; echo "Var 3 = $Var3"
Var 3 = 35
下面的除法結果是 0,因為表達式的結果是一個小於 1 的整型數字:
[student@studentvm1 testdir]$ Var1=5 ; Var2=7 ; Var3=$((Var1/Var2)) ; echo "Var 3 = $Var3"
Var 3 = 0
這是一個我經常在腳本或 CLI 程序中使用的一個簡單的計算,用來查看在 Linux 主機中使用了多少虛擬內存。 free
不提供我需要的數據:
[student@studentvm1 testdir]$ RAM=`free | grep ^Mem | awk '{print $2}'` ; Swap=`free | grep ^Swap | awk '{print $2}'` ; echo "RAM = $RAM and Swap = $Swap" ; echo "Total Virtual memory is $((RAM+Swap))" ;
RAM = 4037080 and Swap = 6291452
Total Virtual memory is 10328532
我使用 ``` 字元來劃定用作命令替換的界限。
我用 Bash 算術擴展的場景主要是用腳本檢查系統資源用量後基於返回的結果選擇一個程序運行的路徑。
總結
本文是 Bash 編程語言系列的第二篇,探討了 Bash 中文件、字元串、數字和各種提供流程式控制制邏輯的邏輯操作符還有不同種類的 shell 擴展。
via: https://opensource.com/article/19/10/programming-bash-logical-operators-shell-expansions
作者:David Both 選題:lujun9972 譯者:lxbwolf 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive