UNIX 新手指南: 一些很好的 Shell 訣竅
當(dāng)編寫 Shell 程序時(shí),您通常會(huì)遇到一些特殊的情況,希望采用自動(dòng)方式處理。本教程包括一些關(guān)于此類情況的 Bourne Shell 腳本示例。這些情況包括字符串的進(jìn)制轉(zhuǎn)換(十進(jìn)制到十六進(jìn)制、十六進(jìn)制到十進(jìn)制、十進(jìn)制到八進(jìn)制,等等)、在管道循環(huán)中讀取鍵盤、Subshell 執(zhí)行、內(nèi)聯(lián)輸入、為目錄中的每個(gè)文件執(zhí)行一次命令,以及使用多種方法構(gòu)造連續(xù)循環(huán)。本系列文章的第 4 部分總結(jié)了一批執(zhí)行有用功能的 Shell 單命令行程序。
開始之前
了解本教程中包含的內(nèi)容以及如何最好地利用本教程。
關(guān)于本系列
本系列教程主要針對(duì)新用戶撰寫,簡要介紹 Unix® 基本概念。本系列教程的前三篇文章站在擁有 Microsoft® Windows® 背景的新用戶的角度重溫了一遍 UNIX 系統(tǒng),講述了文件系統(tǒng)和常用命令,介紹了 vi(最常見的 UNIX 編輯器),并且通過使用 grep、sed 和 awk 工具簡要介紹了篩選器和正則表達(dá)式。
關(guān)于本教程
本教程介紹了一套新用戶易于掌握的訣竅和技巧。說明在特定情況下,如何使用在 Bourne Shell 中編寫的小腳本自動(dòng)執(zhí)行操作,包括自動(dòng)執(zhí)行進(jìn)制轉(zhuǎn)換、讀取鍵盤輸入、在 Subshell 中執(zhí)行命令、為目錄中的所有文件執(zhí)行相同命令,以及多種形式的循環(huán)。本教程最后以一套實(shí)用的 Shell 單命令行程序作為結(jié)束。
目標(biāo)
本教程的目標(biāo)是向新用戶介紹如何使用和實(shí)現(xiàn)許多在各種級(jí)別上提供自動(dòng)化操作的 Shell 方法。本教程通過提供針對(duì)特定情況的訣竅和技巧來說明這些方法,并且提供適用于常見任務(wù)的 Shell 單命令行程序的概要性介紹。
先決條件
本教程面向相對(duì)不熟悉 UNIX 的用戶。唯一的先決條件是了解 UNIX 文件系統(tǒng)的基本知識(shí)和操作命令、命令行本身,以及能夠使用類似 vi 的編輯器編寫文本文件。本系列教程的前面部分對(duì)這些概念作了全面說明。
系統(tǒng)要求
您需要在帶有 Bourne 兼容 Shell 環(huán)境(例如 bash)的 UNIX 系統(tǒng)上擁有用戶級(jí)訪問權(quán)限。這是本教程唯一的系統(tǒng)要求。
Shell 命令執(zhí)行
學(xué)習(xí) Shell 腳本的最佳方法是通過示例。對(duì)于您要在腳本中執(zhí)行的任何命令都可以在命令行上立即嘗試,這也是本教程通篇提供大量實(shí)踐示例的原因所在。例如,echo 命令將一行文本寫入到標(biāo)準(zhǔn)輸出。(許多 Shell 以內(nèi)置命令形式提供其自己版本的 echo 命令,包括 IBM AIX® 的 Bourne Shell 實(shí)現(xiàn)。如果這也是您的現(xiàn)實(shí)情況,那么當(dāng)您運(yùn)行 echo 時(shí),實(shí)際上正在運(yùn)行您的 Shell 版本的命令。)
引用
嘗試在使用 echo 輸出短消息時(shí)加引號(hào):
$ echo "Hello, world"Hello, world
Shell 引用(無論在命令行還是在腳本中加注)是一種將字符串傳遞給 Shell 的方法,可以避免對(duì)字符串中可能包含的任何特殊元字符產(chǎn)生混淆。當(dāng)字符串包含一個(gè)以上的單詞或者段落包含空格字符時(shí)使用引用。如果單個(gè)字符恰好是 Shell 元字符,并且您想去除它的特殊含義,就可以在兩邊加上引號(hào),例如,當(dāng)您要傳遞一個(gè)美元符號(hào) ($) 作為字面上的美元符號(hào)字符而不是作為變量名前的特殊元字符時(shí)。
在引用的文本內(nèi)部發(fā)生各種擴(kuò)展。例如,在雙引號(hào)括起來的文本中,變量被展開為它們的值,而單引號(hào)括起來的文本內(nèi)部引用的變量名則不展開。
有三種重要的引用類型需要了解:
通過在前面加反斜杠 () 引用單個(gè)字符。這樣只會(huì)傳替字符的字面含義,而非它可能包含的任何特殊含義,比如空格符或 Shell 元字符。例如,使用 * 引用一個(gè)星號(hào) (*),它是 Shell 元字符。要引用真正的反斜杠字符,可以使用 。
通過在文本字符串兩邊加雙引號(hào) (") 來傳遞擴(kuò)展的引用。美元符號(hào) ($) 和單引號(hào) (') 字符會(huì)保留其自身含義。因此,和其他字符一起在引用中出現(xiàn)的任何變量名都會(huì)被它們的值所替代。新行或特定字符 ($`") 前的反斜杠被移除,但是引用的字符會(huì)被傳遞。
使用單引號(hào) (') 將文本括起來以傳遞文本字符串的字面引用,所有的變量名、元字符等都作為文字字符,而不它們的含義或值來傳遞。
請(qǐng)注意在不同的 Shell 中引用的確切規(guī)則會(huì)有所區(qū)別。參考您所使用的特殊 Shell 的 man 頁面來了解準(zhǔn)確規(guī)則。
分配一個(gè)變量,然后嘗試使用各種引用格式輸出該變量,如清單 1 中所示。
清單 1. 使用 echo 演示 Shell 變量引用格式
$ myvar = "Hello, world"$ echo $myvarHello, world$ echo "$myvar"Hello, world$ echo '$myvar'$myvar$ echo $myvar$myvar$ echo '$myvar''Hello, world'$ echo "'$myvar'"'Hello, world'$ echo '"$myvar"'"$myvar"$ echo "$myvar""Hello, world"
注意解釋變量的方式取決于所使用的引用格式。
注釋
在 Shell 中,以井號(hào) (#) 開始一個(gè)注釋行。井號(hào)及其后面跟隨的同一行的所有內(nèi)容都被忽略。嘗試輸入幾行夾雜注釋的文本,如清單 2 中所示:
清單 2. 在 Shell 中使用注釋
$ # a comment does nothing$ echo "Hello, world" # This text is ignoredHello, world$ echo # This will not output$ echo 'But a hash (#) can be quoted'But a hash (#) can be quoted$ echo "# Even in double quotes"# Even in double quotes$
創(chuàng)建 Shell 腳本
正如您所看到的,您可以直接在命令行測試這些 Shell 編程結(jié)構(gòu)。但是,當(dāng)您完成了單行命令的學(xué)習(xí)并且真正開始構(gòu)建更長的程序時(shí),您需要將程序?qū)懭敕Q為腳本的文件。腳本 是一個(gè)設(shè)置了可執(zhí)行位的文本文件,并且包含由 Shell 語言命令組成的程序。UNIX Shell 是一種解釋性語言,這意味著它的程序不經(jīng)過編譯,而是由解釋器讀取,解釋器本身是 Shell 可執(zhí)行程序,比如 /bin/sh、/bin/bsh 或 /bin/bash。
Shell 腳本的第一行通常都是相同的:
#!/bin/sh
這是 Shell 自己使用的一種特殊注釋,用于確定文件的語言或目錄。感嘆號(hào)在 UNIX 和排版術(shù)語中常常被稱為 bang,后面跟隨的路徑名告訴 Shell 應(yīng)該使用來執(zhí)行該文件的解釋器。在本例中是 /bin/sh,它在許多系統(tǒng)中代表 Bourne Shell 可執(zhí)行程序本身。舉例來說,特別為 Korn Shell 編寫的腳本應(yīng)該以 #!/usr/bin/ksh 開始,正如 Ruby 腳本將以 #!/usr/bin/ruby 開始。安裝 bash 之后,/bin/sh 通常是到 bash 二進(jìn)制程序的符號(hào)鏈接。并且考慮到兼容性,使用 /bin/sh 比使用 /bin/bash 更可取。在一些系統(tǒng)中,比如 IBM AIX 5L™,Bourne Shell 可執(zhí)行程序的名稱是 bsh,并且位于 /usr/bin/bsh。
清單 3 提供了 Shell 腳本的簡短示例。
清單 3. Shell 腳本示例
#!/bin/sh# This is a shell scriptmessage = "Hello, world!"echo "The message is '"$message"'"
按照本系列教程前面文章中的說明,使用 vi 編輯器鍵入該腳本并保存到名為 myscript 的文件中(請(qǐng)參見參考資料部分)。然后使用 chmod 設(shè)置該文件的執(zhí)行權(quán)限,使該文件可以執(zhí)行:
$ chmod u+x myscript
此命令使該文件只能由您執(zhí)行。如果希望系統(tǒng)中的所有用戶都能執(zhí)行該文件,那么您還可以為所有用戶設(shè)置執(zhí)行權(quán)限:
$ chmod a+x myscript
現(xiàn)在您可以運(yùn)行該腳本。給出該文件的名稱和相對(duì)于當(dāng)前工作目錄的路徑,在路徑中使用一個(gè)點(diǎn)字符 (.) 來表示:
$ ./myscriptThe message is 'Hello, world!'$
Shell 變量 PATH 包含一組以冒號(hào)分隔的目錄。它就是您的路徑,Shell 總是會(huì)“看到這些目錄中的所有文件。UNIX Path 的目的是為了便于運(yùn)行二進(jìn)制文件。這就是為什么您只需要鍵入命令的基本文件名,比如 ls 和 echo,而不用提供它們的完整或相對(duì)路徑名。如果您將腳本移動(dòng)到 Path 中的目錄,那么只需鍵入它的名字就可以運(yùn)行。具體的 Path 取決于您的 UNIX 實(shí)現(xiàn)和本地設(shè)置,但 Path 中的目錄通常包括 /bin、/usr/bin 和 /usr/local/bin。
一些用戶對(duì)它們的 Shell 進(jìn)行配置,從而使 PATH 變量包括當(dāng)前的工作目錄,這在 Path 中以點(diǎn)字符 (".") 表示。如此一來,要在當(dāng)前目錄下運(yùn)行腳本,只需要鍵入它的名稱,不需要指出相對(duì)目錄。,Shell 按給定的順序搜索 Path中的目錄,從而避免中木馬或發(fā)生異常情況,一種極其不明智的做法是把當(dāng)前工作目錄放在 Path 的末尾。
要查看您的 Path,可以使用 echo 顯示 PATH 變量的內(nèi)容,如清單 4 所示。
清單 4. 更改 PATH
$ echo $PATH/usr/local/bin:/usr/bin:/bin:/usr/bin/X11$ myscriptmyscript: command not found$ PATH = $PATH":."$ echo $PATH/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:.$ myscriptThe message is 'Hello, world!'$
在解釋器名稱的后面可以附加特殊選項(xiàng)或標(biāo)志,比如 /usr/bin/bsh -n,這用于調(diào)試目的。連字符關(guān)閉選項(xiàng),加號(hào)則打開選項(xiàng)。特殊的內(nèi)置環(huán)境變量 -(一個(gè)連字符)包含當(dāng)前 Shell 的完整選項(xiàng)列表。
嘗試在您當(dāng)前的交互式 Shell 中設(shè)置了哪些選項(xiàng)。通過使用 echo 顯示 - 變量的內(nèi)容來完成這項(xiàng)任務(wù):
$ echo $-himBH$
參考您使用的 Shell 的 man 頁面來獲取當(dāng)前的標(biāo)志和選項(xiàng)列表。表 1 提供了 AIX® 上的 Bourne Shell 的常用標(biāo)志列表,以及對(duì)每種標(biāo)志作用的簡要說明。
表 1. AIX Bourne Shell 的常用選項(xiàng)
標(biāo)志描述-a 導(dǎo)出所有已分配值的變量。-c Variable 執(zhí)行從變量 中讀取的命令。-e 當(dāng)命令滿足以下條件之一時(shí)立即退出:命令退出時(shí)返回比 0 大的值;命令不是 while、until 或 if 結(jié)構(gòu)的一部分;命令不經(jīng)過 AND 或 OR 檢測;或者命令不是管道前加感嘆號(hào)。-f 禁用所有文件名替換。-h 定義函數(shù)時(shí),定位和記住函數(shù)內(nèi)部調(diào)用的所有命令。-i 指定交互式 Shell。-k 將所有關(guān)鍵字 都放入命令的環(huán)境。-n 讀取命令,但是不執(zhí)行它們。-r 調(diào)用受限制的 Shell。-s 從標(biāo)準(zhǔn)輸入讀取命令,然后將輸出寫入標(biāo)準(zhǔn)錯(cuò)誤(不包括 Shell 內(nèi)置命令的輸出)。-t 讀取并執(zhí)行單個(gè)命令,然后退出。-u 在腳本中,將所有未定義 變量視為錯(cuò)誤。當(dāng)嘗試變量替換時(shí)退出。-v 當(dāng)讀取輸入行時(shí)將其顯示出來。-x 在執(zhí)行命令之前顯示其完整命令(包括所有的參數(shù)和選項(xiàng))。
Shell 運(yùn)算和進(jìn)制轉(zhuǎn)換
Shell 提供大量的基本運(yùn)算操作,在腳本中非常有用。Shell 對(duì)您提供的算術(shù)表達(dá)式求值,執(zhí)行運(yùn)算展開式,此時(shí)使用得出的結(jié)果替換表達(dá)式。以下面的格式提供運(yùn)算表達(dá)式:
$(( expression ))
您可以使用 echo 在命令行顯示運(yùn)算展開式的結(jié)果,了解其工作情況。現(xiàn)在嘗試清單 5 所顯示的結(jié)果。
清單 5. Bourne Shell 中的運(yùn)算展開式
$ echo $((10+40))50$ echo $((5*(3+3)))30
您還可以將展開式分配給變量。嘗試清單 6 所顯示的結(jié)果。
清單 6. 將運(yùn)算展開式分配給 Shell 變量
$ myvar = 10$ echo $myvar10$ echo $(($myvar-2))8$ myvar = $(($myvar+5))$ echo $myvar15$ result = $(($myvar-10))$ echo $result5$
表 2 列出了在大多數(shù) Bourne 以及與 Bourne 兼容的 Shell中可以使用的運(yùn)算符。正如上面第二個(gè)示例,使用圓括號(hào)括起來的語句有更高的優(yōu)先級(jí)。實(shí)際上,Shell 算術(shù)優(yōu)先級(jí)通常根據(jù) C 語言的規(guī)則來確定。
表 2. Shell 條件表達(dá)式
運(yùn)算符描述+ 加- 減* 乘/ 除% 求余< 小于(1 代表真,0 代表假)<= 小于等于(1 代表真,0 代表假)> 大于(1 代表真,0 代表假)>= 大于等于(1 代表真,0 代表假)<< 按位向左移位:將給定的整數(shù)或第一個(gè)表達(dá)式向左移動(dòng)第二個(gè)表達(dá)式表示的位數(shù)>> 按位向右移位:將給定的整數(shù)或第一個(gè)表達(dá)式向右移動(dòng)第二個(gè)表達(dá)式表示的位數(shù)
使用 Shell 運(yùn)算進(jìn)行進(jìn)制轉(zhuǎn)換
假定在您的腳本中有一些數(shù)字,您需要以另外的進(jìn)制處理這些數(shù)字。使用 Shell 運(yùn)算可以很容易地自動(dòng)實(shí)現(xiàn)這類轉(zhuǎn)換。一種情況是使用 Shell 運(yùn)算把一個(gè)數(shù)字從給定的進(jìn)制轉(zhuǎn)換位十進(jìn)制。如果數(shù)字以運(yùn)算展開式的形式提供,那么假定它帶有十進(jìn)制符號(hào),除非 它前面帶有 0(這種情況假定是八進(jìn)制)或 0x(這種情況假定是十六進(jìn)制)。鍵入以下內(nèi)容以得到一些八進(jìn)制和十六進(jìn)制值的十進(jìn)制輸出:
$ echo $((013))$ echo $((0xA4))
您還可以使用以下格式指定 2 到 64 之間的任意進(jìn)制:
$((BASE#NUMBER))
通過在 Shell 提示符后鍵入清單 7 中所示的行,嘗試將二進(jìn)制、八進(jìn)制、十六進(jìn)制以及其他進(jìn)制的數(shù)轉(zhuǎn)換為十進(jìn)制。
清單 7. 在 Shell 中將任意進(jìn)制的數(shù)以十進(jìn)制輸出
echo $((2#1101010))echo $((8#377))echo $((16#D8))echo $((12#10))echo $((36#ZZYY))
使用 bc 進(jìn)行進(jìn)制轉(zhuǎn)換
在 Shell 中進(jìn)行進(jìn)制轉(zhuǎn)換的另一個(gè)訣竅是使用 bc,它是一種任意精度運(yùn)算語言,大多數(shù) UNIX 安裝程序都提供。因?yàn)樗试S您指定輸出進(jìn)制,所以當(dāng)您需要以十進(jìn)制以外的進(jìn)制輸出時(shí),這是一種很好的技術(shù)。
bc 的特殊變量 ibase 和 obase 分別包含用于輸入和輸出的進(jìn)制的值。缺省情況下,都被設(shè)置為 10。要執(zhí)行進(jìn)制轉(zhuǎn)換,需要改變其中的一個(gè)或兩個(gè)值,然后提供一個(gè)數(shù)字。立即嘗試,如清單 8 中所示。
清單 8. 使用 bc 執(zhí)行進(jìn)制轉(zhuǎn)換
$ bc -ql1010obase=1610Aibase=2102Control-D$
要快速執(zhí)行進(jìn)制轉(zhuǎn)換,可以聯(lián)合使用 bc 和 echo形成快捷的單命令行程序,將給定的值通過管道傳輸給 bc。鍵入清單 9 中顯示的內(nèi)容。
清單 9. Shell 單命令行 bc 程序
$ echo 'obase=16; 47' | bc2F$ echo 'obase=10; ibase=16; A03' | bc2563$
警告:當(dāng)您設(shè)置 bc 的輸入進(jìn)制以后,輸入 bc 的所有數(shù)字都使用該進(jìn)制,包括您提供用于設(shè)置輸出進(jìn)制的數(shù)字。因此最好先設(shè)置輸出進(jìn)制,否則可能會(huì)產(chǎn)生意想不到的結(jié)果,如清單 10 中所示。
清單 10. 設(shè)置輸入和輸出進(jìn)制的先后順序的重要性
$ echo 'ibase=16; obase=10; A' | bcA$ echo 'ibase=16; obase=A; A' | bc10$
內(nèi)聯(lián)輸入
盡管 echo 通過管道將內(nèi)容傳遞給交互式命令(比如 bc)可以生成快捷的單命令行程序,但是它對(duì)于多行輸入并不適用,比如可能用到實(shí)際文件中的內(nèi)容。但是另外一種有用的方法可以完成這個(gè)任務(wù)。Shell 有一種工具稱為 here documents 或內(nèi)聯(lián)輸入,這是一種動(dòng)態(tài)構(gòu)建文件的非常好的方法,比如用于腳本內(nèi)部,并且將該文件的內(nèi)容重定向到一個(gè)命令。
使用 Shell << 操作符來指定一個(gè) here document,然后在同一行的后面跟上一個(gè)限定字符串,該字符串標(biāo)記輸入的結(jié)束,并且您可以選擇任何文本,只要是不包含空格字符的單個(gè)詞都可以。其后跟隨構(gòu)成您的輸入文件的行,然后以獨(dú)占一行的限定字符串結(jié)束輸入,在它的前面或后面不能有任何文本,否則該行將被視為輸入的一部分。使用 cat 進(jìn)行嘗試,如清單 11 中所示。
清單 11. 編寫 here document
$ cat << END> END of input text> ENDspace> This is still not the END> ENDING SOON> THE END> ENDEND of input textENDThis is still not the ENDENDING SOONTHE END$
限定字符串(本例中是 END)可以出現(xiàn)在輸入的任何地方,只有當(dāng)它以獨(dú)占一行并且不含空格或其他字符的形式出現(xiàn)時(shí),才表示輸入的結(jié)束。
腳本中的內(nèi)聯(lián)輸入
在腳本中經(jīng)常使用內(nèi)聯(lián)輸入將使用信息輸出到標(biāo)準(zhǔn)輸出。這通常通過將 here document 發(fā)送給 cat 來完成,如清單 12 中的腳本所示。使用 vi 輸入該腳本并保存到名為 baseconv 的文件中,并且將該文件設(shè)置為可執(zhí)行文件(請(qǐng)參見創(chuàng)建 Shell 腳本部分)。
清單 12. 使用 here document 提供 Shell 腳本使用信息
#!/bin/shcat << EOFbaseconv is a program to convert a number from one base to another.Usage: baseconv [options]Options:-iBASE input base-oBASE output base-hdisplay this messageFor more information, consult the baseconv man page.EOF
當(dāng)執(zhí)行該腳本時(shí),here document 的內(nèi)容被發(fā)送到(使用 cat)標(biāo)準(zhǔn)輸出。立即嘗試,如清單 13 中所示。
清單 13. 從 here document 輸出 Shell 腳本使用信息
$ baseconvbaseconv is a program to convert a number from one base to another.Usage: baseconv [options]Options:-iBASE input base-oBASE output base-hdisplay this messageFor more information, consult the baseconv man page.$
此外,Bourne Shell 的大多數(shù)實(shí)現(xiàn)允許出現(xiàn)使用可選的連字符重定向的內(nèi)聯(lián)輸入。可選的連字符將所有的前導(dǎo) Tab 字符從所有輸入行的前面去掉,也包括包含限定字符串的行。這對(duì)于您希望讓編寫的腳本保持當(dāng)前縮進(jìn)時(shí)會(huì)有幫助。由于內(nèi)聯(lián)輸入通常逐字讀取,并且限定字符串必須在行的開始處給出,因此輸入將打亂您的當(dāng)前縮進(jìn)并使腳本看起來不雅觀。因此,您可以重寫清單 12 中的腳本,使其與清單 14 一致,而輸出不會(huì)改變。
清單 14. 帶前導(dǎo)縮進(jìn)的 Shell 腳本 here document
#!/bin/shcat <<- EOFbaseconv is a program to convert a number from one base to another.Usage: baseconv [options]Options:-iBASE input base-oBASE output base-hdisplay this messageFor more information, consult the baseconv man page.EOF
在命令行使用內(nèi)聯(lián)輸入
在命令行中,使用調(diào)用交互式程序的單命令行程序進(jìn)行內(nèi)聯(lián)輸入,比如在使用 bc 進(jìn)制轉(zhuǎn)換部分討論的 bc 計(jì)算程序。在任意交互式命令中,您可以使用 here document 代替實(shí)際文件,或代替任意行的實(shí)際輸入。
嘗試使用 here document 將多行輸入發(fā)送到 bc。鍵入清單 15 中顯示的內(nèi)容。
清單 15. 將內(nèi)聯(lián)輸入發(fā)送到交互式程序
$ bc << EOF> ibase=16> A> EOF10$
通常使用內(nèi)聯(lián)輸入來擴(kuò)展變量。嘗試清單 16 中顯示的內(nèi)容。
清單 16. 內(nèi)聯(lián)輸入如何擴(kuò)展變量
$ BASECON=16$ bc << EOF> ibase=16> $BASECON> EOF22$
Subshell 執(zhí)行
可以在一個(gè)名為 subshell 的新 Shell 中執(zhí)行一個(gè)或一組命令,當(dāng)前 Shell 是 SubShell 的父 Shell。Subshell 繼承父親的環(huán)境。I/O 重定向可以出現(xiàn)在子 Shell 和父 Shell 之間,但是 Subshell 永遠(yuǎn)不能修改父環(huán)境。當(dāng)您為了執(zhí)行這些命令(比如設(shè)置變量)要更改 Shell 的環(huán)境,并且不想更改腳本自身運(yùn)行所在的環(huán)境時(shí),這就是您所期望的技術(shù)。當(dāng)您想要同時(shí)在后臺(tái)啟動(dòng)多個(gè)長時(shí)間運(yùn)行的進(jìn)程時(shí)也最好使用 Subshell。一個(gè) Shell 可以生成多個(gè) Subshell,而 Subshell 又可以循環(huán)生成屬于它們自身的任意數(shù)量的 Subshell。圖 1 說明了這個(gè)過程。
圖 1. Subshell 如何與它的父 Shell 交互
Shell 有時(shí)自動(dòng)生成自身的 Subshell,比如在管道中使用內(nèi)置命令時(shí)。在 Subshell 中,Shell $ 參數(shù)擴(kuò)展到父 Shell 而不是 Subshell 的進(jìn)程 ID (PID)。
在 Subshell 中運(yùn)行命令
要在 Subshell 中運(yùn)行一組命令,可以使用括號(hào)將其括起來。您可以使用重定向?qū)⑤斎氚l(fā)送到 Subshell 的標(biāo)準(zhǔn)輸入,或?qū)?Subshell 的集合輸出發(fā)送到文件或管道。
嘗試在您的 home 目錄鍵入清單 17 中顯示的內(nèi)容。該示例創(chuàng)建一個(gè) example 目錄和一些測試文件,前提是原來不存在 example 目錄。
清單 17. 在 Subshell 中創(chuàng)建一組文件
$ pwd/home/user$ (mkdir example; cd example; touch A B C)$ pwd/home/user$ cd example; lsA B C$ pwd/home/user/example$
在本例中,Shell 生成一個(gè)在后臺(tái)運(yùn)行的 Subshell,建立 example 目錄,然后使用 touch 在該目錄中生成三個(gè)虛擬文件。同時(shí),Shell 返回 home 目錄的命令行。
當(dāng)您有一組執(zhí)行時(shí)間長的命令時(shí),在命令行和腳本中使用 Subshell 都很方便。為了讓 Shell 保持空閑,您可以在后臺(tái)運(yùn)行 Subshell,或者在后臺(tái)運(yùn)行許多個(gè) Subshell。
( group-of-long-running-commands ) &( another-group-of-long-running-commands ) &( yet-another-group-of-long-running-commands ) &
Subshell 和變量
理解變量與 Subshell 的交互方式非常重要。因?yàn)?Subshell 環(huán)境是其父親的副本,所以它繼承了父親的所有變量。但是父 Shell 從不會(huì)看到 Subshell 環(huán)境發(fā)生的任何變化,同樣,Subshell 生成以后,再也不會(huì)看到父親發(fā)生的任何變化。
作為示例,使用 vi 編輯器將清單 18 中的腳本保存到 home 目錄的 vartest 文件中,然后將其設(shè)置為可執(zhí)行(請(qǐng)參見編寫 shell 腳本部分)。
清單 18. 演示 Subshell 中變量行為的 Shell 腳本
#!/bin/sh# Demonstrates variable behavior in a subshell environmentVAR=10echo "VAR is" $VAR(echo "In the subshell, VAR is still" $VARVAR=$(($VAR+5))echo "The new value of VAR in the subshell is" $VAR)echo "Outside of the subshell, VAR is" $VAR
現(xiàn)在嘗試通過鍵入腳本的名稱來執(zhí)行它,如清單 19 中所示。
清單 19. vartest 腳本的輸出
$ vartestVAR is 10In the subshell, VAR is still 10The new value of VAR in the subshell is 15Outside of the subshell, VAR is 10$
連續(xù)循環(huán)
現(xiàn)在來看循環(huán),它允許您執(zhí)行重復(fù)任務(wù),比如對(duì)一組文件執(zhí)行一些操作或命令。Shell 有幾種構(gòu)造循環(huán)的方法。
構(gòu)造 for 循環(huán)
最常見的循環(huán)結(jié)構(gòu)是 for 循環(huán)。首先定義一個(gè)變量作為循環(huán)的名稱,提供一組成員,可以是包括整數(shù)和文件名在內(nèi)的任何單詞,然后提供每次重復(fù)執(zhí)行的命令。每個(gè)命令都以分號(hào)結(jié)束 (;),整個(gè)命令組以位于單詞 do 和 done 之間。清單 20 描述了它的結(jié)構(gòu)。
清單 20. Shell 中循環(huán)的結(jié)構(gòu)
for loopname in membersdocommand;command;...command;done
在循環(huán)的第一次重復(fù)中,loopname 變量獲取第一個(gè)成員的值。然后 loopname 的值被清單中下一個(gè)成員的值替代,接下來它繼續(xù)重復(fù)直到遍歷所有成員。
在大多數(shù) Shell 中,do 和 done 都可以被大括號(hào)所替代,如清單 21 中所示。
清單 21. Shell 循環(huán)的替代結(jié)構(gòu)
for loopname in members{command;command;...command;}
鍵入清單 22 中的文本來運(yùn)行包含三個(gè)成員的簡單循環(huán):
清單 22. 使用循環(huán)來改變變量的值
$ for i in 1 2 3> {> VAR = $(($VAR+$i))> echo $i:$VAR> }1:12:33:6$
針對(duì)目錄中的每個(gè)文件執(zhí)行命令
您可以使用循環(huán)針對(duì)給定的一組文件執(zhí)行一個(gè)或一組命令。如果您提供文件的名稱作為 for 循環(huán)的成員,那么循環(huán)按您提供名稱的順序在每個(gè)文件上執(zhí)行操作。您可以兩次提供同一個(gè)文件,循環(huán)將依次對(duì)該文件執(zhí)行操作。在您的 example 目錄中嘗試使用清單 23 中的文本執(zhí)行上述操作。
清單 23. 利用一組文件構(gòu)造循環(huán)
$ cd ~/example$ lsA B C$ for file in C B B C> {> echo $file> }CBBC$
要對(duì)同一目錄下的所有文件執(zhí)行操作,可以使用星號(hào) (*) 作為循環(huán)的唯一成員,如清單 24 中所示。Shell 將星號(hào)擴(kuò)展為目錄中的所有文件。然后,對(duì)于循環(huán)中您要對(duì)所有文件執(zhí)行的命令,使用 loopname 變量作為合適的參數(shù)或選項(xiàng)。
清單 24. 針對(duì)目錄中的所有文件執(zhí)行同一命令
$ lsA B C$ for file in *> {> mv $file $((0x$file))> }$
如果您正在運(yùn)行本教程中的所有示例,那么您的 example 目錄中的內(nèi)容應(yīng)該已改變:
$ ls10 11 12$
發(fā)生的情況是循環(huán)中的 mv 命令將文件的名稱從十六進(jìn)制值(通過在名稱的前面插入 0x 構(gòu)成)更改為與它相等的十進(jìn)制值。
構(gòu)造 while 循環(huán)
您可以構(gòu)造一種當(dāng)滿足某些條件就一直運(yùn)行的循環(huán)。使用 while 條件語句來實(shí)現(xiàn)這一目標(biāo),其格式如清單 25 所示。
清單 25. Shell while 循環(huán)的結(jié)構(gòu)
while [ condition ]; docommand;command;...command;done
在循環(huán)中,condition 可以是使用操作符(請(qǐng)參見表 3)構(gòu)建的語句,或者可以像一個(gè)變量名那樣簡單。只要值是非 0 的,就代表真。
表 3. 常用 Shell 操作符
操作符描述-eq 等于-ne 不等于-lt 小于-le 小于等于-gt 大于-ge 大于等于
構(gòu)造 while 循環(huán)時(shí),有一些注意事項(xiàng)需要牢記在心。首先,在條件與將它括起來的括號(hào)之間必須留有空白字符。其次,如果在條件中將變量用于數(shù)字比較,那么在 while 語句之前必須首先定義該變量。
鍵入清單 26 中的文本以執(zhí)行一個(gè)簡短的 while 循環(huán):
清單 26. 使用 while 循環(huán)更改變量
$ VAR=0$ while [ $VAR -lt 10 ]; do> echo $VAR;> VAR=$(($VAR+1));> done0123456789$
構(gòu)造 until 循環(huán)
until 條件語句與 while 相似并使用相同的操作符,但是它們的行為相反。它只有當(dāng)條件為假時(shí)才執(zhí)行循環(huán),并且循環(huán)持續(xù)重復(fù)直到 給定的條件為真。它的格式在清單 27 中說明。
清單 27. Shell until 循環(huán)的結(jié)構(gòu)
until [ condition ] ; docommand;command;...command;done
通過鍵入清單 28 中所示的內(nèi)容嘗試運(yùn)行一個(gè)簡短的 until 循環(huán):
清單 28. 使用 until 循環(huán)更改變量
$ VAR=10$ until [ $VAR -eq 0 ]; do> echo $VAR;> VAR=$(($VAR-1));> done10987654321$
嵌套多重循環(huán)
您可以嵌套循環(huán)和組合多種類型的循環(huán)來執(zhí)行各種類型的復(fù)雜操作。由于 for 循環(huán)的成員不必是數(shù)字或以任意類型的順序排列,因此您可以使用稍后在某個(gè)內(nèi)部循環(huán)中作為命令執(zhí)行的命令名稱作為其成員,比如 printf、echo、stop、resume,等等。
嘗試運(yùn)行清單 29 中的示例。這是一個(gè)執(zhí)行算術(shù)替換的 until 循環(huán),同時(shí)嵌套在循環(huán)詞未按數(shù)字順序排列的 for 循環(huán)內(nèi)部。
清單 29. 使用嵌套循環(huán)進(jìn)行算術(shù)替換
$ for i in 250 100 2136 875> {>VAR=10;>until [ $VAR -eq 0 ]; do> echo "$i / $VAR = $(($i/$VAR)) $i * $VAR = $(($i*$VAR)) $i + $VAR = $(($i+$VAR)) $i - $VAR = $(($i-$VAR))";> VAR=$(($VAR-1);>done;> }250 / 10 = 25 250 * 10 = 2500 250 + 10 = 260 250 - 10 = 240250 / 9 = 27 250 * 9 = 2250 250 + 9 = 259 250 - 9 = 241250 / 8 = 31 250 * 8 = 2000 250 + 8 = 258 250 - 8 = 242250 / 7 = 35 250 * 7 = 1750 250 + 7 = 257 250 - 7 = 243250 / 6 = 41 250 * 6 = 1500 250 + 6 = 256 250 - 6 = 244250 / 5 = 50 250 * 5 = 1250 250 + 5 = 255 250 - 5 = 245250 / 4 = 62 250 * 4 = 1000 250 + 4 = 254 250 - 4 = 246250 / 3 = 83 250 * 3 = 750 250 + 3 = 253 250 - 3 = 247250 / 2 = 125 250 * 2 = 500 250 + 2 = 252 250 - 2 = 248250 / 1 = 250 250 * 1 = 250 250 + 1 = 251 250 - 1 = 249100 / 10 = 10 100 * 10 = 1000 100 + 10 = 110 100 - 10 = 90100 / 9 = 11 100 * 9 = 900 100 + 9 = 109 100 - 9 = 91100 / 8 = 12 100 * 8 = 800 100 + 8 = 108 100 - 8 = 92100 / 7 = 14 100 * 7 = 700 100 + 7 = 107 100 - 7 = 93100 / 6 = 16 100 * 6 = 600 100 + 6 = 106 100 - 6 = 94100 / 5 = 20 100 * 5 = 500 100 + 5 = 105 100 - 5 = 95100 / 4 = 25 100 * 4 = 400 100 + 4 = 104 100 - 4 = 96100 / 3 = 33 100 * 3 = 300 100 + 3 = 103 100 - 3 = 97100 / 2 = 50 100 * 2 = 200 100 + 2 = 102 100 - 2 = 98100 / 1 = 100 100 * 1 = 100 100 + 1 = 101 100 - 1 = 992136 / 10 = 213 2136 * 10 = 21360 2136 + 10 = 2146 2136 - 10 = 21262136 / 9 = 237 2136 * 9 = 19224 2136 + 9 = 2145 2136 - 9 = 21272136 / 8 = 267 2136 * 8 = 17088 2136 + 8 = 2144 2136 - 8 = 21282136 / 7 = 305 2136 * 7 = 14952 2136 + 7 = 2143 2136 - 7 = 21292136 / 6 = 356 2136 * 6 = 12816 2136 + 6 = 2142 2136 - 6 = 21302136 / 5 = 427 2136 * 5 = 10680 2136 + 5 = 2141 2136 - 5 = 21312136 / 4 = 534 2136 * 4 = 8544 2136 + 4 = 2140 2136 - 4 = 21322136 / 3 = 712 2136 * 3 = 6408 2136 + 3 = 2139 2136 - 3 = 21332136 / 2 = 1068 2136 * 2 = 4272 2136 + 2 = 2138 2136 - 2 = 21342136 / 1 = 2136 2136 * 1 = 2136 2136 + 1 = 2137 2136 - 1 = 2135875 / 10 = 87 875 * 10 = 8750 875 + 10 = 885 875 - 10 = 865875 / 9 = 97 875 * 9 = 7875 875 + 9 = 884 875 - 9 = 866875 / 8 = 109 875 * 8 = 7000 875 + 8 = 883 875 - 8 = 867875 / 7 = 125 875 * 7 = 6125 875 + 7 = 882 875 - 7 = 868875 / 6 = 145 875 * 6 = 5250 875 + 6 = 881 875 - 6 = 869875 / 5 = 175 875 * 5 = 4375 875 + 5 = 880 875 - 5 = 870875 / 4 = 218 875 * 4 = 3500 875 + 4 = 879 875 - 4 = 871875 / 3 = 291 875 * 3 = 2625 875 + 3 = 878 875 - 3 = 872875 / 2 = 437 875 * 2 = 1750 875 + 2 = 877 875 - 2 = 873875 / 1 = 875 875 * 1 = 875 875 + 1 = 876 875 - 1 = 874$
讀取鍵盤輸入
您還可以在腳本中或從命令行本身讀取鍵盤輸入。使用 read 命令可以實(shí)現(xiàn)這一功能,這是一個(gè)內(nèi)置函數(shù),將任意數(shù)量的變量名作為參數(shù)。它從標(biāo)準(zhǔn)輸入讀取變量的值,讀入單行輸入并將各個(gè)輸入詞分配給各個(gè)變量。
嘗試讀取一個(gè)變量,如清單 30 中所示:
清單 30. 使用 read 讀取一個(gè)變量
$ read VAR23$ echo $VAR23$
使用 -p 選項(xiàng)為每次 read 提供提示。使用以引號(hào)括起來的字符串提供提示,如清單 31 中所示。發(fā)生變量擴(kuò)展。
清單 31. 在變量讀取時(shí)使用提示
$ read -p "Instead of $VAR, what number would you like? " VARInstead of 23, what number would you like? 17$ echo $VAR17$
如果鍵盤輸入的詞比變量個(gè)數(shù)多,那么依次為變量分配輸入的詞,到最后一個(gè)變量時(shí),為其分配輸入行余下的部分。(如果輸入的詞比變量個(gè)數(shù)少,那么為變量分配值直到所有的輸入都已分配,然后為所有剩余的變量分配空值。)
在循環(huán)中讀取
您可以在循環(huán)中使用 read 作為條件表達(dá)式。現(xiàn)在使用清單 32 中的內(nèi)容嘗試這一操作:
清單 32. 在循環(huán)中讀取一組文件名
$ while read -p "File? " file; do ls $file; doneFile? 1010File? 1212File? 4242: no such file or DirectoryFile?Carriage return10 11 12File?Control-C$
此技術(shù)通常在對(duì)循環(huán)的輸入使用管道時(shí)使用。嘗試鍵入清單 33 中的文本,該文本使用循環(huán)替代 ls 命令的輸出:
清單 33. 從管道讀取
$ ls | while read file; do ls $file; done101112$
您還可以跨多行操作變量,比如將一條消息發(fā)送到標(biāo)準(zhǔn)輸出,然后對(duì) loopname 變量執(zhí)行 Shell 運(yùn)算(請(qǐng)參見 Shell 運(yùn)算和進(jìn)制轉(zhuǎn)換部分)。嘗試清單 34 中提供的示例:
清單 34. 使用管道讀取的較長循環(huán)
$ ls | while read file; do echo "The file is " `ls -i $file`;echo "If the number were in hex, the value would be $((16#$file))"doneThe file is 100267120 10If the number were in hex, the value would be 16The file is 100267121 11If the number were in hex, the value would be 17The file is 100267122 12If the number were in hex, the value would be 18$
您可以在一個(gè)管道輸入的 read 中讀取多個(gè)值,如清單 35 中所示。
清單 35. 從一個(gè)管道讀取多個(gè)變量
$ ls -i | while read inode file; doecho "File $file has inode $inode"doneFile 10 has inode 100267120File 11 has inode 100267121File 12 has inode 100267122$
實(shí)際運(yùn)用
此結(jié)束部分將您在前面學(xué)到的訣竅和技術(shù)加以組合來實(shí)現(xiàn)在實(shí)際中有用的單命令行程序。它還包括一個(gè)簡單的 Shell 腳本——執(zhí)行任意進(jìn)制的轉(zhuǎn)換。
有用的單命令行程序
以下示例是執(zhí)行有用功能的 Shell 單命令行程序樣本。它們?nèi)坑杀窘坛讨忻枋龅母鞣N結(jié)構(gòu)組成。
從當(dāng)前目錄中獲取一組文件名恰好為兩個(gè)字符長的文件,并使用 .ppm 擴(kuò)展名為其重新命名:
for i in ??; { mv $i $i.ppm; }
使用 tar 和 Subshell 復(fù)制整個(gè)目錄樹,同時(shí)保持相同的文件權(quán)限:
( cd source ; tar pcf - * ) | ( cd target ; tar pxvf - )
讀取二進(jìn)制數(shù)并以十進(jìn)制輸出值:
read BINLOC;echo $((2#$BINLOC))
在 /usr/local 目錄樹中找到所有帶 .MP3 擴(kuò)展名的文件(這些文件的名稱中可能包含空格字符),然后使用 bzip2 實(shí)用程序壓縮這些文件:
find /usr/local -name "*.mp3" | while read name ; do bzip2 $name; done
將給定文件中所有十進(jìn)制數(shù)的值以十六進(jìn)制輸出:
cat file | while read number ; do echo $((0x$number)); done
將給定文件中所有十進(jìn)制數(shù)轉(zhuǎn)換為十六進(jìn)制的值,并將值輸出到帶有 .hex 擴(kuò)展名的新文件中:
cat file | while read number ; do echo $((0x$number)) >> file.hex; done
構(gòu)造重復(fù)十次的循環(huán),以數(shù)字(從 0 到 90 以 10 遞增)作為傳遞的參數(shù)運(yùn)行 command:
i=0; while [ $i -ne 100 ]; do command $i; i=$(($i+10)); done
示例腳本:將數(shù)字轉(zhuǎn)換為其他進(jìn)制
本教程中討論的一些訣竅在清單 36 中被組合在一起。它是一個(gè)示例腳本——baseconv,將數(shù)字從給定的輸入進(jìn)制轉(zhuǎn)換為輸出進(jìn)制。為它提供輸入進(jìn)制和輸出進(jìn)制的值作為參數(shù),然后它從鍵盤輸入讀取數(shù)字,直到讀取了數(shù)字 0。
清單 36. 轉(zhuǎn)換進(jìn)制的簡單腳本
#!/bin/sh# baseconv, convert numbers from one base to another.#NUMBER=1while [ $NUMBER ]; doread -p "Input base: " INread -p "Output base: " OUTread -p "Number: " NUMBERbc -ql <<- EOFobase=$OUTibase=$IN$NUMBEREOFdone
當(dāng)您把它保存到可執(zhí)行文件后(請(qǐng)參見創(chuàng)建 Shell 腳本部分),嘗試運(yùn)行該文件,如清單 37 中所示:
清單 37. baseconv 腳本的輸出
$ ./baseconvInput base: 10Output base: 16Number: 3321Input base: 2Output base: 1100Number: 1015Input base: 16Output base: ANumber: ACA2762Input base: 10Output base: 10Number:Carriage return$
結(jié)束語
總結(jié)
噢!本教程確實(shí)涵蓋了許多內(nèi)容,帶您快速瀏覽了基本的 Shell 編程概念。在學(xué)習(xí)本教程的過程中,您了解了有關(guān) Shell 編程的許多核心概念:連續(xù)循環(huán)、內(nèi)聯(lián)輸入、讀取鍵盤輸入、進(jìn)制轉(zhuǎn)換及 Subshell 執(zhí)行。您還了解到 Shell 代碼片段如何能夠作為單命令行程序直接從 Shell 提示符上運(yùn)行,以及如何將它們放在同一個(gè)文件中作為可執(zhí)行腳本。您可以從中學(xué)習(xí)一些最重要的腳本編程概念。您如果能綜合運(yùn)用在本教程以及本系列教程的前面部分學(xué)到的知識(shí),那么您已成功地邁上 UNIX 專家之路。
