|
Shell 定制
本節(jié)介紹了初級管理(LPIC-1)考試 102 的 1.109.1 主題的內(nèi)容,。這個主題的權值為 5,。
在本節(jié)中,我們將學習如何:
- 設置并取消環(huán)境變量
- 使用配置文件在登錄或派生新 shell 時設置環(huán)境變量
- 對經(jīng)常使用的命令序列編寫 shell 函數(shù)
- 使用命令列表
Shell 和環(huán)境
在出現(xiàn)圖形界面之前,,程序員都是使用打字機終端或 ASCII 顯示終端連接到 UNIX® 系統(tǒng)的,。用戶可以使用打字機終端輸入命令,輸出結(jié)果通常會被打印到連續(xù)的紙張上,。大部分 ASCII 顯示終端都是每行 80 個字符,,每屏 25 行,不過也有比這更大或更小的終端,。程序員輸入一條命令并按下回車鍵之后,,系統(tǒng)就會解釋并執(zhí)行這條命令。
盡管在當今這個使用拖拽式圖形界面的時代,,這一切看起來似乎太過原始,,但是與原來編寫程序、打卡,、對卡迭(card deck)進行匯編并運行程序的方式相比,,這已經(jīng)是非常大的一個進步了。隨著編輯器的出現(xiàn),,程序員甚至可以作為卡像來創(chuàng)建程序,,并在終端會話中編譯程序。
在終端中輸入的字節(jié)流向 shell 提供了一個標準輸入流,shell 返回的字符流可以打印到紙上,,也可以顯示到標準輸出 上,。
接受并執(zhí)行命令的程序稱為 shell。它位于您和操作系統(tǒng)之間,。UNIX shell 和 Linux shell 的功能都非常強大,,可以通過組合一些基本的函數(shù)來構造非常復雜的操作,。通過使用編程結(jié)構則可以構建一些函數(shù)在 shell 中直接執(zhí)行,,或者將這些函數(shù)保存成 shell 腳本 的形式,這樣就可以一次次重用這些函數(shù)了,。
有時需要在系統(tǒng)引導之前就執(zhí)行一些命令,,以便能夠進行終端連接;有時又需要周期性地執(zhí)行命令,,而不管您登錄與否,。shell 可以為您完成這些功能。標準輸入和輸出并不需要來自于(或定向到)終端處的真實用戶,。
在本節(jié)中,,將學習更多有關 shell 的內(nèi)容。具體來說,,您將學習有關 bash(又稱為 Bourne again)shell 的內(nèi)容,,它是對原來 Bourne shell 的一個增強,另外還提供了其他 shell 所具有的一些特性,,以及對 Bourne shell 所做的一些更改以使其更加兼容 POSIX,。
POSIX 是 Portable Operating System Interface for uniX 的簡稱,它是一系列 IEEE 標準,,總稱為 IEEE 1003,。這些標準中的第一個標準是 IEEE Standard 1003.1-1988,它是在 1988 年發(fā)布的,。其他知名的 shell 包括 Korn shell(ksh),、C shell(csh)及其派生產(chǎn)品 tcsh、Almquist shell(ash)及其 Debian 派生產(chǎn)品(dash),。一些腳本常常需要用到上述某個 shell 的特性,,所以要對這些 shell 有一些了解。
您與計算機的很多交互特性在這些會話中都是相同的,?;叵胍幌略诮坛?“LPI 101 考試準備(主題 103):GNU 和 UNIX 命令” 中,當使用 Bash shell 時,,就擁有了一個 shell 環(huán)境,,它定義了很多內(nèi)容,例如提示符格式、主目錄,、工作目錄,、shell 名、已經(jīng)打開的文件,、已經(jīng)定義的函數(shù)等,。每個 shell 進程都可以使用這個環(huán)境。shell(包括 bash)讓您可以創(chuàng)建并修改 shell 變量,,并可以將其導出 到環(huán)境中由在 shell 中運行的其他進程或從當前 shell 中派生的其他 shell 使用,。
環(huán)境變量和 shell 變量都有名稱。您可以通過在變量名前加上一個 ‘$‘ 符號來引用變量的值,。一些常用的 bash 變量如表 3 所示,。
表 3. 常用 bash 環(huán)境變量
變量名 |
功能 |
USER |
已登錄用戶的用戶名 |
UID |
已登錄用戶的數(shù)字用戶 id |
HOME |
用戶的主目錄 |
PWD |
當前工作目錄 |
SHELL |
shell 名 |
$ |
進程 id(或正在運行的 Bash shell 進程或其他進程的 PID) |
PPID |
啟動這個進程的進程的進程 id (即父進程的 id) |
|
上一個命令的退出碼 |
設置變量
在 Bash shell 中,可以通過在一個名字后面緊跟上一個等號(=)來創(chuàng)建或設置 shell 變量,。變量名(或標識符)是由字符,、數(shù)字和下劃線構成的單詞,它只能由字符或下劃線開頭,。變量是大小寫敏感的,,例如 var1 和 VAR1 是不同的兩個變量。按照慣例,,變量 —— 尤其是導出后的變量 —— 都采用大寫,,不過這并不是硬性要求。通常,,$$ 和 $? 是 shell 參數(shù),,而不是變量。它們只能被引用,;無法對它們進行賦值,。
在創(chuàng)建 shell 變量時,通常都會希望將該變量導出 到環(huán)境中,,這樣從這個 shell 中啟動的其他進程也都可以使用該變量了,。但所導出的變量對父 shell 不可用??梢允褂?export 命令導出一個變量名,。在 bash 中,可以在一個步驟中完成賦值和導出,。
為了展示賦值和導出操作,,讓我們在 Bash shell 中運行 bash 命令,然后在這個新 Bash shell 中在運行 Korn shell(ksh),。我們會使用 ps 命令來顯示有關正在運行的命令的信息,。
清單 1. 設置并導出 shell 變量
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
30576 30575 -bash
[ian@echidna ian]$ bash
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
16353 30576 bash
[ian@echidna ian]$ VAR1=var1
[ian@echidna ian]$ VAR2=var2
[ian@echidna ian]$ export VAR2
[ian@echidna ian]$ export VAR3=var3
[ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3
var1 var2 var3
[ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $SHELL
var1 var2 var3 /bin/bash
[ian@echidna ian]$ ksh
$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
16448 16353 ksh
$ export VAR4=var4
$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL
var2 var3 var4 /bin/bash
$ exit
$ [ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL
var1 var2 var3 /bin/bash
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
16353 30576 bash
[ian@echidna ian]$ exit
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd"
PID PPID CMD
30576 30575 -bash
[ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL
/bin/bash
|
注意:
- 在這些操作開始時,,Bash shell 的 PID 是 30576。
- 第二個 Bash shell 的 PID 是 16353,,其父 shell 的 PID 是 30576,,也就是原來的 Bash shell。
- 我們在第二個 Bash shell 中創(chuàng)建了 VAR1,、VAR2 和 VAR3 三個變量,,但是只導出了 VAR2 和 VAR3。
- 在 Korn shell 中,,我們創(chuàng)建了 VAR4,。
echo 命令只顯示了 VAR2、VAR3 和 VAR4 的值,,這就證實了 VAR1 的確沒有導出,。看到提示符改變之后,,SHELL 變量的值卻還未改變,您會非常奇怪么,?通常不能總依賴 SHELL 來告訴您正在哪個 shell 下運行,,不過 ps 命令的確可以告訴您實際的命令。注意 ps 會在第一個 Bash shell 前面放上一個連字符(-)來說明這是一個登錄 shell,。
- 現(xiàn)在回到第二個 Bash shell 中,,我們可以看到 VAR1、VAR2 和 VAR3,。
- 最后,,當我們返回到原始的 shell 中時,新變量都不存在了,。
清單 2 顯示了在這些常用的 bash 變量中可以看到什么,。
清單 2. 環(huán)境和 shell 變量
[ian@echidna ian]$ echo $USER $UID
ian 500
[ian@echidna ian]$ echo $SHELL $HOME $PWD
/bin/bash /home/ian /home/ian
[ian@echidna ian]$ (exit 0);echo $?;(exit 4);echo $?
0
4
[ian@echidna ian]$ echo $$ $PPID
30576 30575
|
環(huán)境和 C shell
在諸如 C 和 tcsh shell 之類的 shell 中,可以使用 set 命令在 shell 中設置變量,,使用 setenv 命令來設置并導出變量,。清單 3 中給出的語法與 export 命令的語法稍有不同。請注意在使用 set 命令時使用的等號(=),。
清單 3. 在 C shell 中設置環(huán)境變量
ian@attic4:~$ echo $VAR1 $VAR2
ian@attic4:~$ csh
% set VAR1=var1
% setenv VAR2 var2
% echo $VAR1 $VAR2
var1 var2
% bash
ian@attic4:~$ echo $VAR1 $VAR2
var2
|
取消變量
可以使用 unset 命令從 Bash shell 中清除變量,。可以使用 -v 選項來確保刪除變量定義,。函數(shù)可以使用與變量相同的名字,,因此如果希望清除函數(shù)定義,就請使用 -f 選項,。在沒有使用 -f 或 -v 的情況下,,如果存在這樣一個變量,,那么 bash 的 unset 命令就會清除變量定義;否則,,如果存在這樣一個函數(shù),,這個命令就清除函數(shù)定義(函數(shù)將在后面的 Shell 函數(shù) 一節(jié)中更詳細地加以介紹)。
清單 4. bash unset 命令
ian@attic4:~$ VAR1=var1
ian@attic4:~$ VAR2=var2
ian@attic4:~$ echo $VAR1 $VAR2
var1 var2
ian@attic4:~$ unset VAR1
ian@attic4:~$ echo $VAR1 $VAR2
var2
ian@attic4:~$ unset -v VAR2
ian@attic4:~$ echo $VAR1 $VAR2
|
默認情況下,,bash 會將取消的變量視為該變量的值為空,,因此您可能會納悶為什么一定要取消變量,為什么不僅僅為其賦一個空值呢,。如果引用了未定義的變量,,Bash 和很多其他 shell 都會允許您生成一個錯誤。使用命令 set -u 可以針對引用未定義的變量的情況生成一個錯誤,,使用 set +u 可以禁用這種警告,,如清單 5 所示。
清單 5. 針對取消的變量生成錯誤
ian@attic4:~$ set -u
ian@attic4:~$ VAR1=var1
ian@attic4:~$ echo $VAR1
var1
ian@attic4:~$ unset VAR1
ian@attic4:~$ echo $VAR1
-bash: VAR1: unbound variable
ian@attic4:~$ VAR1=
ian@attic4:~$ echo $VAR1
ian@attic4:~$ unset VAR1
ian@attic4:~$ echo $VAR1
-bash: VAR1: unbound variable
ian@attic4:~$ unset -v VAR1
ian@attic4:~$ set +u
ian@attic4:~$ echo $VAR1
ian@attic4:~$
|
注意取消一個不存在的變量并不會產(chǎn)生錯誤,,即使在指定 set -u 時也是如此,。
配置文件
在登錄 Linux 系統(tǒng)時,您的 id 就有了一個默認 shell,,它就是您的登錄 shell,。如果這個 shell 是 bash,那么它就會在您控制系統(tǒng)之前先執(zhí)行幾個配置腳本,。如果存在 /etc/profile 文件,,就首先執(zhí)行這個文件。根據(jù)發(fā)行版的不同,,/etc 中的其他腳本也可能會執(zhí)行,,例如 /etc/bash.bashrc 或 /etc/bashrc。這些腳本運行之后,,如果主目錄中存在腳本,,該腳本也會被執(zhí)行。Bash 會按照 ~/.bash_profile,、~/.bash_login 和 ~/.profile 的順序來查找文件,。最先找到的文件會首先執(zhí)行。
當您登出系統(tǒng)時,,如果主目錄中存在 ~/.bash_logout 腳本,,bash 就會執(zhí)行它。
一旦登錄進系統(tǒng)并使用 bash,,您還可以啟動另外一個 shell(稱為交互式 shell)來運行命令,,例如在后臺運行命令。在這種情況中,,bash 只會執(zhí)行 ~/.bashrc 腳本(假設這個腳本存在),。通??梢允褂萌缜鍐?6 所示的命令在 ~/.bash_profile 檢查這個腳本,以便可以在登錄時或在啟動交互式 shell 時執(zhí)行它,。
清單 6. 檢查 ~/.bashrc
# include .bashrc if it exists
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
|
可以使用 --login 選項強制 bash 像登錄 shell 一樣讀取配置文件,。如果不希望執(zhí)行登錄 shell 的配置文件,可以指定 --noprofile 選項,。類似地,,如果希望對某個交互式 shell 不執(zhí)行 ~/.bashrc 文件,可以使用 --norc 選項來啟動 bash,。也可以通過指定 --rcfile 選項加上希望使用的文件名來強制 bash 使用 ~/.bashrc 之外的文件,。清單 7 展示了創(chuàng)建一個名為 testrc 的簡單文件并使用 --rcfile 選項來使用這個文件的例子。注意 VAR1 變量并不是 在外部 shell 中設置的,,而是通過 testrc 文件針對內(nèi)部 shell 設置的,。
清單 7. 使用 --rcfile 選項
ian@attic4:~$ echo VAR1=var1>testrc
ian@attic4:~$ echo $VAR1
ian@attic4:~$ bash --rcfile testrc
ian@attic4:~$ echo $VAR1
var1
|
以其他方式啟動 bash
除了前面介紹的這種在終端中運行 bash 的標準方法之外,bash 也可以通過其他方法加以使用,。
除非您引用(source) 腳本在當前 shell 中運行,,否則它就會在自己的非交互式 shell 中運行,上面的配置文件都不會被讀取,。然而,,如果設置了 BASH_ENV 變量,那么 bash 就會對這個值進行擴展,,并假設它是一個文件名。如果這個文件存在,,那么 bash 就會在非交互式 shell 中執(zhí)行任何腳本或命令之前先執(zhí)行這個文件,。清單 8 通過兩個簡單的文件展示了這一點。
清單 8. 使用 BASH_ENV
ian@attic4:~$ cat testenv.sh
#!/bin/bash
echo "Testing the environment"
ian@attic4:~$ cat somescript.sh
#!/bin/bash
echo "Doing nothing"
ian@attic4:~$ export BASH_ENV="~/testenv.sh"
ian@attic4:~$ ./somescript.sh
Testing the environment
Doing nothing
|
非交互式 shell 也可以使用 --login 選項啟動,,從而強制配置文件的執(zhí)行,。
Bash 也可以使用 --posix 選項以 POSIX 模式啟動。這種模式與非交互式 shell 非常類似,,只不過在這種模式下,,要執(zhí)行的文件是在 ENV 環(huán)境變量中設定的。
在 Linux 系統(tǒng)中常常會使用一個符號鏈接來以 /bin/sh 運行 bash,。當 bash 檢測到它正在以 sh 的名義運行時,,它就會試圖遵循老式 Bourne shell 的啟動行為,而同時又可以兼容 POSIX 標準,。當作為登錄 shell 運行時,,bash 會試圖讀取并執(zhí)行 /etc/profile 和 ~/.profile 文件。當使用 sh 命令作為一個交互式 shell 運行時,,bash 會試圖執(zhí)行由 ENV 變量指定的文件,,與在 POSIX 模式下被調(diào)用時一樣,。當作為 sh 交互運行時,它只 會使用由 ENC 變量指定的文件,;--rcfile 選項會一直被忽略,。
如果 bash 是由遠程 shell 守護進程調(diào)用的,那么它的行為就與交互式 shell 非常類似,,如果存在 ~/.bashrc 文件就會使用該文件,。
Shell 別名
Bash shell 允許為命令定義一些 別名。使用別名的最常見原因是為了給命令提供其他名字,,或者為命令提供一些默認參數(shù),。很多年以來,vi 編輯器一直都是 UNIX 和 Linux 系統(tǒng)上的一個主要工具,。vim(Vi IMproved)編輯器與 vi 非常類似,,不過有很多改進。因此如果您在使用編輯器時習慣于輸入 “vi”,,但是實際上卻更喜歡使用 vim,,那么您就可以借助于別名。清單 9 顯示了如何使用 alias 命令來實現(xiàn)這種功能,。
清單 9. 使用 vi 作為 vim 的別名
[ian@pinguino ~]$ alias vi=‘vim‘
[ian@pinguino ~]$ which vi
alias vi=‘vim‘
/usr/bin/vim
[ian@pinguino ~]$ /usr/bin/which vi
/bin/vi
|
注意在這個例子中,,如果使用 which 命令來查看 vi 程序的位置,那就會看到兩行輸出:第一個是別名,,第二個是 vim 的位置(/usr/bin/vim),。然而,如果使用完整路徑來執(zhí)行 which 命令(/usr/bin/which ),,就可以獲得 vi 命令的位置,。如果您猜測這可能意味著 which 命令本身在這個系統(tǒng)上就是一個別名,那么您就猜對了,。
可以使用 alias 命令來顯示所有的別名(如果沒使用任何選項,,或者只使用了 -p 選項),還可以通過給出別名作為參數(shù)但不進行賦值來顯示一個或多個別名,。清單 10 顯示了 which 和 vi 的別名,。
清單 10. which 和 vi 的別名
[ian@pinguino ~]$ alias which vi
alias which=‘a(chǎn)lias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde‘
alias vi=‘vim‘
|
which 命令的別名有些奇怪。為什么會將 alias 命令(沒有參數(shù))的輸出定向到 /usr/bin/which 上呢,?如果查看一下 which 命令的手冊頁,,就會發(fā)現(xiàn) --read-alias 選項通知 which 從標準輸入讀取一個別名列表,并將匹配項輸出到標準輸出設備上,。這允許 which 命令報告別名和 PATH 中的命令,,這種用法非常常見,因此您的發(fā)行版可能已將其作為默認設置了,。這是很好的一個做法,,因為如果別名和命令名相同,,那么 shell 就首先執(zhí)行別名。知道了這一點以后,,就可以使用 alias which 來加以檢查,。還可以通過運行 which which 命令來了解是否為 which 命令設置了這種別名。 .
別名的另外一種常見用法是自動為命令添加參數(shù),,正如在上面看到的 which 命令的 --read-alias 和其他幾個參數(shù)一樣,。這種方法也可用在 root 用戶使用 cp 、mv 和 rm 命令的時候,,這樣在刪除或覆蓋文件之前能夠顯示一個提示,。具體用法如清單 11 所示。
清單 11. 為了安全起見添加參數(shù)
[root@pinguino ~]# alias cp mv rm
alias cp=‘cp -i‘
alias mv=‘mv -i‘
alias rm=‘rm -i‘
|
命令列表
在之前的教程 “LPI 101 考試準備(主題 103):GNU 和 UNIX 命令” 中,,您已經(jīng)學習了命令序列 或列表,。您剛剛又看到了別名中使用的管道(|)操作符,您也可以使用命令列表,。舉個簡單的例子來說,,假設您希望使用一個命令來顯示當前目錄中的內(nèi)容,以及當前目錄及其子目錄所使用的空間,。讓我們就將其稱為 lsdu 命令,。因此您可以簡單地將 ls 和 du 命令序列賦值給別名 lsdu。清單 12 給出了實現(xiàn)這種功能的正確方法和錯誤方法,。在閱讀之前請仔細查看一下,,并考慮為什么第一次嘗試會失敗。
清單 12. 命令序列的別名
[ian@pinguino developerworks]$ alias lsdu=ls;du -sh # Wrong way
2.9M .
[ian@pinguino developerworks]$ lsdu
a tutorial new-article.sh new-tutorial.sh readme tools xsl
my-article new-article.vbs new-tutorial.vbs schema web
[ian@pinguino developerworks]$ alias ‘lsdu=ls;du -sh‘ # Right way way
[ian@pinguino developerworks]$ lsdu
a tutorial new-article.sh new-tutorial.sh readme tools xsl
my-article new-article.vbs new-tutorial.vbs schema web
2.9M .
|
在引用構成別名的完整序列時需要非常仔細,。如果使用 shell 變量作為別名的一部分,,還需要注意是使用雙引號還是使用單引號。您希望在定義或執(zhí)行別名時讓 shell 對變量進行擴展嗎,?清單 13 顯示了創(chuàng)建名為 mywd 定制命令來打印當前工作目錄名的錯誤方法。
清單 13. 定制 pwd —— 嘗試 1
[ian@pinguino developerworks]$ alias mywd="echo \"My working directory is $PWD\""
[ian@pinguino developerworks]$ mywd
My working directory is /home/ian/developerworks
[ian@pinguino developerworks]$ cd ..
[ian@pinguino ~]$ mywd
My working directory is /home/ian/developerworks
|
注意雙引號會導致 bash 在執(zhí)行命令之前就對變量進行擴展,。清單 14 使用了 alias 命令來顯示所生成的別名實際上是什么樣子,,從中可以看出我們的錯誤是很明顯的。清單 14 還給出了定義這個別名的正確方法,。
清單 14. 定制 pwd —— 嘗試 2
[ian@pinguino developerworks]$ alias mywd
alias mywd=‘echo \"My working directory is $PWD\"‘
[ian@pinguino developerworks]$ mywd
"My working directory is /home/ian/developerworks"
[ian@pinguino developerworks]$ cd ..
[ian@pinguino ~]$ mywd
"My working directory is /home/ian"
|
終于成功了,。
Shell 函數(shù)
別名讓您可以對某個命令或命令列表選用一種簡寫或其他名字。此外,,還可以添加其他一些內(nèi)容,,例如在 which 命令中加上希望查找的程序名。當 shell 執(zhí)行用戶的輸入時,,就會對別名進行擴展,;之后輸入的其他內(nèi)容都會在最后一個命令或命令列表執(zhí)行之前添加到該擴展,。這意味著只能在命令或命令列表之后添加參數(shù),也只能在最后一個命令中使用這些參數(shù),。函數(shù)提供了更多功能,,包括對參數(shù)進行處理的能力。函數(shù)是 POSIX shell 定義的一部分,,在諸如 bash,、dash 和 ksh 之類的 shell 中可以使用,但在 csh 或 tcsh 中不能使用,。
在接下來的幾節(jié)中,,將逐步構建一個復雜的命令:從很小的構建塊開始,逐漸在每個步驟加以完善,,并將其轉(zhuǎn)換成一個函數(shù),,以供以后使用。
假想問題
可以使用 ls 命令顯示有關文件系統(tǒng)中目錄和文件的各種信息,。假設您喜歡使用一個命令,,假定就是 ldirs , 來顯示目錄名,,所顯示的內(nèi)容如清單 15 所示,。
清單 15. ldirs 命令輸出結(jié)果
[ian@pinguino developerworks]$ ldirs *[st]* tools/*a*
my dw article
schema
tools
tools/java
xsl
|
為了保持簡單性起見,本節(jié)中的例子使用了 developerWorks author package 中的目錄和文件(請參看 參考資料),,如果您想為 developerWorks 編寫文章和教程,,也可以使用它們。在這些例子中,,我們使用了這個包中提供的 new-article.sh 腳本來為一篇我們稱之為 “my dw article” 的文章創(chuàng)建一個模板,。
在撰寫本文時,developerWorks author package 的版本是 5.6,,因此如果您使用更新的版本,,可能會發(fā)現(xiàn)一些不同之處?;蛘吣部梢灾皇褂米约旱奈募湍夸?。ldirs 命令也可以處理這些內(nèi)容。在 developerWorks author package 提供的工具中,,可以找到其他 bash 函數(shù)的例子,。
查找目錄項
如果在 ls 命令中使用了上述別名例子所示的顏色選項,請暫時忽略 *[st]* tools/*a* ,,這樣就可以看到類似于圖 1 所示的輸出結(jié)果,。
圖 1. 使用 ls 命令區(qū)分文件和目錄
在本例中,目錄都是使用深藍色顯示的,不過使用在本系列教程中所學到的知識還不足以解釋這個問題,。不過,,使用-l 選項會對如何繼續(xù)處理給出一點線索:目錄列表在第一個位置處有一個 “d” 字符。因此第一個步驟應該是使用 grep 對這個長列表中的內(nèi)容進行一些簡單的過濾,,如清單 16 所示,。
清單 16. 使用 grep 過濾目錄項
[ian@pinguino developerworks]$ ls -l | grep "^d"
drwxrwxr-x 2 ian ian 4096 Jan 24 17:06 my dw article
drwxrwxr-x 2 ian ian 4096 Jan 18 16:23 readme
drwxrwxr-x 3 ian ian 4096 Jan 19 07:41 schema
drwxrwxr-x 3 ian ian 4096 Jan 19 15:08 tools
drwxrwxr-x 3 ian ian 4096 Jan 17 16:03 web
drwxrwxr-x 3 ian ian 4096 Jan 19 10:59 xsl
|
截取目錄項
可以考慮使用 awk 而不是 grep ,來在一個步驟中既對列表進行過濾,,又截取每行的最后一部分內(nèi)容,,也就是目錄名,如清單 17 所示,。
清單 17. 使用 awk 代替 grep 進行處理
[ian@pinguino developerworks]$ ls -l | awk ‘/^d/ { print $NF } ‘
article
readme
schema
tools
web
xsl
|
清單 17 中的方法有一個問題:它無法正確處理名字中有空格的那些目錄名,,例如 “my dw article”。就像是 Linux 和我們生活中的大部分事情一樣,,解決一個問題通常有很多方法,,不過此處的目標是學習函數(shù)的知識,因此讓我們回到使用 grep 方法上來,。在本系列文章中我們學過的另外一個工具是 cut ,,它可以從一個文件(包括 stdin)中截取出很多域。現(xiàn)在讓我們在回過頭來看一下清單 16,,在文件名之前,,可以看到 8 個由空格分隔的域。在之前的命令后面加上 cut 就可以得到如清單 18 所示的輸出結(jié)果,。注意 -f9- 選項告訴 cut 打印第 9 個域以及之后的域的內(nèi)容,。
清單 18. 使用 cut 截取名稱
[ian@pinguino developerworks]$ ls -l | grep "^d" | cut -d" " -f9-
my dw article
readme
schema
tools
web
xsl
|
如果我們在 tools 目錄而不是當前目錄上執(zhí)行這個命令,使用這種方法存在的一個小問題就會變得十分明顯,,如清單 19 所示,。
清單 19. 使用 cut 存在的問題
[ian@pinguino developerworks]$ ls -l tools | grep "^d" | cut -d" " -f9-
11:25 java
[ian@pinguino developerworks]$ ls -ld tools/[fjt]*
-rw-rw-r-- 1 ian ian 4798 Jan 8 14:38 tools/figure1.gif
drwxrwxr-x 2 ian ian 4096 Oct 31 11:25 tools/java
-rw-rw-r-- 1 ian ian 39431 Jan 18 23:31 tools/template-dw-article-5.6.xml
-rw-rw-r-- 1 ian ian 39407 Jan 18 23:32 tools/template-dw-tutorial-5.6.xml
|
時間戳為什么會出現(xiàn)呢?兩個模板文件都有 5 個數(shù)字的大小,,而 java 目錄的大小則只有 4 個數(shù)字,,因此 cut 會將多出來的空格當作另外一個域分隔符來解釋。
使用 seq 來查找分割點
cut 命令也可以使用字符位置而不是域來進行分割,。除了計算字符個數(shù)之外,,bash shell 還有很多工具可以使用,因此可以嘗試使用 seq 和 printf 命令來在長目錄列表上面打印一個標尺,,這樣就可以方便地確定在什么地方對輸出行的內(nèi)容進行分割了。seq 命令最多可以使用 3 個參數(shù),,這就允許您可以打印出給定值之前的所有數(shù)字,,或者打印出一個值到另一個值之間的所有數(shù)字,又或者打印出從某個值開始按給定的步值到第三個數(shù)值結(jié)束的所有數(shù)字,。使用 seq 可以實現(xiàn)的其他有趣功能(包括打印 8 進制和 16 進制數(shù)字)請參看手冊頁?,F(xiàn)在,,讓我們使用 seq 和 printf 命令來打印一個標尺,每 10 個字符處的位置就標記一下,,如清單 20 所示,。
清單 20. 使用 seq 和 printf 打印標尺
[ian@pinguino developerworks]$ printf "....+...%2.d" `seq 10 10 60`;printf "\n";ls -l
....+...10....+...20....+...30....+...40....+...50....+...60
total 88
drwxrwxr-x 2 ian ian 4096 Jan 24 17:06 my dw article
-rwxr--r-- 1 ian ian 215 Sep 27 16:34 new-article.sh
-rwxr--r-- 1 ian ian 1078 Sep 27 16:34 new-article.vbs
-rwxr--r-- 1 ian ian 216 Sep 27 16:34 new-tutorial.sh
-rwxr--r-- 1 ian ian 1079 Sep 27 16:34 new-tutorial.vbs
drwxrwxr-x 2 ian ian 4096 Jan 18 16:23 readme
drwxrwxr-x 3 ian ian 4096 Jan 19 07:41 schema
drwxrwxr-x 3 ian ian 4096 Jan 19 15:08 tools
drwxrwxr-x 3 ian ian 4096 Jan 17 16:03 web
drwxrwxr-x 3 ian ian 4096 Jan 19 10:59 xsl
|
啊哈!現(xiàn)在可以使用 ls -l | grep "^d" | cut -c40- 命令來截取從位置 40 處開始的內(nèi)容了,。我們的第一反應是這也沒有真正解決問題,,因為更大的文件依然會將正確的分割位置向右移。您可以自己試驗一下,。
救援的 sed
sed 是 UNIX 和 Linux 工具包中的一個功能非常強大的編輯過濾器,,它使用了正則表達式。您知道我們的任務是從以 “d” 開頭的每一個輸出行去掉它前面的 8 個單詞和之后的空格,??梢允褂?sed 來實現(xiàn)這種功能:使用模式匹配表達式 /^d/ 選擇感興趣的行,并使用替換命令 s/^d\([^ ]* *\)\(8\}// 將前 8 個單詞替換為空字符串,。使用 -n 選項可以只打印那些通過 p 命令指定的行,,如清單 21 所示。
清單 21. 使用 sed 截取目錄名
[ian@pinguino developerworks]$ ls -l | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘
my dw article
readme
schema
tools
web
xsl
[ian@pinguino developerworks]$ ls -l tools | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘
java
|
要學習更多有關 sed 的內(nèi)容,,請參看 參考資料 一節(jié)的內(nèi)容,。
最終的函數(shù)
現(xiàn)在我們已經(jīng)得到滿足 ldirs 函數(shù)功能的復雜命令了,接下來應該學習如何將其編寫成一個函數(shù),。函數(shù)由函數(shù)名加上后面的 () 構成,,然后是一系列復合命令。對于現(xiàn)在來說,,復合命令可以是任何命令或命令列表,,使用一個分號結(jié)束,并使用一對花括號包括起來(且必須使用空格與其他符號分隔開來),。在后面 Shell 腳本 一節(jié)中您將學到其他的復合命令,。
注意:在 Bash shell 中,函數(shù)名前面可以加上單詞 “function”,,但這并不是 POSIX 規(guī)范的一部分,,諸如 dash 之類的更簡單的 shell 并不支持這種用法。在 Shell 腳本 一節(jié)中,,您將學習在使用了不同的 shell 時,,如何確保腳本會被適當?shù)?shell 解釋。
在函數(shù)內(nèi)部,,可以使用表 4 中給出的 bash 特殊變量來引用參數(shù),。可以像其他 shell 變量一樣在這些變量前面加上一個 $ 符號來引用這些變量。
表 4. 函數(shù)的 Shell 參數(shù)
參數(shù) |
用途 |
0, 1, 2, ... |
從參數(shù) 0 開始的位置參數(shù),。參數(shù) 0 指的是啟動 bash 的程序名,;如果函數(shù)是在一個 shell 腳本中運行的,就是這個 shell 腳本的名字,。有關其他可能的信息,,請參看 bash 的手冊頁,例如使用 -c 參數(shù)啟動 bash 時的情況,。以單引號或雙引號括起來的字符串都會當作一個參數(shù)傳遞,,引號會被剝離掉。在雙引號的情況中,,諸如 $HOME 之類的 shell 變量會在調(diào)用函數(shù)之前被展開,。您可能需要使用單引號或雙引號來傳遞參數(shù),這些參數(shù)可以包含對 shell 具有特殊意義的嵌入空格或其他字符,。 |
* |
從參數(shù) 1 開始的位置參數(shù),。如果已經(jīng)把雙引號中的內(nèi)容展開了,那么展開后就是一個單詞,,使用域間分隔符(IFS)特殊變量的第一個字符來分隔參數(shù),;如果 IFS 為空,就不會插入任何分隔,。默認的 IFS 值可以是空白,、制表符和換行符。如果 IFS 沒有設置,,那么所使用的分隔符就是空白,,就像默認的 IFS 一樣。 |
@ |
從參數(shù) 1 開始的位置參數(shù),。如果已經(jīng)把雙引號中的內(nèi)容展開了,,那么每個參數(shù)都變成一個單詞,因此 “$@” 就等于 “$1”“$2”...,。如果參數(shù)中可能會包含嵌入空白,,就可以使用這種格式。 |
# |
參數(shù)個數(shù),,不包括參數(shù) 0,。 |
注意: 如果參數(shù)多于 9 個,就不能使用 $10 來引用第 10 個參數(shù),。而必須首先處理或保存第一個參數(shù)($1),,然后使用 shift 命令來刪除第 1 個參數(shù),并將其他參數(shù)下移 1 位,,這樣 $10 就變成了 $9,,依此類推,。$# 的值也同時會被更新,從而反應剩余參數(shù)的個數(shù),。
現(xiàn)在可以定義一個簡單函數(shù),其功能僅僅是說明有多少個參數(shù),,并顯示這些參數(shù),;如清單 12 所示。
清單 22. 函數(shù)參數(shù)
[ian@pinguino developerworks]$ testfunc () { echo "$# parameters"; echo "$@"; }
[ian@pinguino developerworks]$ testfunc
0 parameters
[ian@pinguino developerworks]$ testfunc a b c
3 parameters
a b c
[ian@pinguino developerworks]$ testfunc a "b c"
2 parameters
a b c
|
不管使用的是 $*,、"$*",、$@ 還是 "$@",在上面這個函數(shù)的輸出結(jié)果中并沒有太大區(qū)別,,不過當問題變得復雜時,,可以肯定區(qū)別將會變得非常大。
現(xiàn)在,,用這個到目前為止最為復雜的命令來創(chuàng)建一個 ldirs 函數(shù),,使用 “$@” 表示參數(shù)??梢韵袂懊娴睦右粯訉⑷亢瘮?shù)都輸入到一行中,;當然 bash 也允許在多行中輸入命令,在這種情況中會自動添加分號,,如清單 23 所示,。清單 23 還顯示了使用 type 命令來顯示函數(shù)定義。注意在 type 的輸出結(jié)果中,, ls 命令已經(jīng)被它別名的展開值替換掉了,。如果需要避免這個問題,可以使用 /bin/ls 而不是單單的 ls ,。
清單 23. 第一個 ldirs 函數(shù)
[ian@pinguino developerworks]$ # Enter the function on a single line
[ian@pinguino developerworks]$ ldirs () { ls -l "$@"|sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘; }
[ian@pinguino developerworks]$ # Enter the function on multiple lines
[ian@pinguino developerworks]$ ldirs ()
> {
> ls -l "$@"|sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘
> }
[ian@pinguino developerworks]$ type ldirs
ldirs is a function
ldirs ()
{
ls --color=tty -l "$@" | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘
}
[ian@pinguino developerworks]$ ldirs
my dw article
readme
schema
tools
web
xsl
[ian@pinguino developerworks]$ ldirs tools
java
|
現(xiàn)在您的函數(shù)似乎已經(jīng)可以正常工作了,。但是如果像清單 24 那樣運行 ldirs * 會如何呢?
清單 24. 運行 ldirs *
[ian@pinguino developerworks]$ ldirs *
5.6
java
www.ibm.com
5.6
|
感到驚奇嗎,?實際上,,您并沒有找到當前目錄中的目錄,而是找到了第 2 級子目錄的內(nèi)容,。查看一下 ls 命令的手冊頁或本系列前面的教程就可以理解這是為什么了,。或者像清單 25 那樣運行 find 命令來查找第 2 級子目錄名,。
清單 25. 查找第 2 級子目錄
[ian@pinguino developerworks]$ find . -mindepth 2 -maxdepth 2 -type d
./tools/java
./web/www.ibm.com
./xsl/5.6
./schema/5.6
|
添加測試
使用通配符暴露了這種方法在邏輯上存在的一個問題,。我們忽略了這樣的一個事實,即不使用任何參數(shù)時 ldirs 顯示的是當前目錄的子目錄,,而 ldirs tools 顯示的是 tools 目錄中的 java 子目錄,,而不是 tools 目錄本身,,這與將 ls 命令用于文件而非目錄的情形是一樣的。理想情況下,,如果沒有給定參數(shù),,就應該使用 ls -l ;如果給定了一些參數(shù),,就應該使用 ls -ld 命令,。可以使用 test 命令來測試參數(shù)個數(shù),,然后使用 && 和 || 來構建一個命令列表,,并執(zhí)行適當?shù)拿睢J褂?test 的 [ test expression ] 格式,,您的表達式可能會是這樣: { [ $# -gt 0 ] &&/bin/ls -ld "$@" || /bin/ls -l } | sed -ne ... ,。
不過這段代碼還有一個小問題,如果 ls -ld 命令不能找到任何匹配文件或目錄,,就會產(chǎn)生一條錯誤消息,,并返回一個非 0 的退出代碼,這會導致 ls -l 命令也會被執(zhí)行,。這可能并不是我們所期望的,。一個解決的方案是為第一個 ls 命令構造一個復合命令,這樣如果命令失敗,,就可以對參數(shù)個數(shù)再次進行測試,。可以對原來的函數(shù)進行擴充來包含這種功能,,現(xiàn)在這個函數(shù)應該如清單 26 所示,。可以利用清單 26 中的參數(shù)來嘗試使用該函數(shù),,也可以利用您自己的參數(shù)來體驗一下,,看這個函數(shù)是怎樣工作的。
清單 26. 使用 ldirs 處理通配符
[ian@pinguino ~]$ type ldirs
ldirs is a function
ldirs ()
{
{
[ $# -gt 0 ] && {
/bin/ls -ld "$@" || [ $# -gt 0 ]
} || /bin/ls -l
} | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘
}
[ian@pinguino developerworks]$ ldirs *
my dw article
readme
schema
tools
web
xsl
[ian@pinguino developerworks]$ ldirs tools/*
tools/java
[ian@pinguino developerworks]$ ldirs *xxx*
/bin/ls: *xxx*: No such file or directory
[ian@pinguino developerworks]$ ldirs *a* *s*
my dw article
readme
schema
schema
tools
xsl
|
最終版本
現(xiàn)在,,在清單 26 中給出的這個例子中,,可以看到一個目錄被列出了兩次。如果希望,,可以通過 sort | uniq 對 sed 的輸出結(jié)果進行過濾,,從而擴充原來的函數(shù)來解決這個問題。
從一些基本的構造塊開始,,現(xiàn)在您已經(jīng)構建了一個非常復雜的 shell 函數(shù)了,。
定制擊鍵組合
您在終端會話中輸入的擊鍵組合,以及在諸如 FTP 之類的程序中使用的擊鍵組合,,都是由 readline 庫進行處理的,,并且可以進行配置,。默認情況下,定制文件是主目錄中的 .inputrc 文件,;如果系統(tǒng)中存在這個文件,,就會在 bash 啟動過程中讀取這個文件??梢酝ㄟ^設置 INPUTRC 變量來配置不同的文件,。如果沒有設置這個變量,就會使用主目錄中的 .inputrc 文件,。很多系統(tǒng)在 /etc/inputrc 中都有一個默認的鍵映射,因此您通常會希望使用 $include 指令來包含它,。
清單 27 展示了如何將 ldirs 函數(shù)綁定到 Ctrl-t 的鍵盤組合上(按下并一直按著 Ctrl 鍵,,然后按下 t)。如果希望此命令執(zhí)行時不使用任何參數(shù),,可以在配置行末尾添加 \n,。
清單 27. 樣例 .inputrc 文件
# My custom key mappings
$include /etc/inputrc
|
可以通過先按 Ctrl-x 再按 Ctrl-r 來強制再次讀取 INPUTRC 文件。注意如果沒有自己的 .inputrc 文件,,有些發(fā)行版會設置 INPUTRC=/etc/inputrc,,因此如果您在這種系統(tǒng)上創(chuàng)建了 .inputrc 文件,就需要先登出系統(tǒng),,然后再登錄一次,,這樣才能使用新的定義。只將 INPUTRC 設置為空或?qū)⑵渲赶蛐挛募粫匦伦x取原來的文件,,而不是新的規(guī)范,。
INPUTRC 文件可以包括一些條件規(guī)范。例如,,您的鍵盤行為可能會根據(jù)您使用的是 emacs 編輯模式(bash 默認值)還是 vi 模式而有所不同,。有關如何定制鍵盤的更多細節(jié),請參看 bash 的手冊頁,。
保存別名和函數(shù)
您可以將自己的別名和函數(shù)添加到自己的 ~/.bashrc 文件中,,不過也可以將它們保存到任何您喜歡的文件中。不管怎樣做,,都請記住使用 source 或 . 命令來引用這些文件,,這樣就會讀取文件的內(nèi)容,并在當前環(huán)境中執(zhí)行這個文件,。如果創(chuàng)建了一個腳本并簡單執(zhí)行它,,那么這個腳本就是在一個子 shell 中執(zhí)行的,當這個子 shell 退出并將控制權返回給您時,,所有有價值的定制就全部丟失了,。
在下一節(jié)中,,將學習如何超越這些簡單的函數(shù),如何添加一些編程結(jié)構,,例如條件測試和循環(huán)結(jié)構,,并將它們與多個函數(shù)結(jié)合起來來創(chuàng)建或修改 bash shell 腳本。
|