所有的計算機(jī)程序都是用來和數(shù)據(jù)打交道的。在過去的章節(jié)中,我們專注于處理文件級別的數(shù)據(jù)。 然而,許多程序問題需要使用更小的數(shù)據(jù)單位來解決,比方說字符串和數(shù)字。
在這一章中,我們將查看幾個用來操作字符串和數(shù)字的 shell 功能。shell 提供了各種執(zhí)行字符串操作的參數(shù)展開功能。 除了算術(shù)展開(在第七章中接觸過),還有一個常見的命令行程序叫做 bc,能執(zhí)行更高級別的數(shù)學(xué)運算。
盡管參數(shù)展開在第七章中出現(xiàn)過,但我們并沒有詳盡地介紹它,因為大多數(shù)的參數(shù)展開會用在腳本中,而不是命令行中。 我們已經(jīng)使用了一些形式的參數(shù)展開;例如,shell 變量。shell 提供了更多方式。
最簡單的參數(shù)展開形式反映在平常使用的變量上。
例如:
$a
當(dāng) $a 展開后,會變成變量 a 所包含的值。簡單參數(shù)也可能用花括號引起來:
${a}
雖然這對展開沒有影響,但若該變量 a 與其它的文本相鄰,可能會把 shell 搞糊涂了。在這個例子中,我們試圖 創(chuàng)建一個文件名,通過把字符串 “_file” 附加到變量 a 的值的后面。
[me@linuxbox ~]$ a="foo"
[me@linuxbox ~]$ echo "$a_file"
如果我們執(zhí)行這個序列,沒有任何輸出結(jié)果,因為 shell 會試著展開一個稱為 a_file 的變量,而不是 a。通過 添加花括號可以解決這個問題:
[me@linuxbox ~]$ echo "${a}_file"
foo_file
我們已經(jīng)知道通過把數(shù)字包裹在花括號中,可以訪問大于9的位置參數(shù)。例如,訪問第十一個位置參數(shù),我們可以這樣做:
${11}
幾種用來處理不存在和空變量的參數(shù)展開形式。這些展開形式對于解決丟失的位置參數(shù)和給參數(shù)指定默認(rèn)值的情況很方便。
${parameter:-word}
若 parameter 沒有設(shè)置(例如,不存在)或者為空,展開結(jié)果是 word 的值。若 parameter 不為空,則展開結(jié)果是 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 沒有設(shè)置或為空,展開結(jié)果是 word 的值。另外,word 的值會賦值給 parameter。 若 parameter 不為空,展開結(jié)果是 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
注意: 位置參數(shù)或其它的特殊參數(shù)不能以這種方式賦值。
${parameter:?word}
若 parameter 沒有設(shè)置或為空,這種展開導(dǎo)致腳本帶有錯誤退出,并且 word 的內(nèi)容會發(fā)送到標(biāo)準(zhǔn)錯誤。若 parameter 不為空, 展開結(jié)果是 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 沒有設(shè)置或為空,展開結(jié)果為空。若 parameter 不為空, 展開結(jié)果是 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 具有返回變量名的能力。這會用在一些相當(dāng)獨特的情況下。
${!prefix*}
${!prefix@}
這種展開會返回以 prefix 開頭的已有變量名。根據(jù) bash 文檔,這兩種展開形式的執(zhí)行結(jié)果相同。 這里,我們列出了所有以 BASH 開頭的環(huán)境變量名:
[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 是 @ 或者是 * 的話, 則展開結(jié)果是位置參數(shù)的個數(shù)。
[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 的值為負(fù)數(shù),則認(rèn)為 offset 值是從字符串的末尾開始算起,而不是從開頭。注意負(fù)數(shù)前面必須有一個空格, 為防止與 ${parameter:-word} 展開形式混淆。length,若出現(xiàn),則必須不能小于零。
如果 parameter 是 @,展開結(jié)果是 length 個位置參數(shù),從第 offset 個位置參數(shù)開始。
[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 是 通配符模式,就如那些用在路徑名展開中的模式。這兩種形式的差異之處是該 # 形式清除最短的匹配結(jié)果, 而該 ## 模式清除最長的匹配結(jié)果。
[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 的內(nèi)容執(zhí)行查找和替換操作。如果找到了匹配通配符 pattern 的文本, 則用 string 的內(nèi)容替換它。在正常形式下,只有第一個匹配項會被替換掉。在該 // 形式下,所有的匹配項都會被替換掉。 該 /# 要求匹配項出現(xiàn)在字符串的開頭,而 /% 要求匹配項出現(xiàn)在字符串的末尾。/string 可能會省略掉,這樣會 導(dǎo)致刪除匹配的文本。
[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
知道參數(shù)展開是件很好的事情。字符串操作展開可以用來替換其它常見命令比方說 sed 和 cut。 通過減少使用外部程序,展開提高了腳本的效率。舉例說明,我們將修改在之前章節(jié)中討論的 longest-word 程序, 用參數(shù)展開 ${#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秒,而該新版本,使用參數(shù)展開,僅僅花費了0.06秒 —— 一個非常巨大的提高。
最新的 bash 版本已經(jīng)支持字符串的大小寫轉(zhuǎn)換了。bash 有四個參數(shù)展開和 declare 命令的兩個選項來支持大小寫轉(zhuǎn)換。
那么大小寫轉(zhuǎn)換對什么有好處呢? 除了明顯的審美價值,它在編程領(lǐng)域還有一個重要的角色。 讓我們考慮一個數(shù)據(jù)庫查詢的案例。假設(shè)一個用戶已經(jīng)敲寫了一個字符串到數(shù)據(jù)輸入框中, 而我們想要在一個數(shù)據(jù)庫中查找這個字符串。該用戶輸入的字符串有可能全是大寫字母或全是小寫或是兩者的結(jié)合。 我們當(dāng)然不希望把每個可能的大小寫拼寫排列填充到我們的數(shù)據(jù)庫中。那怎么辦?
解決這個問題的常見方法是規(guī)范化用戶輸入。也就是,在我們試圖查詢數(shù)據(jù)庫之前,把用戶的輸入轉(zhuǎn)換成標(biāo)準(zhǔn)化。 我們能做到這一點,通過把用戶輸入的字符全部轉(zhuǎn)換成小寫字母或大寫字母,并且確保數(shù)據(jù)庫中的條目 按同樣的方式規(guī)范化。
這個 declare 命令可以用來把字符串規(guī)范成大寫或小寫字符。使用 declare 命令,我們能強(qiáng)制一個 變量總是包含所需的格式,無論如何賦值給它。
#!/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 命令來創(chuàng)建兩個變量,upper 和 lower。我們把第一個命令行參數(shù)的值(位置參數(shù)1)賦給 每一個變量,然后把變量值在屏幕上顯示出來:
[me@linuxbox ~]$ ul-declare aBc
ABC
abc
正如我們所看到的,命令行參數(shù)(“aBc”)已經(jīng)規(guī)范化了。
有四個參數(shù)展開,可以執(zhí)行大小寫轉(zhuǎn)換操作:
格式 | 結(jié)果 |
---|---|
${parameter,,} | 把 parameter 的值全部展開成小寫字母。 |
${parameter,} | 僅僅把 parameter 的第一個字符展開成小寫字母。 |
${parameter^^} | 把 parameter 的值全部轉(zhuǎn)換成大寫字母。 |
${parameter^} | 僅僅把 parameter 的第一個字符轉(zhuǎn)換成大寫字母(首字母大寫)。 |
這里是一個腳本,演示了這些展開格式:
#!/bin/bash
# ul-param - demonstrate case conversion via parameter expansion
if [[ $1 ]]; then
echo ${1,,}
echo ${1,}
echo ${1^^}
echo ${1^}
fi
這里是腳本運行后的結(jié)果:
[me@linuxbox ~]$ ul-param aBc
abc
aBc
ABC
ABc
再次,我們處理了第一個命令行參數(shù),輸出了由參數(shù)展開支持的四種變體。盡管這個腳本使用了第一個位置參數(shù), 但參數(shù)可以是任意字符串,變量,或字符串表達(dá)式。
我們在第七章中已經(jīng)接觸過算術(shù)展開了。它被用來對整數(shù)執(zhí)行各種算術(shù)運算。它的基本格式是:
$((expression))
這里的 expression 是一個有效的算術(shù)表達(dá)式。
這個與復(fù)合命令 (( )) 有關(guān),此命令用做算術(shù)求值(真測試),我們在第27章中遇到過。
在之前的章節(jié)中,我們看到過一些類型的表達(dá)式和運算符。這里,我們將看到一個更完整的列表。
回到第9章,我們看過八進(jìn)制(以8為底)和十六進(jìn)制(以16為底)的數(shù)字。在算術(shù)表達(dá)式中,shell 支持任意進(jìn)制的整形常量。
表示法 | 描述 |
---|---|
number | 默認(rèn)情況下,沒有任何表示法的數(shù)字被看做是十進(jìn)制數(shù)(以10為底)。 |
0number | 在算術(shù)表達(dá)式中,以零開頭的數(shù)字被認(rèn)為是八進(jìn)制數(shù)。 |
0xnumber | 十六進(jìn)制表示法 |
base#number | number 以 base 為底 |
一些例子:
[me@linuxbox ~]$ echo $((0xff))
255
[me@linuxbox ~]$ echo $((2#11111111))
255
在上面的示例中,我們打印出十六進(jìn)制數(shù) ff(最大的兩位數(shù))的值和最大的八位二進(jìn)制數(shù)(以2為底)。
有兩個二元運算符,+ 和 -,它們被分別用來表示一個數(shù)字是正數(shù)還是負(fù)數(shù)。例如,-5。
下表中列出了普通算術(shù)運算符:
運算符 | 描述 |
---|---|
+ | 加 |
- | 減 |
* | 乘 |
/ | 整除 |
** | 乘方 |
% | 取模(余數(shù)) |
其中大部分運算符是不言自明的,但是整除和取模運算符需要進(jìn)一步解釋一下。
因為 shell 算術(shù)只操作整形,所以除法運算的結(jié)果總是整數(shù):
[me@linuxbox ~]$ echo $(( 5 / 2 ))
2
這使得確定除法運算的余數(shù)更為重要:
[me@linuxbox ~]$ echo $(( 5 % 2 ))
1
通過使用除法和取模運算符,我們能夠確定5除以2得數(shù)是2,余數(shù)是1。
在循環(huán)中計算余數(shù)是很有用處的。在循環(huán)執(zhí)行期間,它允許某一個操作在指定的間隔內(nèi)執(zhí)行。在下面的例子中, 我們顯示一行數(shù)字,并高亮顯示5的倍數(shù):
#!/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"
當(dāng)腳本執(zhí)行后,輸出結(jié)果看起來像這樣:
[me@linuxbox ~]$ modulo
<0> 1 2 3 4 <5> 6 7 8 9 <10> 11 12 13 14 <15> 16 17 18 19 <20>
盡管它的使用不是那么明顯,算術(shù)表達(dá)式可能執(zhí)行賦值運算。雖然在不同的上下文中,我們已經(jīng)執(zhí)行了許多次賦值運算。 每次我們給變量一個值,我們就執(zhí)行了一次賦值運算。我們也能在算術(shù)表達(dá)式中執(zhí)行賦值運算:
[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 的確為空。下一步,我們執(zhí)行一個 if 復(fù)合命令 (( foo = 5 ))。 這個過程完成兩件有意思的事情:1)它把5賦值給變量 foo,2)它計算測試條件為真,因為 foo 的值非零。
注意: 記住上面表達(dá)式中 = 符號的真正含義非常重要。單個 = 運算符執(zhí)行賦值運算。foo = 5 是說“使得 foo 等于5”, 而 == 運算符計算等價性。foo == 5 是說“是否 foo 等于5?”。這會讓人感到非常迷惑,因為 test 命令接受單個 = 運算符 來測試字符串等價性。這也是使用更現(xiàn)代的 [[ ]] 和 (( )) 復(fù)合命令來代替 test 命令的另一個原因。
除了 = 運算符,shell 也提供了其它一些表示法,來執(zhí)行一些非常有用的賦值運算:
表示法 | 描述 |
---|---|
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。 |
這些賦值運算符為許多常見算術(shù)任務(wù)提供了快捷方式。特別關(guān)注一下自增(++)和自減(--)運算符,它們會把它們的參數(shù)值加1或減1。 這種風(fēng)格的表示法取自C 編程語言并且被其它幾種編程語言吸收,包括 bash。
自增和自減運算符可能會出現(xiàn)在參數(shù)的前面或者后面。然而它們都是把參數(shù)值加1或減1,這兩個位置有個微小的差異。 若運算符放置在參數(shù)的前面,參數(shù)值會在參數(shù)返回之前增加(或減少)。若放置在后面,則運算會在參數(shù)返回之后執(zhí)行。 這相當(dāng)奇怪,但這是它預(yù)期的行為。這里是個演示的例子:
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((foo++))
1
[me@linuxbox ~]$ echo $foo
2
如果我們把1賦值給變量 foo,然后通過把自增運算符 ++ 放到參數(shù)名 foo 之后來增加它,foo 返回1。 然而,如果我們第二次查看變量 foo 的值,我們看到它的值增加了1。若我們把 ++ 運算符放到參數(shù) foo 之前, 我們得到更期望的行為:
[me@linuxbox ~]$ foo=1
[me@linuxbox ~]$ echo $((++foo))
2
[me@linuxbox ~]$ echo $foo
2
對于大多數(shù) shell 應(yīng)用來說,前綴運算符最有用。
自增 ++ 和 自減 -- 運算符經(jīng)常和循環(huán)操作結(jié)合使用。我們將改進(jìn)我們的 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"
位運算符是一類以不尋常的方式操作數(shù)字的運算符。這些運算符工作在位級別的數(shù)字。它們被用在某類底層的任務(wù)中, 經(jīng)常涉及到設(shè)置或讀取位標(biāo)志。
運算符 | 描述 |
---|---|
~ | 按位取反。對一個數(shù)字所有位取反。 |
位左移. 把一個數(shù)字的所有位向左移動。 | |
>> | 位右移. 把一個數(shù)字的所有位向右移動。 |
& | 位與。對兩個數(shù)字的所有位執(zhí)行一個 AND 操作。 |
| | 位或。對兩個數(shù)字的所有位執(zhí)行一個 OR 操作。 |
^ | 位異或。對兩個數(shù)字的所有位執(zhí)行一個異或操作。 |
注意除了按位取反運算符之外,其它所有位運算符都有相對應(yīng)的賦值運算符(例如,<\<=)。
這里我們將演示產(chǎn)生2的冪列表的操作,使用位左移運算符:
[me@linuxbox ~]$ for ((i=0;i<8;++i)); do echo $((1<<i)); done
1
2
4
8
16
32
64
128
正如我們在第27章中所看到的,復(fù)合命令 (( )) 支持各種各樣的比較運算符。還有一些可以用來計算邏輯運算。 這里是比較運算符的完整列表:
運算符 | 描述 |
---|---|
小于或相等 | |
>= | 大于或相等 |
小于 | |
> | 大于 |
== | 相等 |
!= | 不相等 |
&& | 邏輯與 |
|| | 邏輯或 |
expr1?expr2:expr3 | 條件(三元)運算符。若表達(dá)式 expr1 的計算結(jié)果為非零值(算術(shù)真),則 執(zhí)行表達(dá)式 expr2,否則執(zhí)行表達(dá)式 expr3。 |
當(dāng)表達(dá)式用于邏輯運算時,表達(dá)式遵循算術(shù)邏輯規(guī)則;也就是,表達(dá)式的計算結(jié)果是零,則認(rèn)為假,而非零表達(dá)式認(rèn)為真。 該 (( )) 復(fù)合命令把結(jié)果映射成 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 編程語言里的三元運算符)執(zhí)行一個單獨的邏輯測試。 它用起來類似于 if/then/else 語句。它操作三個算術(shù)表達(dá)式(字符串不會起作用),并且若第一個表達(dá)式為真(或非零), 則執(zhí)行第二個表達(dá)式。否則,執(zhí)行第三個表達(dá)式。我們可以在命令行中實驗一下:
[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
這里我們看到一個實際使用的三元運算符。這個例子實現(xiàn)了一個切換。每次運算符執(zhí)行的時候,變量 a 的值從零變?yōu)?,或反之亦然。
請注意在表達(dá)式內(nèi)執(zhí)行賦值卻并非易事。
當(dāng)企圖這樣做時,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")
通過把賦值表達(dá)式用括號括起來,可以解決這個錯誤:
[me@linuxbox ~]$ ((a<1?(a+=1):(a-=1)))
下一步,我們看一個使用算術(shù)運算符更完備的例子,該示例產(chǎn)生一個簡單的數(shù)字表格:
#!/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 的值實現(xiàn)了一個 until 循環(huán)。首先,把變量 finished 的值設(shè)為零(算術(shù)假), 繼續(xù)執(zhí)行循環(huán)之道它的值變?yōu)榉橇恪T谘h(huán)體內(nèi),我們計算計數(shù)器 a 的平方和立方。在循環(huán)末尾,計算計數(shù)器變量 a 的值。 若它小于10(最大迭代次數(shù)),則 a 的值加1,否則給變量 finished 賦值為1,使得變量 finished 算術(shù)為真, 從而終止循環(huán)。運行該腳本得到這樣的結(jié)果:
[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
我們已經(jīng)看到 shell 是可以處理所有類型的整形算術(shù)的,但是如果我們需要執(zhí)行更高級的數(shù)學(xué)運算或僅使用浮點數(shù),該怎么辦? 答案是,我們不能這樣做。至少不能直接用 shell 完成此類運算。為此,我們需要使用外部程序。 有幾種途徑可供我們采用。嵌入的 Perl 或者 AWK 程序是一種可能的方案,但是不幸的是,超出了本書的內(nèi)容大綱。 另一種方式就是使用一種專業(yè)的計算器程序。這樣一個程序叫做 bc,在大多數(shù) Linux 系統(tǒng)中都可以找到。
該 bc 程序讀取一個用它自己的類似于 C 語言的語法編寫的腳本文件。一個 bc 腳本可能是一個分離的文件或者是讀取 標(biāo)準(zhǔn)輸入。bc 語言支持相當(dāng)少的功能,包括變量,循環(huán)和程序員定義的函數(shù)。這里我們不會討論整個 bc 語言, 僅僅體驗一下。查看 bc 的手冊頁,其文檔整理非常好。
讓我們從一個簡單的例子開始。我們將編寫一個 bc 腳本來執(zhí)行2加2運算:
/* A very simple bc script */
2 + 2
腳本的第一行是一行注釋。bc 使用和 C 編程語言一樣的注釋語法。注釋,可能會跨越多行,開始于 /*
結(jié)束于 */
。
如果我們把上面的 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
如果我們仔細(xì)觀察,我們看到算術(shù)結(jié)果在最底部,版權(quán)信息之后??梢酝ㄟ^ -q(quiet)選項禁止這些版權(quán)信息。 bc 也能夠交互使用:
[me@linuxbox ~]$ bc -q
2 + 2
4
quit
當(dāng)使用 bc 交互模式時,我們簡單地輸入我們希望執(zhí)行的運算,結(jié)果就立即顯示出來。bc 的 quit 命令結(jié)束交互會話。
也可能通過標(biāo)準(zhǔn)輸入把一個腳本傳遞給 bc 程序:
[me@linuxbox ~]$ bc < foo.bc
4
這種接受標(biāo)準(zhǔn)輸入的能力,意味著我們可以使用 here 文檔,here字符串,和管道來傳遞腳本。這里是一個使用 here 字符串的例子:
[me@linuxbox ~]$ bc <<< "2+2"
4
作為一個真實世界的例子,我們將構(gòu)建一個腳本,用于計算每月的還貸金額。在下面的腳本中, 我們使用了 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
當(dāng)腳本執(zhí)行后,輸出結(jié)果像這樣:
[me@linuxbox ~]$ loan-calc 135000 0.0775 180
475
1270.7222490000
若貸款 135,000 美金,年利率為 7.75%,借貸180個月(15年),這個例子計算出每月需要還貸的金額。 注意這個答案的精確度。這是由腳本中變量 scale 的值決定的。bc 的手冊頁提供了對 bc 腳本語言的詳盡描述。 雖然 bc 的數(shù)學(xué)符號與 shell 的略有差異(bc 與 C 更相近),但是基于目前我們所學(xué)的內(nèi)容, 大多數(shù)符號是我們相當(dāng)熟悉的。
在這一章中,我們學(xué)習(xí)了很多小東西,在腳本中這些小零碎可以完成“真正的工作”。隨著我們編寫腳本經(jīng)驗的增加, 能夠有效地操作字符串和數(shù)字的能力將具有極為重要的價值。我們的 loan-calc 腳本表明, 甚至可以創(chuàng)建簡單的腳本來完成一些真正有用的事情。
雖然該 loan-calc 腳本的基本功能已經(jīng)很到位了,但腳本還遠(yuǎn)遠(yuǎn)不夠完善。為了額外加分,試著 給腳本 loan-calc 添加以下功能:
完整的命令行參數(shù)驗證
用一個命令行選項來實現(xiàn)“交互”模式,提示用戶輸入本金、利率和貸款期限
《Bash Hackers Wiki》對參數(shù)展開有一個很好的論述:
《Bash 參考手冊》也介紹了這個:
http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion
Wikipedia 上面有一篇很好的文章描述了位運算:
和一篇關(guān)于三元運算的文章:
還有一個對計算還貸金額公式的描述,我們的 loan-calc 腳本中用到了這個公式: