所有的計算機程序都是用來和數據打交道的。在過去的章節中,我們專注于處理文件級別的數據。 然而,許多程序問題需要使用更小的數據單位來解決,比方說字符串和數字。
在這一章中,我們將查看幾個用來操作字符串和數字的 shell 功能。shell 提供了各種執行字符串操作的參數展開功能。 除了算術展開(在第七章中接觸過),還有一個常見的命令行程序叫做 bc,能執行更高級別的數學運算。
## 參數展開
盡管參數展開在第七章中出現過,但我們并沒有詳盡地介紹它,因為大多數的參數展開會用在腳本中,而不是命令行中。 我們已經使用了一些形式的參數展開;例如,shell 變量。shell 提供了更多方式。
### 基本參數
最簡單的參數展開形式反映在平常使用的變量上。
例如:
**$a**
當 $a 展開后,會變成變量 a 所包含的值。簡單參數也可能用花括號引起來:
**${a}**
雖然這對展開沒有影響,但若該變量 a 與其它的文本相鄰,可能會把 shell 搞糊涂了。在這個例子中,我們試圖 創建一個文件名,通過把字符串 “_file” 附加到變量 a 的值的后面。
~~~
[me@linuxbox ~]$ a="foo"
[me@linuxbox ~]$ echo "$a_file"
~~~
如果我們執行這個序列,沒有任何輸出結果,因為 shell 會試著展開一個稱為 a_file 的變量,而不是 a。通過 添加花括號可以解決這個問題:
~~~
[me@linuxbox ~]$ echo "${a}_file"
foo_file
~~~
我們已經知道通過把數字包裹在花括號中,可以訪問大于9的位置參數。例如,訪問第十一個位置參數,我們可以這樣做:
**${11}**
### 管理空變量的展開
幾種用來處理不存在和空變量的參數展開形式。這些展開形式對于解決丟失的位置參數和給參數指定默認值的情況很方便。
**${parameter:-word}**
若 parameter 沒有設置(例如,不存在)或者為空,展開結果是 word 的值。若 parameter 不為空,則展開結果是 parameter 的值。
~~~
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
if unset
substitute value
[me@linuxbox ~]$ echo $foo
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:-"substitute value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar
~~~
**${parameter:=word}**
若 parameter 沒有設置或為空,展開結果是 word 的值。另外,word 的值會賦值給 parameter。 若 parameter 不為空,展開結果是 parameter 的值。
~~~
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
default value if unset
[me@linuxbox ~]$ echo $foo
default value if unset
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:="default value if unset"}
bar
[me@linuxbox ~]$ echo $foo
bar
~~~
* * *
注意: 位置參數或其它的特殊參數不能以這種方式賦值。
* * *
**${parameter:?word}**
若 parameter 沒有設置或為空,這種展開導致腳本帶有錯誤退出,并且 word 的內容會發送到標準錯誤。若 parameter 不為空, 展開結果是 parameter 的值。
~~~
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bash: foo: parameter is empty
[me@linuxbox ~]$ echo $?
1
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:?"parameter is empty"}
bar
[me@linuxbox ~]$ echo $?
0
~~~
**${parameter:+word}**
若 parameter 沒有設置或為空,展開結果為空。若 parameter 不為空, 展開結果是 word 的值會替換掉 parameter 的值;然而,parameter 的值不會改變。
~~~
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}
[me@linuxbox ~]$ foo=bar
[me@linuxbox ~]$ echo ${foo:+"substitute value if set"}
substitute value if set
~~~
## 返回變量名的參數展開
shell 具有返回變量名的能力。這會用在一些相當獨特的情況下。
~~~
${!prefix*}
${!prefix@}
~~~
這種展開會返回以 prefix 開頭的已有變量名。根據 bash 文檔,這兩種展開形式的執行結果相同。 這里,我們列出了所有以 BASH 開頭的環境變量名:
~~~
[me@linuxbox ~]$ echo ${!BASH*}
BASH BASH_ARGC BASH_ARGV BASH_COMMAND BASH_COMPLETION
BASH_COMPLETION_DIR BASH_LINENO BASH_SOURCE BASH_SUBSHELL
BASH_VERSINFO BASH_VERSION
~~~
### 字符串展開
有大量的展開形式可用于操作字符串。其中許多展開形式尤其適用于路徑名的展開。
**${#parameter}**
展開成由 parameter 所包含的字符串的長度。通常,parameter 是一個字符串;然而,如果 parameter 是 @ 或者是 * 的話, 則展開結果是位置參數的個數。
~~~
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo "'$foo' is ${#foo} characters long."
'This string is long.' is 20 characters long.
~~~
**${parameter:offset}**
**${parameter:offset:length}**
這些展開用來從 parameter 所包含的字符串中提取一部分字符。提取的字符始于 第 offset 個字符(從字符串開頭算起)直到字符串的末尾,除非指定提取的長度。
~~~
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo:5}
string is long.
[me@linuxbox ~]$ echo ${foo:5:6}
string
~~~
若 offset 的值為負數,則認為 offset 值是從字符串的末尾開始算起,而不是從開頭。注意負數前面必須有一個空格, 為防止與 ${parameter:-word} 展開形式混淆。length,若出現,則必須不能小于零。
如果 parameter 是 @,展開結果是 length 個位置參數,從第 offset 個位置參數開始。
~~~
[me@linuxbox ~]$ foo="This string is long."
[me@linuxbox ~]$ echo ${foo: -5}
long.
[me@linuxbox ~]$ echo ${foo: -5:2}
lo
~~~
**${parameter#pattern}**
**${parameter##pattern}**
這些展開會從 paramter 所包含的字符串中清除開頭一部分文本,這些字符要匹配定義的 patten。pattern 是 通配符模式,就如那些用在路徑名展開中的模式。這兩種形式的差異之處是該 # 形式清除最短的匹配結果, 而該 ## 模式清除最長的匹配結果。
~~~
[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo#*.}
txt.zip
[me@linuxbox ~]$ echo ${foo##*.}
zip
~~~
**${parameter%pattern}**
**${parameter%%pattern}**
這些展開和上面的 # 和 ## 展開一樣,除了它們清除的文本從 parameter 所包含字符串的末尾開始,而不是開頭。
~~~
[me@linuxbox ~]$ foo=file.txt.zip
[me@linuxbox ~]$ echo ${foo%.*}
file.txt
[me@linuxbox ~]$ echo ${foo%%.*}
file
~~~
**${parameter/pattern/string}**
**${parameter//pattern/string}**
**${parameter/#pattern/string}**
**${parameter/%pattern/string}**
這種形式的展開對 parameter 的內容執行查找和替換操作。如果找到了匹配通配符 pattern 的文本, 則用 string 的內容替換它。在正常形式下,只有第一個匹配項會被替換掉。在該 // 形式下,所有的匹配項都會被替換掉。 該 /# 要求匹配項出現在字符串的開頭,而 /% 要求匹配項出現在字符串的末尾。/string 可能會省略掉,這樣會 導致刪除匹配的文本。
~~~
[me@linuxbox~]$ foo=JPG.JPG
[me@linuxbox ~]$ echo ${foo/JPG/jpg}
jpg.JPG
[me@linuxbox~]$ echo ${foo//JPG/jpg}
jpg.jpg
[me@linuxbox~]$ echo ${foo/#JPG/jpg}
jpg.JPG
[me@linuxbox~]$ echo ${foo/%JPG/jpg}
JPG.jpg
~~~
知道參數展開是件很好的事情。字符串操作展開可以用來替換其它常見命令比方說 sed 和 cut。 通過減少使用外部程序,展開提高了腳本的效率。舉例說明,我們將修改在之前章節中討論的 longest-word 程序, 用參數展開 ${#j} 取代命令 $(echo $j | wc -c) 及其 subshell ,像這樣:
~~~
#!/bin/bash
# longest-word3 : find longest string in a file
for i; do
if [[ -r $i ]]; then
max_word=
max_len=
for j in $(strings $i); do
len=${#j}
if (( len > max_len )); then
max_len=$len
max_word=$j
fi
done
echo "$i: '$max_word' ($max_len characters)"
fi
shift
done
~~~
下一步,我們將使用 time 命令來比較這兩個腳本版本的效率:
~~~
[me@linuxbox ~]$ time longest-word2 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list' (38
characters)
real 0m3.618s
user 0m1.544s
sys 0m1.768s
[me@linuxbox ~]$ time longest-word3 dirlist-usr-bin.txt
dirlist-usr-bin.txt: 'scrollkeeper-get-extended-content-list' (38
characters)
real 0m0.060s
user 0m0.056s
sys 0m0.008s
~~~
原來的腳本掃描整個文本文件需耗時3.168秒,而該新版本,使用參數展開,僅僅花費了0.06秒 —— 一個非常巨大的提高。
### 大小寫轉換
最新的 bash 版本已經支持字符串的大小寫轉換了。bash 有四個參數展開和 declare 命令的兩個選項來支持大小寫轉換。
那么大小寫轉換對什么有好處呢? 除了明顯的審美價值,它在編程領域還有一個重要的角色。 讓我們考慮一個數據庫查詢的案例。假設一個用戶已經敲寫了一個字符串到數據輸入框中, 而我們想要在一個數據庫中查找這個字符串。該用戶輸入的字符串有可能全是大寫字母或全是小寫或是兩者的結合。 我們當然不希望把每個可能的大小寫拼寫排列填充到我們的數據庫中。那怎么辦?
解決這個問題的常見方法是規范化用戶輸入。也就是,在我們試圖查詢數據庫之前,把用戶的輸入轉換成標準化。 我們能做到這一點,通過把用戶輸入的字符全部轉換成小寫字母或大寫字母,并且確保數據庫中的條目 按同樣的方式規范化。
這個 declare 命令可以用來把字符串規范成大寫或小寫字符。使用 declare 命令,我們能強制一個 變量總是包含所需的格式,無論如何賦值給它。
~~~
#!/bin/bash
# ul-declare: demonstrate case conversion via declare
declare -u upper
declare -l lower
if [[ $1 ]]; then
upper="$1"
lower="$1"
echo $upper
echo $lower
fi
~~~
在上面的腳本中,我們使用 declare 命令來創建兩個變量,upper 和 lower。我們把第一個命令行參數的值(位置參數1)賦給 每一個變量,然后把變量值在屏幕上顯示出來:
~~~
[me@linuxbox ~]$ ul-declare aBc
ABC
abc
~~~
正如我們所看到的,命令行參數(“aBc”)已經規范化了。
有四個參數展開,可以執行大小寫轉換操作:
表 35-1: 大小寫轉換參數展開
| 格式 | 結果 |
|------|-------|
| ${parameter,,} | 把 parameter 的值全部展開成小寫字母。 |
| ${parameter,} | 僅僅把 parameter 的第一個字符展開成小寫字母。 |
| ${parameter^^} | 把 parameter 的值全部轉換成大寫字母。 |
| ${parameter^} | 僅僅把 parameter 的第一個字符轉換成大寫字母(首字母大寫)。 |
這里是一個腳本,演示了這些展開格式:
~~~
#!/bin/bash
# ul-param - demonstrate case conversion via parameter expansion
if [[ $1 ]]; then
echo ${1,,}
echo ${1,}
echo ${1^^}
echo ${1^}
fi
~~~
這里是腳本運行后的結果:
~~~
[me@linuxbox ~]$ ul-param aBc
abc
aBc
ABC
ABc
~~~
再次,我們處理了第一個命令行參數,輸出了由參數展開支持的四種變體。盡管這個腳本使用了第一個位置參數, 但參數可以是任意字符串,變量,或字符串表達式。
## 算術求值和展開
我們在第七章中已經接觸過算術展開了。它被用來對整數執行各種算術運算。它的基本格式是:
~~~
$((expression))
~~~
這里的 expression 是一個有效的算術表達式。
這個與復合命令 (( )) 有關,此命令用做算術求值(真測試),我們在第27章中遇到過。
在之前的章節中,我們看到過一些類型的表達式和運算符。這里,我們將看到一個更完整的列表。
### 數基
回到第9章,我們看過八進制(以8為底)和十六進制(以16為底)的數字。在算術表達式中,shell 支持任意進制的整形常量。
表 35-2: 指定不同的數基
| 表示法 | 描述 |
|------|-------|
| number | 默認情況下,沒有任何表示法的數字被看做是十進制數(以10為底)。 |
| 0number | 在算術表達式中,以零開頭的數字被認為是八進制數。 |
| 0xnumber | 十六進制表示法 |
| base#number | number 以 base 為底 |
一些例子:
~~~
[me@linuxbox ~]$ echo $((0xff))
255
[me@linuxbox ~]$ echo $((2#11111111))
255
~~~
在上面的示例中,我們打印出十六進制數 ff(最大的兩位數)的值和最大的八位二進制數(以2為底)。
### 一元運算符
有兩個二元運算符,+ 和 -,它們被分別用來表示一個數字是正數還是負數。例如,-5。
### 簡單算術
下表中列出了普通算術運算符:
表 35-3: 算術運算符
| 運算符 | 描述 |
|------|-------|
| + | 加 |
| - | 減 |
| * | 乘 |
| / | 整除 |
| ** | 乘方 |
| % | 取模(余數) |
其中大部分運算符是不言自明的,但是整除和取模運算符需要進一步解釋一下。
因為 shell 算術只操作整形,所以除法運算的結果總是整數:
~~~
[me@linuxbox ~]$ echo $(( 5 / 2 ))
2
~~~
這使得確定除法運算的余數更為重要:
~~~
[me@linuxbox ~]$ echo $(( 5 % 2 ))
1
~~~
通過使用除法和取模運算符,我們能夠確定5除以2得數是2,余數是1。
在循環中計算余數是很有用處的。在循環執行期間,它允許某一個操作在指定的間隔內執行。在下面的例子中, 我們顯示一行數字,并高亮顯示5的倍數:
~~~
#!/bin/bash
# modulo : demonstrate the modulo operator
for ((i = 0; i <= 20; i = i + 1)); do
remainder=$((i % 5))
if (( remainder == 0 )); then
printf "<%d> " $i
else
printf "%d " $i
fi
done
printf "\n"
~~~
當腳本執行后,輸出結果看起來像這樣:
~~~
[me@linuxbox ~]$ modulo
<0> 1 2 3 4 <5> 6 7 8 9 <10> 11 12 13 14 <15> 16 17 18 19 <20>
~~~
### 賦值運算符
盡管它的使用不是那么明顯,算術表達式可能執行賦值運算。雖然在不同的上下文中,我們已經執行了許多次賦值運算。 每次我們給變量一個值,我們就執行了一次賦值運算。我們也能在算術表達式中執行賦值運算:
~~~
[me@linuxbox ~]$ foo=
[me@linuxbox ~]$ echo $foo
[me@linuxbox ~]$ if (( foo = 5 ));then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ echo $foo
5
~~~
在上面的例子中,首先我們給變量 foo 賦了一個空值,然后驗證 foo 的確為空。下一步,我們執行一個 if 復合命令 (( foo = 5 ))。 這個過程完成兩件有意思的事情:1)它把5賦值給變量 foo,2)它計算測試條件為真,因為 foo 的值非零。
* * *
注意: 記住上面表達式中 = 符號的真正含義非常重要。單個 = 運算符執行賦值運算。foo = 5 是說“使得 foo 等于5”, 而 == 運算符計算等價性。foo == 5 是說“是否 foo 等于5?”。這會讓人感到非常迷惑,因為 test 命令接受單個 = 運算符 來測試字符串等價性。這也是使用更現代的 [[ ]] 和 (( )) 復合命令來代替 test 命令的另一個原因。
* * *
除了 = 運算符,shell 也提供了其它一些表示法,來執行一些非常有用的賦值運算:
表35-4: 賦值運算符
| 表示法 | 描述 |
|------|-------|
| parameter = value | 簡單賦值。給 parameter 賦值。 |
| parameter += value | 加。等價于 parameter = parameter + value。 |
| parameter -= value | 減。等價于 parameter = parameter – value。 |
| parameter *= value | 乘。等價于 parameter = parameter * value。 |
| parameter /= value | 整除。等價于 parameter = parameter / value。 |
| parameter %= value | 取模。等價于 parameter = parameter % value。 |
| parameter++ | 后綴自增變量。等價于 parameter = parameter + 1 (但,要看下面的討論)。 |
| parameter-- | 后綴自減變量。等價于 parameter = parameter - 1。 |
| ++parameter | 前綴自增變量。等價于 parameter = parameter + 1。 |
| --parameter | 前綴自減變量。等價于 parameter = parameter - 1。 |
這些賦值運算符為許多常見算術任務提供了快捷方式。特別關注一下自增(++)和自減(--)運算符,它們會把它們的參數值加1或減1。 這種風格的表示法取自C 編程語言并且被其它幾種編程語言吸收,包括 bash。
自增和自減運算符可能會出現在參數的前面或者后面。然而它們都是把參數值加1或減1,這兩個位置有個微小的差異。 若運算符放置在參數的前面,參數值會在參數返回之前增加(或減少)。若放置在后面,則運算會在參數返回之后執行。 這相當奇怪,但這是它預期的行為。這里是個演示的例子:
~~~
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((foo++))
1
[me@linuxbox ~]$ echo $foo
2
~~~
如果我們把1賦值給變量 foo,然后通過把自增運算符 ++ 放到參數名 foo 之后來增加它,foo 返回1。 然而,如果我們第二次查看變量 foo 的值,我們看到它的值增加了1。若我們把 ++ 運算符放到參數 foo 之前, 我們得到更期望的行為:
~~~
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((++foo))
2
[me@linuxbox ~]$ echo $foo
2
~~~
對于大多數 shell 應用來說,前綴運算符最有用。
自增 ++ 和 自減 -- 運算符經常和循環操作結合使用。我們將改進我們的 modulo 腳本,讓代碼更緊湊些:
~~~
#!/bin/bash
# modulo2 : demonstrate the modulo operator
for ((i = 0; i <= 20; ++i )); do
if (((i % 5) == 0 )); then
printf "<%d> " $i
else
printf "%d " $i
fi
done
printf "\n"
~~~
### 位運算符
位運算符是一類以不尋常的方式操作數字的運算符。這些運算符工作在位級別的數字。它們被用在某類底層的任務中, 經常涉及到設置或讀取位標志。
表35-5: 位運算符
| 運算符 | 描述 |
|------|-------|
| ~ | 按位取反。對一個數字所有位取反。 |
| << | 位左移. 把一個數字的所有位向左移動。 |
| >> | 位右移. 把一個數字的所有位向右移動。 |
| & | 位與。對兩個數字的所有位執行一個 AND 操作。 |
| | | 位或。對兩個數字的所有位執行一個 OR 操作。 |
| ^ | 位異或。對兩個數字的所有位執行一個異或操作。 |
注意除了按位取反運算符之外,其它所有位運算符都有相對應的賦值運算符(例如,<<=)。
這里我們將演示產生2的冪列表的操作,使用位左移運算符:
~~~
[me@linuxbox ~]$ for ((i=0;i<8;++i)); do echo $((1<<i)); done
1
2
4
8
16
32
64
128
~~~
### 邏輯運算符
正如我們在第27章中所看到的,復合命令 (( )) 支持各種各樣的比較運算符。還有一些可以用來計算邏輯運算。 這里是比較運算符的完整列表:
表35-6: 比較運算符
| 運算符 | 描述 |
|------|-------|
| <= | 小于或相等 |
| >= | 大于或相等 |
| < | 小于 |
| > | 大于 |
| == | 相等 |
| != | 不相等 |
| && | 邏輯與 |
| || | 邏輯或 |
| expr1?expr2:expr3 | 條件(三元)運算符。若表達式 expr1 的計算結果為非零值(算術真),則 執行表達式 expr2,否則執行表達式 expr3。 |
當表達式用于邏輯運算時,表達式遵循算術邏輯規則;也就是,表達式的計算結果是零,則認為假,而非零表達式認為真。 該 (( )) 復合命令把結果映射成 shell 正常的退出碼:
~~~
[me@linuxbox ~]$ if ((1)); then echo "true"; else echo "false"; fi
true
[me@linuxbox ~]$ if ((0)); then echo "true"; else echo "false"; fi
false
~~~
最陌生的邏輯運算符就是這個三元運算符了。這個運算符(仿照于 C 編程語言里的三元運算符)執行一個單獨的邏輯測試。 它用起來類似于 if/then/else 語句。它操作三個算術表達式(字符串不會起作用),并且若第一個表達式為真(或非零), 則執行第二個表達式。否則,執行第三個表達式。我們可以在命令行中實驗一下:
~~~
[me@linuxbox~]$ a=0
[me@linuxbox~]$ ((a<1?++a:--a))
[me@linuxbox~]$ echo $a
1
[me@linuxbox~]$ ((a<1?++a:--a))
[me@linuxbox~]$ echo $a
0
~~~
這里我們看到一個實際使用的三元運算符。這個例子實現了一個切換。每次運算符執行的時候,變量 a 的值從零變為1,或反之亦然。
請注意在表達式內執行賦值卻并非易事。
當企圖這樣做時,bash 會聲明一個錯誤:
~~~
[me@linuxbox ~]$ a=0
[me@linuxbox ~]$ ((a<1?a+=1:a-=1))
bash: ((: a<1?a+=1:a-=1: attempted assignment to non-variable (error token is "-=1")
~~~
通過把賦值表達式用括號括起來,可以解決這個錯誤:
~~~
[me@linuxbox ~]$ ((a<1?(a+=1):(a-=1)))
~~~
下一步,我們看一個使用算術運算符更完備的例子,該示例產生一個簡單的數字表格:
~~~
#!/bin/bash
# arith-loop: script to demonstrate arithmetic operators
finished=0
a=0
printf "a\ta**2\ta**3\n"
printf "=\t====\t====\n"
until ((finished)); do
b=$((a**2))
c=$((a**3))
printf "%d\t%d\t%d\n" $a $b $c
((a<10?++a:(finished=1)))
done
~~~
在這個腳本中,我們基于變量 finished 的值實現了一個 until 循環。首先,把變量 finished 的值設為零(算術假), 繼續執行循環之道它的值變為非零。在循環體內,我們計算計數器 a 的平方和立方。在循環末尾,計算計數器變量 a 的值。 若它小于10(最大迭代次數),則 a 的值加1,否則給變量 finished 賦值為1,使得變量 finished 算術為真, 從而終止循環。運行該腳本得到這樣的結果:
~~~
[me@linuxbox ~]$ arith-loop
a a**2 a**3
= ==== ====
0 0 0
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
6 36 216
7 49 343
8 64 512
9 81 729
10 100 1000
~~~
## bc - 一種高精度計算器語言
我們已經看到 shell 是可以處理所有類型的整形算術的,但是如果我們需要執行更高級的數學運算或僅使用浮點數,該怎么辦? 答案是,我們不能這樣做。至少不能直接用 shell 完成此類運算。為此,我們需要使用外部程序。 有幾種途徑可供我們采用。嵌入的 Perl 或者 AWK 程序是一種可能的方案,但是不幸的是,超出了本書的內容大綱。 另一種方式就是使用一種專業的計算器程序。這樣一個程序叫做 bc,在大多數 Linux 系統中都可以找到。
該 bc 程序讀取一個用它自己的類似于 C 語言的語法編寫的腳本文件。一個 bc 腳本可能是一個分離的文件或者是讀取 標準輸入。bc 語言支持相當少的功能,包括變量,循環和程序員定義的函數。這里我們不會討論整個 bc 語言, 僅僅體驗一下。查看 bc 的手冊頁,其文檔整理非常好。
讓我們從一個簡單的例子開始。我們將編寫一個 bc 腳本來執行2加2運算:
~~~
/* A very simple bc script */
2 + 2
~~~
腳本的第一行是一行注釋。bc 使用和 C 編程語言一樣的注釋語法。注釋,可能會跨越多行,開始于?`/*`?結束于`*/`。
### 使用 bc
如果我們把上面的 bc 腳本保存為 foo.bc,然后我們就能這樣運行它:
~~~
[me@linuxbox ~]$ bc foo.bc
bc 1.06.94
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software
Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
4
~~~
如果我們仔細觀察,我們看到算術結果在最底部,版權信息之后。可以通過 -q(quiet)選項禁止這些版權信息。 bc 也能夠交互使用:
~~~
[me@linuxbox ~]$ bc -q
2 + 2
4
quit
~~~
當使用 bc 交互模式時,我們簡單地輸入我們希望執行的運算,結果就立即顯示出來。bc 的 quit 命令結束交互會話。
也可能通過標準輸入把一個腳本傳遞給 bc 程序:
~~~
[me@linuxbox ~]$ bc < foo.bc
4
~~~
這種接受標準輸入的能力,意味著我們可以使用 here 文檔,here字符串,和管道來傳遞腳本。這里是一個使用 here 字符串的例子:
~~~
[me@linuxbox ~]$ bc <<< "2+2"
4
~~~
### 一個腳本實例
作為一個真實世界的例子,我們將構建一個腳本,用于計算每月的還貸金額。在下面的腳本中, 我們使用了 here 文檔把一個腳本傳遞給 bc:
~~~
#!/bin/bash
# loan-calc : script to calculate monthly loan payments
PROGNAME=$(basename $0)
usage () {
cat <<- EOF
Usage: $PROGNAME PRINCIPAL INTEREST MONTHS
Where:
PRINCIPAL is the amount of the loan.
INTEREST is the APR as a number (7% = 0.07).
MONTHS is the length of the loan's term.
EOF
}
if (($# != 3)); then
usage
exit 1
fi
principal=$1
interest=$2
months=$3
bc <<- EOF
scale = 10
i = $interest / 12
p = $principal
n = $months
a = p * ((i * ((1 + i) ^ n)) / (((1 + i) ^ n) - 1))
print a, "\n"
EOF
~~~
當腳本執行后,輸出結果像這樣:
~~~
[me@linuxbox ~]$ loan-calc 135000 0.0775 180
475
1270.7222490000
~~~
若貸款 135,000 美金,年利率為 7.75%,借貸180個月(15年),這個例子計算出每月需要還貸的金額。 注意這個答案的精確度。這是由腳本中變量 scale 的值決定的。bc 的手冊頁提供了對 bc 腳本語言的詳盡描述。 雖然 bc 的數學符號與 shell 的略有差異(bc 與 C 更相近),但是基于目前我們所學的內容, 大多數符號是我們相當熟悉的。
## 總結
在這一章中,我們學習了很多小東西,在腳本中這些小零碎可以完成“真正的工作”。隨著我們編寫腳本經驗的增加, 能夠有效地操作字符串和數字的能力將具有極為重要的價值。我們的 loan-calc 腳本表明, 甚至可以創建簡單的腳本來完成一些真正有用的事情。
## 額外加分
雖然該 loan-calc 腳本的基本功能已經很到位了,但腳本還遠遠不夠完善。為了額外加分,試著 給腳本 loan-calc 添加以下功能:
* 完整的命令行參數驗證
* 用一個命令行選項來實現“交互”模式,提示用戶輸入本金、利率和貸款期限
* 輸出格式美化
## 拓展閱讀
* 《Bash Hackers Wiki》對參數展開有一個很好的論述:
[http://wiki.bash-hackers.org/syntax/pe](http://wiki.bash-hackers.org/syntax/pe)
* 《Bash 參考手冊》也介紹了這個:
[http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion](http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion)
* Wikipedia 上面有一篇很好的文章描述了位運算:
[http://en.wikipedia.org/wiki/Bit_operation](http://en.wikipedia.org/wiki/Bit_operation)
* 和一篇關于三元運算的文章:
[http://en.wikipedia.org/wiki/Ternary_operation](http://en.wikipedia.org/wiki/Ternary_operation)
* 還有一個對計算還貸金額公式的描述,我們的 loan-calc 腳本中用到了這個公式:
[http://en.wikipedia.org/wiki/Amortization_calculator](http://en.wikipedia.org/wiki/Amortization_calculator)
- 第一章:引言
- 第二章:什么是shell
- 第三章:文件系統中跳轉
- 第四章:研究操作系統
- 第五章:操作文件和目錄
- 第六章:使用命令
- 第七章:重定向
- 第八章:從shell眼中看世界
- 第九章:鍵盤高級操作技巧
- 第十章:權限
- 第十一章:進程
- 第十二章:shell環境
- 第十三章:VI簡介
- 第十四章:自定制shell提示符
- 第十五章:軟件包管理
- 第十六章:存儲媒介
- 第十七章:網絡系統
- 第十八章:查找文件
- 第十九章:歸檔和備份
- 第二十章:正則表達式
- 第二十一章:文本處理
- 第二十二章:格式化輸出
- 第二十三章:打印
- 第二十四章:編譯程序
- 第二十五章:編寫第一個shell腳本
- 第二十六章:啟動一個項目
- 第二十七章:自頂向下設計
- 第二十八章:流程控制 if分支結構
- 第二十九章:讀取鍵盤輸入
- 第三十章:流程控制 while/until 循環
- 第三十一章:疑難排解
- 第三十二章:流程控制 case分支
- 第三十三章:位置參數
- 第三十四章:流程控制 for循環
- 第三十五章:字符串和數字
- 第三十六章:數組
- 第三十七章:奇珍異寶