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