# 第?31?章?Shell腳本
**目錄**
+ [1\. Shell的歷史](ch31s01.html)
+ [2\. Shell如何執行命令](ch31s02.html)
+ [2.1\. 執行交互式命令](ch31s02.html#id2872017)
+ [2.2\. 執行腳本](ch31s02.html#id2872211)
+ [3\. Shell的基本語法](ch31s03.html)
+ [3.1\. 變量](ch31s03.html#id2872666)
+ [3.2\. 文件名代換(Globbing):* ? []](ch31s03.html#id2872839)
+ [3.3\. 命令代換:`或 $()](ch31s03.html#id2872936)
+ [3.4\. 算術代換:$(())](ch31s03.html#id2872971)
+ [3.5\. 轉義字符\](ch31s03.html#id2873001)
+ [3.6\. 單引號](ch31s03.html#id2873083)
+ [3.7\. 雙引號](ch31s03.html#id2873112)
+ [4\. bash啟動腳本](ch31s04.html)
+ [4.1\. 作為交互登錄Shell啟動,或者使用--login參數啟動](ch31s04.html#id2873231)
+ [4.2\. 以交互非登錄Shell啟動](ch31s04.html#id2873387)
+ [4.3\. 非交互啟動](ch31s04.html#id2873571)
+ [4.4\. 以sh命令啟動](ch31s04.html#id2873616)
+ [5\. Shell腳本語法](ch31s05.html)
+ [5.1\. 條件測試:test [](ch31s05.html#id2873722)
+ [5.2\. if/then/elif/else/fi](ch31s05.html#id2874121)
+ [5.3\. case/esac](ch31s05.html#id2874366)
+ [5.4\. for/do/done](ch31s05.html#id2874526)
+ [5.5\. while/do/done](ch31s05.html#id2874637)
+ [5.6\. 位置參數和特殊變量](ch31s05.html#id2874685)
+ [5.7\. 函數](ch31s05.html#id2874943)
+ [6\. Shell腳本的調試方法](ch31s06.html)
## 1.?Shell的歷史
Shell的作用是解釋執行用戶的命令,用戶輸入一條命令,Shell就解釋執行一條,這種方式稱為交互式(Interactive),Shell還有一種執行命令的方式稱為批處理(Batch),用戶事先寫一個Shell腳本(Script),其中有很多條命令,讓Shell一次把這些命令執行完,而不必一條一條地敲命令。Shell腳本和編程語言很相似,也有變量和流程控制語句,但Shell腳本是解釋執行的,不需要編譯,Shell程序從腳本中一行一行讀取并執行這些命令,相當于一個用戶把腳本中的命令一行一行敲到Shell提示符下執行。
由于歷史原因,UNIX系統上有很多種Shell:
1. `sh`(Bourne Shell):由Steve Bourne開發,各種UNIX系統都配有`sh`。
2. `csh`(C Shell):由Bill Joy開發,隨BSD UNIX發布,它的流程控制語句很像C語言,支持很多Bourne Shell所不支持的功能:作業控制,命令歷史,命令行編輯。
3. `ksh`(Korn Shell):由David Korn開發,向后兼容`sh`的功能,并且添加了`csh`引入的新功能,是目前很多UNIX系統標準配置的Shell,在這些系統上`/bin/sh`往往是指向`/bin/ksh`的符號鏈接。
4. `tcsh`(TENEX C Shell):是`csh`的增強版本,引入了命令補全等功能,在FreeBSD、Mac OS X等系統上替代了`csh`。
5. `bash`(Bourne Again Shell):由GNU開發的Shell,主要目標是與POSIX標準保持一致,同時兼顧對`sh`的兼容,`bash`從`csh`和`ksh`借鑒了很多功能,是各種Linux發行版標準配置的Shell,在Linux系統上`/bin/sh`往往是指向`/bin/bash`的符號鏈接<sup>[[38](#ftn.id2871814)]</sup>。雖然如此,`bash`和`sh`還是有很多不同的,一方面,`bash`擴展了一些命令和參數,另一方面,`bash`并不完全和`sh`兼容,有些行為并不一致,所以`bash`需要模擬`sh`的行為:當我們通過`sh`這個程序名啟動`bash`時,`bash`可以假裝自己是`sh`,不認擴展的命令,并且行為與`sh`保持一致。
文件`/etc/shells`給出了系統中所有已知(不一定已安裝)的Shell,除了上面提到的Shell之外還有很多變種。
```
# /etc/shells: valid login shells
/bin/csh
/bin/sh
/usr/bin/es
/usr/bin/ksh
/bin/ksh
/usr/bin/rc
/usr/bin/tcsh
/bin/tcsh
/usr/bin/esh
/bin/dash
/bin/bash
/bin/rbash
/usr/bin/screen
```
用戶的默認Shell設置在`/etc/passwd`文件中,例如下面這行對用戶mia的設置:
```
mia:L2NOfqdlPrHwE:504:504:Mia Maya:/home/mia:/bin/bash
```
用戶mia從字符終端登錄或者打開圖形終端窗口時就會自動執行`/bin/bash`。如果要切換到其它Shell,可以在命令行輸入程序名,例如:
```
~$ sh(在bash提示符下輸入sh命令)
$(出現sh的提示符)
$(按Ctrl-d或者輸入exit命令)
~$(回到bash提示符)
~$(再次按Ctrl-d或者輸入exit命令會退出登錄或者關閉圖形終端窗口)
```
本章只介紹`bash`和`sh`的用法和相關語法,不介紹其它Shell。所以下文提到Shell都是指`bash`或`sh`。
* * *
<sup>[[38](#id2871814)]</sup> 最新的發行版有一些變化,例如Ubuntu 7.10的`/bin/sh`是指向`/bin/dash`的符號鏈接,`dash`也是一種類似`bash`的Shell。
```
$ ls /bin/sh /bin/dash -l
-rwxr-xr-x 1 root root 79988 2008-03-12 19:22 /bin/dash
lrwxrwxrwx 1 root root 4 2008-07-04 05:58 /bin/sh -> dash
```
## 2.?Shell如何執行命令
### 2.1.?執行交互式命令
用戶在命令行輸入命令后,一般情況下Shell會`fork`并`exec`該命令,但是Shell的內建命令例外,執行內建命令相當于調用Shell進程中的一個函數,并不創建新的進程。以前學過的`cd`、`alias`、`umask`、`exit`等命令即是內建命令,凡是用`which`命令查不到程序文件所在位置的命令都是內建命令,內建命令沒有單獨的man手冊,要在man手冊中查看內建命令,應該
```
$ man bash-builtins
```
本節會介紹很多內建命令,如`export`、`shift`、`if`、`eval`、`[`、`for`、`while`等等。內建命令雖然不創建新的進程,但也會有Exit Status,通常也用0表示成功非零表示失敗,雖然內建命令不創建新的進程,但執行結束后也會有一個狀態碼,也可以用特殊變量`$?`讀出。
#### 習題
1、在完成[第?5?節 “練習:實現簡單的Shell”](ch30s05.html#process.implementshell)時也許有的讀者已經試過了,在自己實現的Shell中不能執行`cd`命令,因為`cd`是一個內建命令,沒有程序文件,不能用`exec`執行。現在請完善該程序,實現`cd`命令的功能,用`chdir(2)`函數可以改變進程的當前工作目錄。
2、思考一下,為什么`cd`命令要實現成內建命令?可不可以實現一個獨立的`cd`程序,例如`/bin/cd`,就像`/bin/ls`一樣?
### 2.2.?執行腳本
首先編寫一個簡單的腳本,保存為`script.sh`:
**例?31.1.?簡單的Shell腳本**
```
#! /bin/sh
cd ..
ls
```
Shell腳本中用`#`表示注釋,相當于C語言的`//`注釋。但如果`#`位于第一行開頭,并且是`#!`(稱為Shebang)則例外,它表示該腳本使用后面指定的解釋器`/bin/sh`解釋執行。如果把這個腳本文件加上可執行權限然后執行:
```
$ chmod +x script.sh
$ ./script.sh
```
Shell會`fork`一個子進程并調用`exec`執行`./script.sh`這個程序,`exec`系統調用應該把子進程的代碼段替換成`./script.sh`程序的代碼段,并從它的`_start`開始執行。然而`script.sh`是個文本文件,根本沒有代碼段和`_start`函數,怎么辦呢?其實`exec`還有另外一種機制,如果要執行的是一個文本文件,并且第一行用Shebang指定了解釋器,則用解釋器程序的代碼段替換當前進程,并且從解釋器的`_start`開始執行,而這個文本文件被當作命令行參數傳給解釋器。因此,執行上述腳本相當于執行程序
```
$ /bin/sh ./script.sh
```
以這種方式執行不需要`script.sh`文件具有可執行權限。再舉個例子,比如某個`sed`腳本的文件名是`script`,它的開頭是
```
#! /bin/sed -f
```
執行`./script`相當于執行程序
```
$ /bin/sed -f ./script.sh
```
以上介紹了兩種執行Shell腳本的方法:
```
$ ./script.sh
$ sh ./script.sh
```
這兩種方法本質上是一樣的,執行上述腳本的步驟為:
**圖?31.1.?Shell腳本的執行過程**

1. 交互Shell(`bash`)`fork`/`exec`一個子Shell(`sh`)用于執行腳本,父進程`bash`等待子進程`sh`終止。
2. `sh`讀取腳本中的`cd ..`命令,調用相應的函數執行內建命令,改變當前工作目錄為上一級目錄。
3. `sh`讀取腳本中的`ls`命令,`fork`/`exec`這個程序,列出當前工作目錄下的文件,`sh`等待`ls`終止。
4. `ls`終止后,`sh`繼續執行,讀到腳本文件末尾,`sh`終止。
5. `sh`終止后,`bash`繼續執行,打印提示符等待用戶輸入。
如果將命令行下輸入的命令用()括號括起來,那么也會`fork`出一個子Shell執行小括號中的命令,一行中可以輸入由分號;隔開的多個命令,比如:
```
$ (cd ..;ls -l)
```
和上面兩種方法執行Shell腳本的效果是相同的,`cd ..`命令改變的是子Shell的`PWD`,而不會影響到交互式Shell。然而命令
```
$ cd ..;ls -l
```
則有不同的效果,`cd ..`命令是直接在交互式Shell下執行的,改變交互式Shell的`PWD`,然而這種方式相當于這樣執行Shell腳本:
```
$ source ./script.sh
```
或者
```
$ . ./script.sh
```
`source`或者`.`命令是Shell的內建命令,這種方式也不會創建子Shell,而是直接在交互式Shell下逐行執行腳本中的命令。
#### 習題
1、解釋如下命令的執行過程:
```
$ (exit 2)
$ echo $?
2
```
## 3.?Shell的基本語法
### 3.1.?變量
按照慣例,Shell變量由全大寫字母加下劃線組成,有兩種類型的Shell變量:
環境變量
在[第?2?節 “環境變量”](ch30s02.html#process.environ)中講過,環境變量可以從父進程傳給子進程,因此Shell進程的環境變量可以從當前Shell進程傳給`fork`出來的子進程。用`printenv`命令可以顯示當前Shell進程的環境變量。
本地變量
只存在于當前Shell進程,用`set`命令可以顯示當前Shell進程中定義的所有變量(包括本地變量和環境變量)和函數。
環境變量是任何進程都有的概念,而本地變量是Shell特有的概念。在Shell中,環境變量和本地變量的定義和用法相似。在Shell中定義或賦值一個變量:
```
$ VARNAME=value
```
注意等號兩邊都不能有空格,否則會被Shell解釋成命令和命令行參數。
一個變量定義后僅存在于當前Shell進程,它是本地變量,用`export`命令可以把本地變量導出為環境變量,定義和導出環境變量通常可以一步完成:
```
$ export VARNAME=value
```
也可以分兩步完成:
```
$ VARNAME=value
$ export VARNAME
```
用`unset`命令可以刪除已定義的環境變量或本地變量。
```
$ unset VARNAME
```
如果一個變量叫做`VARNAME`,用`${VARNAME}`可以表示它的值,在不引起歧義的情況下也可以用`$VARNAME`表示它的值。通過以下例子比較這兩種表示法的不同:
```
$ echo $SHELL
$ echo $SHELLabc
$ echo $SHELL abc
$ echo ${SHELL}abc
```
注意,在定義變量時不用$,取變量值時要用$。和C語言不同的是,Shell變量不需要明確定義類型,事實上Shell變量的值都是字符串,比如我們定義`VAR=45`,其實`VAR`的值是字符串`45`而非整數。Shell變量不需要先定義后使用,如果對一個沒有定義的變量取值,則值為空字符串。
### 3.2.?文件名代換(Globbing):* ? []
這些用于匹配的字符稱為通配符(Wildcard),具體如下:
**表?31.1.?通配符**
| | |
| --- | --- |
| \* | 匹配0個或多個任意字符 |
| ? | 匹配一個任意字符 |
| [若干字符] | 匹配方括號中任意一個字符的一次出現 |
```
$ ls /dev/ttyS*
$ ls ch0?.doc
$ ls ch0[0-2].doc
$ ls ch[012][0-9].doc
```
注意,Globbing所匹配的文件名是由Shell展開的,也就是說在參數還沒傳給程序之前已經展開了,比如上述`ls ch0[012].doc`命令,如果當前目錄下有`ch00.doc`和`ch02.doc`,則傳給`ls`命令的參數實際上是這兩個文件名,而不是一個匹配字符串。
### 3.3.?命令代換:`或 $()
由反引號括起來的也是一條命令,Shell先執行該命令,然后將輸出結果立刻代換到當前命令行中。例如定義一個變量存放`date`命令的輸出:
```
$ DATE=`date`
$ echo $DATE
```
命令代換也可以用`$()`表示:
```
$ DATE=$(date)
```
### 3.4.?算術代換:$(())
用于算術計算,`$(())`中的Shell變量取值將轉換成整數,例如:
```
$ VAR=45
$ echo $(($VAR+3))
```
`$(())`中只能用+-*/和()運算符,并且只能做整數運算。
### 3.5.?轉義字符\
和C語言類似,\在Shell中被用作轉義字符,用于去除緊跟其后的單個字符的特殊意義(回車除外),換句話說,緊跟其后的字符取字面值。例如:
```
$ echo $SHELL
/bin/bash
$ echo \$SHELL
$SHELL
$ echo \\
\
```
比如創建一個文件名為“$ $”的文件可以這樣:
```
$ touch \$\ \$
```
還有一個字符雖然不具有特殊含義,但是要用它做文件名也很麻煩,就是-號。如果要創建一個文件名以-號開頭的文件,這樣是不行的:
```
$ touch -hello
touch: invalid option -- h
Try `touch --help' for more information.
```
即使加上\轉義也還是報錯:
```
$ touch \-hello
touch: invalid option -- h
Try `touch --help' for more information.
```
因為各種UNIX命令都把-號開頭的命令行參數當作命令的選項,而不會當作文件名。如果非要處理以-號開頭的文件名,可以有兩種辦法:
```
$ touch ./-hello
```
或者
```
$ touch -- -hello
```
\還有一種用法,在\后敲回車表示續行,Shell并不會立刻執行命令,而是把光標移到下一行,給出一個續行提示符>,等待用戶繼續輸入,最后把所有的續行接到一起當作一個命令執行。例如:
```
$ ls \
> -l
(ls -l命令的輸出)
```
### 3.6.?單引號
和C語言不一樣,Shell腳本中的單引號和雙引號一樣都是字符串的界定符(雙引號下一節介紹),而不是字符的界定符。單引號用于保持引號內所有字符的字面值,即使引號內的\和回車也不例外,但是字符串中不能出現單引號。如果引號沒有配對就輸入回車,Shell會給出續行提示符,要求用戶把引號配上對。例如:
```
$ echo '$SHELL'
$SHELL
$ echo 'ABC\(回車)
> DE'(再按一次回車結束命令)
ABC\
DE
```
### 3.7.?雙引號
雙引號用于保持引號內所有字符的字面值(回車也不例外),但以下情況除外:
* $加變量名可以取變量的值
* 反引號仍表示命令替換
* \$表示$的字面值
* \`表示`的字面值
* \"表示"的字面值
* \\表示\的字面值
* 除以上情況之外,在其它字符前面的\無特殊含義,只表示字面值
```
$ echo "$SHELL"
/bin/bash
$ echo "`date`"
Sun Apr 20 11:22:06 CEST 2003
$ echo "I'd say: \"Go for it\""
I'd say: "Go for it"
$ echo "\"(回車)
>"(再按一次回車結束命令)
"
$ echo "\\"
\
```
## 4.?bash啟動腳本
啟動腳本是`bash`啟動時自動執行的腳本。用戶可以把一些環境變量的設置和`alias`、`umask`設置放在啟動腳本中,這樣每次啟動Shell時這些設置都自動生效。思考一下,`bash`在執行啟動腳本時是以`fork`子Shell方式執行的還是以`source`方式執行的?
啟動bash的方法不同,執行啟動腳本的步驟也不相同,具體可分為以下幾種情況。
### 4.1.?作為交互登錄Shell啟動,或者使用--login參數啟動
交互Shell是指用戶在提示符下輸命令的Shell而非執行腳本的Shell,登錄Shell就是在輸入用戶名和密碼登錄后得到的Shell,比如從字符終端登錄或者用`telnet`/`ssh`從遠程登錄,但是從圖形界面的窗口管理器登錄之后會顯示桌面而不會產生登錄Shell(也不會執行啟動腳本),在圖形界面下打開終端窗口得到的Shell也不是登錄Shell。
這樣啟動`bash`會自動執行以下腳本:
1. 首先執行`/etc/profile`,系統中每個用戶登錄時都要執行這個腳本,如果系統管理員希望某個設置對所有用戶都生效,可以寫在這個腳本里
2. 然后依次查找當前用戶主目錄的`~/.bash_profile`、`~/.bash_login`和`~/.profile`三個文件,找到第一個存在并且可讀的文件來執行,如果希望某個設置只對當前用戶生效,可以寫在這個腳本里,由于這個腳本在`/etc/profile`之后執行,`/etc/profile`設置的一些環境變量的值在這個腳本中可以修改,也就是說,當前用戶的設置可以覆蓋(Override)系統中全局的設置。`~/.profile`這個啟動腳本是`sh`規定的,`bash`規定首先查找以`~/.bash_`開頭的啟動腳本,如果沒有則執行`~/.profile`,是為了和`sh`保持一致。
3. 順便一提,在退出登錄時會執行`~/.bash_logout`腳本(如果它存在的話)。
### 4.2.?以交互非登錄Shell啟動
比如在圖形界面下開一個終端窗口,或者在登錄Shell提示符下再輸入`bash`命令,就得到一個交互非登錄的Shell,這種Shell在啟動時自動執行`~/.bashrc`腳本。
為了使登錄Shell也能自動執行`~/.bashrc`,通常在`~/.bash_profile`中調用`~/.bashrc`:
```
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
```
這幾行的意思是如果`~/.bashrc`文件存在則`source`它。多數Linux發行版在創建帳戶時會自動創建`~/.bash_profile`和`~/.bashrc`腳本,`~/.bash_profile`中通常都有上面這幾行。所以,如果要在啟動腳本中做某些設置,使它在圖形終端窗口和字符終端的Shell中都起作用,最好就是在`~/.bashrc`中設置。
下面做一個實驗,在`~/.bashrc`文件末尾添加一行(如果這個文件不存在就創建它):
```
export PATH=$PATH:/home/akaedu
```
然后關掉終端窗口重新打開,或者從字符終端`logout`之后重新登錄,現在主目錄下的程序應該可以直接輸程序名運行而不必輸入路徑了,例如:
```
~$ a.out
```
就可以了,而不必
```
~$ ./a.out
```
為什么登錄Shell和非登錄Shell的啟動腳本要區分開呢?最初的設計是這樣考慮的,如果從字符終端或者遠程登錄,那么登錄Shell是該用戶的所有其它進程的父進程,也是其它子Shell的父進程,所以環境變量在登錄Shell的啟動腳本里設置一次就可以自動帶到其它非登錄Shell里,而Shell的本地變量、函數、`alias`等設置沒有辦法帶到子Shell里,需要每次啟動非登錄Shell時設置一遍,所以就需要有非登錄Shell的啟動腳本,所以一般來說在`~/.bash_profile`里設置環境變量,在`~/.bashrc`里設置本地變量、函數、`alias`等。如果你的Linux帶有圖形系統則不能這樣設置,由于從圖形界面的窗口管理器登錄并不會產生登錄Shell,所以環境變量也應該在`~/.bashrc`里設置。
### 4.3.?非交互啟動
為執行腳本而`fork`出來的子Shell是非交互Shell,啟動時執行的腳本文件由環境變量`BASH_ENV`定義,相當于自動執行以下命令:
```
if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
```
如果環境變量`BASH_ENV`的值不是空字符串,則把它的值當作啟動腳本的文件名,`source`這個腳本。
### 4.4.?以sh命令啟動
如果以`sh`命令啟動`bash`,`bash`將模擬`sh`的行為,以`~/.bash_`開頭的那些啟動腳本就不認了。所以,如果作為交互登錄Shell啟動,或者使用--login參數啟動,則依次執行以下腳本:
1. `/etc/profile`
2. `~/.profile`
如果作為交互Shell啟動,相當于自動執行以下命令:
```
if [ -n "$ENV" ]; then . "$ENV"; fi
```
如果作為非交互Shell啟動,則不執行任何啟動腳本。通常我們寫的Shell腳本都以`#! /bin/sh`開頭,都屬于這種方式。
## 5.?Shell腳本語法
### 5.1.?條件測試:test [
命令`test`或`[`可以測試一個條件是否成立,如果測試結果為真,則該命令的Exit Status為0,如果測試結果為假,則命令的Exit Status為1(注意與C語言的邏輯表示正好相反)。例如測試兩個數的大小關系:
```
$ VAR=2
$ test $VAR -gt 1
$ echo $?
0
$ test $VAR -gt 3
$ echo $?
1
$ [ $VAR -gt 3 ]
$ echo $?
1
```
_雖然看起來很奇怪,但左方括號`[`確實是一個命令的名字,傳給命令的各參數之間應該用空格隔開_,比如,`$VAR`、`-gt`、`3`、`]`是`[`命令的四個參數,它們之間必須用空格隔開。命令`test`或`[`的參數形式是相同的,只不過`test`命令不需要`]`參數。以`[`命令為例,常見的測試命令如下表所示:
**表?31.2.?測試命令**
| | |
| --- | --- |
| `[ -d DIR ]` | 如果`DIR`存在并且是一個目錄則為真 |
| `[ -f FILE ]` | 如果`FILE`存在且是一個普通文件則為真 |
| `[ -z STRING ]` | 如果`STRING`的長度為零則為真 |
| `[ -n STRING ]` | 如果`STRING`的長度非零則為真 |
| `[ STRING1 = STRING2 ]` | 如果兩個字符串相同則為真 |
| `[ STRING1 != STRING2 ]` | 如果字符串不相同則為真 |
| `[ ARG1 OP ARG2 ]` | `ARG1`和`ARG2`應該是整數或者取值為整數的變量,`OP`是`-eq`(等于)`-ne`(不等于)`-lt`(小于)`-le`(小于等于)`-gt`(大于)`-ge`(大于等于)之中的一個 |
和C語言類似,測試條件之間還可以做與、或、非邏輯運算:
**表?31.3.?帶與、或、非的測試命令**
| | |
| --- | --- |
| `[ ! EXPR ]` | `EXPR`可以是上表中的任意一種測試條件,!表示邏輯反 |
| `[ EXPR1 -a EXPR2 ]` | `EXPR1`和`EXPR2`可以是上表中的任意一種測試條件,`-a`表示邏輯與 |
| `[ EXPR1 -o EXPR2 ]` | `EXPR1`和`EXPR2`可以是上表中的任意一種測試條件,`-o`表示邏輯或 |
例如:
```
$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ]
$ echo $?
0
```
注意,如果上例中的`$VAR`變量事先沒有定義,則被Shell展開為空字符串,會造成測試條件的語法錯誤(展開為`[ -d Desktop -a = 'abc' ]`),作為一種好的Shell編程習慣,應該總是把變量取值放在雙引號之中(展開為`[ -d Desktop -a "" = 'abc' ]`):
```
$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ]
bash: [: too many arguments
$ [ -d Desktop -a "$VAR" = 'abc' ]
$ echo $?
1
```
### 5.2.?if/then/elif/else/fi
和C語言類似,在Shell中用`if`、`then`、`elif`、`else`、`fi`這幾條命令實現分支控制。這種流程控制語句本質上也是由若干條Shell命令組成的,例如先前講過的
```
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
```
其實是三條命令,`if [ -f ~/.bashrc ]`是第一條,`then . ~/.bashrc`是第二條,`fi`是第三條。如果兩條命令寫在同一行則需要用;號隔開,一行只寫一條命令就不需要寫;號了,另外,`then`后面有換行,但這條命令沒寫完,Shell會自動續行,把下一行接在`then`后面當作一條命令處理。和`[`命令一樣,要注意命令和各參數之間必須用空格隔開。`if`命令的參數組成一條子命令,如果該子命令的Exit Status為0(表示真),則執行`then`后面的子命令,如果Exit Status非0(表示假),則執行`elif`、`else`或者`fi`后面的子命令。`if`后面的子命令通常是測試命令,但也可以是其它命令。Shell腳本沒有{}括號,所以用`fi`表示`if`語句塊的結束。見下例:
```
#! /bin/sh
if [ -f /bin/bash ]
then echo "/bin/bash is a file"
else echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi
```
`:`是一個特殊的命令,稱為空命令,該命令不做任何事,但Exit Status總是真。此外,也可以執行`/bin/true`或`/bin/false`得到真或假的Exit Status。再看一個例子:
```
#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
if [ "$YES_OR_NO" = "yes" ]; then
echo "Good morning!"
elif [ "$YES_OR_NO" = "no" ]; then
echo "Good afternoon!"
else
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1
fi
exit 0
```
上例中的`read`命令的作用是等待用戶輸入一行字符串,將該字符串存到一個Shell變量中。
此外,Shell還提供了&&和||語法,和C語言類似,具有Short-circuit特性,很多Shell腳本喜歡寫成這樣:
```
test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)
```
&&相當于“if...then...”,而||相當于“if not...then...”。&&和||用于連接兩個命令,而上面講的`-a`和`-o`僅用于在測試表達式中連接兩個測試條件,要注意它們的區別,例如,
```
test "$VAR" -gt 1 -a "$VAR" -lt 3
```
和以下寫法是等價的
```
test "$VAR" -gt 1 && test "$VAR" -lt 3
```
### 5.3.?case/esac
`case`命令可類比C語言的`switch`/`case`語句,`esac`表示`case`語句塊的結束。C語言的`case`只能匹配整型或字符型常量表達式,而Shell腳本的`case`可以匹配字符串和Wildcard,每個匹配分支可以有若干條命令,末尾必須以;;結束,執行時找到第一個匹配的分支并執行相應的命令,然后直接跳到`esac`之后,不需要像C語言一樣用`break`跳出。
```
#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
echo "Good Morning!";;
[nN]*)
echo "Good Afternoon!";;
*)
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1;;
esac
exit 0
```
使用`case`語句的例子可以在系統服務的腳本目錄`/etc/init.d`中找到。這個目錄下的腳本大多具有這種形式(以`/etc/apache2`為例):
```
case $1 in
start)
...
;;
stop)
...
;;
reload | force-reload)
...
;;
restart)
...
*)
log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}"
exit 1
;;
esac
```
啟動`apache2`服務的命令是
```
$ sudo /etc/init.d/apache2 start
```
`$1`是一個特殊變量,在執行腳本時自動取值為第一個命令行參數,也就是`start`,所以進入`start)`分支執行相關的命令。同理,命令行參數指定為`stop`、`reload`或`restart`可以進入其它分支執行停止服務、重新加載配置文件或重新啟動服務的相關命令。
### 5.4.?for/do/done
Shell腳本的`for`循環結構和C語言很不一樣,它類似于某些編程語言的`foreach`循環。例如:
```
#! /bin/sh
for FRUIT in apple banana pear; do
echo "I like $FRUIT"
done
```
`FRUIT`是一個循環變量,第一次循環`$FRUIT`的取值是`apple`,第二次取值是`banana`,第三次取值是`pear`。再比如,要將當前目錄下的`chap0`、`chap1`、`chap2`等文件名改為`chap0~`、`chap1~`、`chap2~`等(按慣例,末尾有~字符的文件名表示臨時文件),這個命令可以這樣寫:
```
$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done
```
也可以這樣寫:
```
$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done
```
### 5.5.?while/do/done
`while`的用法和C語言類似。比如一個驗證密碼的腳本:
```
#! /bin/sh
echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
echo "Sorry, try again"
read TRY
done
```
下面的例子通過算術運算控制循環的次數:
```
#! /bin/sh
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+1))
done
```
Shell還有until循環,類似C語言的do...while循環。本章從略。
#### 習題
1、把上面驗證密碼的程序修改一下,如果用戶輸錯五次密碼就報錯退出。
### 5.6.?位置參數和特殊變量
有很多特殊變量是被Shell自動賦值的,我們已經遇到了`$?`和`$1`,現在總結一下:
**表?31.4.?常用的位置參數和特殊變量**
| | |
| --- | --- |
| `$0` | 相當于C語言`main`函數的`argv[0]` |
| `$1`、`$2`... | 這些稱為位置參數(Positional Parameter),相當于C語言`main`函數的`argv[1]`、`argv[2]`... |
| `$#` | 相當于C語言`main`函數的`argc - 1`,注意這里的`#`后面不表示注釋 |
| `$@` | 表示參數列表`"$1" "$2" ...`,例如可以用在`for`循環中的`in`后面。 |
| `$?` | 上一條命令的Exit Status |
| `$$` | 當前Shell的進程號 |
位置參數可以用`shift`命令左移。比如`shift 3`表示原來的`$4`現在變成`$1`,原來的`$5`現在變成`$2`等等,原來的`$1`、`$2`、`$3`丟棄,`$0`不移動。不帶參數的`shift`命令相當于`shift 1`。例如:
```
#! /bin/sh
echo "The program $0 is now running"
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
shift
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
```
### 5.7.?函數
和C語言類似,Shell中也有函數的概念,但是函數定義中沒有返回值也沒有參數列表。例如:
```
#! /bin/sh
foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"
```
注意函數體的左花括號{和后面的命令之間必須有空格或換行,如果將最后一條命令和右花括號`}`寫在同一行,命令末尾必須有;號。
在定義`foo()`函數時并不執行函數體中的命令,就像定義變量一樣,只是給`foo`這個名字一個定義,到后面調用`foo`函數的時候(注意Shell中的函數調用不寫括號)才執行函數體中的命令。Shell腳本中的函數必須先定義后調用,一般把函數定義都寫在腳本的前面,把函數調用和其它命令寫在腳本的最后(類似C語言中的`main`函數,這才是整個腳本實際開始執行命令的地方)。
Shell函數沒有參數列表并不表示不能傳參數,事實上,函數就像是迷你腳本,調用函數時可以傳任意個參數,在函數內同樣是用`$0`、`$1`、`$2`等變量來提取參數,函數中的位置參數相當于函數的局部變量,改變這些變量并不會影響函數外面的`$0`、`$1`、`$2`等變量。函數中可以用`return`命令返回,如果`return`后面跟一個數字則表示函數的Exit Status。
下面這個腳本可以一次創建多個目錄,各目錄名通過命令行參數傳入,腳本逐個測試各目錄是否存在,如果目錄不存在,首先打印信息然后試著創建該目錄。
```
#! /bin/sh
is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
return 1
else
return 0
fi
}
for DIR in "$@"; do
if is_directory "$DIR"
then :
else
echo "$DIR doesn't exist. Creating it now..."
mkdir $DIR > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Cannot create directory $DIR"
exit 1
fi
fi
done
```
注意`is_directory()`返回0表示真返回1表示假。
## 6.?Shell腳本的調試方法
Shell提供了一些用于調試腳本的選項,如下所示:
-n
讀一遍腳本中的命令但不執行,用于檢查腳本中的語法錯誤
-v
一邊執行腳本,一邊將執行過的腳本命令打印到標準錯誤輸出
-x
提供跟蹤執行信息,將執行的每一條命令和結果依次打印出來
使用這些選項有三種方法,一是在命令行提供參數
```
$ sh -x ./script.sh
```
二是在腳本開頭提供參數
```
#! /bin/sh -x
```
第三種方法是在腳本中用set命令啟用或禁用參數
```
#! /bin/sh
if [ -z "$1" ]; then
set -x
echo "ERROR: Insufficient Args."
exit 1
set +x
fi
```
`set -x`和`set +x`分別表示啟用和禁用`-x`參數,這樣可以只對腳本中的某一段進行跟蹤調試。
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引