shell十三问--shell教程
為什么80%的碼農都做不了架構師?>>> ??
13_questions_of_shell
shell十三問--shell教程(markdown 版本)
##shell十三問之1: 何為shell?
shell是什么東西之前,不妨讓我們重新審視使用者和計算機系統的關系: (此處為使用者和計算機系統的關系圖)
我們知道計算機的運作不能離開硬件,但使用者卻無法直接操作硬件, 硬件的驅動只能通過一種稱為“操作系統(OS,Opertating System)”的軟件來管控。 事實上,我們每天所談的“linux”,嚴格來說只是一個操作系統(OS), 我們稱之為“內核(kernel)”。
然而,從使用者的角度來說,使用者沒有辦法直接操作一個kernel, 而是通過kernel的“外殼”程序,也就是所謂的shell,來與kernel溝通。 這也正是kernel跟shell的形象命名的的關系。如圖: (此處為kernel-->shell關系圖;)
從技術的角度來說,shell是一個使用者與系統的交互界面(interface), 只能讓使用者通過命令行(command line)來使用系統來完成工作。 因此,shell最簡單的定義就是----命令解釋器( Command Interpreter):
- 將使用者的命令翻譯給kernel來處理;
- 同時,將kernel的處理結果翻譯給使用者。
每次當我們完成系統登入(login), 我們就取得一個交互模式的shell, 也稱之為login shell 或者 primary shell。
若從進程(process)的角度來說,我們在shell所下達的命令,均是shell所產生的子進程。 這種現象,我暫可稱之為fork。
如果是執行shell腳本(shell script)的話,腳本中命令則是由另一個非交互模式的 子shell(sub shell)來執行的。 也就是primary shell產生sub shell的進程,而該sub shell 進程再產生script中所有命令的進程。 (關于進程,我們日后有機會在補充)
這里, 我們必須知道:kernel 與 shell 是不同的兩套軟件,而且都是可以被替換的:
- 不同的OS使用不同的kernel;
- 同一個kernel之上,也可以使用不同的shell;
在Linux的預設系統中,通常可以找到好幾種不同的shell, 且通常會被記錄在如下文件中:
/etc/shells不同的shell有著不同的功能,且彼此各異,或者說“大同小異”。 常見的shell主要分為兩大主流:
- burne shell (sh)
- burne again shell (bash)
- c shell (csh)
- tc shell (tcsh)
- korn shell (ksh) (FIXME)
大部分的Linux操作系統的預設shell都是bash,其原因大致如下兩種:
- 自由軟件
- 功能強大 bash是gnu project最成功的產品之一,自推出以來深受廣大Unix用戶的喜愛, 且也逐漸成為不少組織的系統標準。
##shell十三問之2:shell prompt(PS1)與Carriage Return(CR)關系
當你成功登陸一個shell終端的文字界面之后,大部分的情形下, 你會在屏幕上看到一個不斷閃爍的方塊或者底線(視不同的版本而別), 我們稱之為游標(cursor). cursor作用就是告訴你接下來你從鍵盤輸入的按鍵所插入的位置, 且每輸入一個鍵,cursor便向右移動一個格子, 如果連續輸入太多的話,則自動接在下一行輸入。
假如你剛完成登陸,還沒有輸入任何按鍵之前, 你所看到的cursor所在的位置的同一行的左邊部分,我們稱之為提示符(prompt)。
提示符的格式或因不同的版本而各有不同, 在Linux上,只需留意最接近游標的一個提示符號,通常是如下兩者之一:
- $: 給一般用戶賬號使用;
- #: 給root(管理員)賬號使用;
事實上,shell prompt的意思很簡單: 告訴shell使用者,您現在可以輸入命令行了。
我們可以說,使用者只有在得到shell prompt才能打命令行, 而cursor是指示鍵盤在命令行的輸入位置,使用者每輸入一個鍵, cursor就往后移動一個格,直到碰到命令行讀進CR(Carriage Return, 由Enter鍵產生)字符為止。 CR的意思也很簡單: 使用者告訴shell:老兄,你可以執行的我命令行了。 嚴格來說: 所謂的命令行, 就是在shell prompt與CR之間所輸入的文字。
(question:為何我們這里堅持使用CR字符而不說Enter按鍵呢? 答案在后面的學習中給出)。
不同的命令可以接受的命令的格式各有不同, 一般情況下,一個標準的命令行格式為如下所列:
command-name options argument若從技術的細節上來看, shell會依據IFS(Internal Field Seperator) 將 command line 所輸入的文字給拆解為字段(word). 然后在針對特殊的字符(meta)先做處理,最后在重組整行command line。
(注意:請務必理解以上兩句的意思,我們日后的學習中常回到這里思考。)
其中IFS是shell預設使用的字段位分隔符號,可以由一個及多個如下按鍵組成:
- 空白鍵(White Space)
- 表格鍵(Tab)
- 回車鍵(Enter)
系統可以接受的命令的名稱(command-name)可以從如下途徑獲得:
- 確的路徑所指定的外部命令
- 命令的別名(alias)
- shell內建命令(built-in)
- $PATH之下的外部命令
每一個命令行均必須包含命令的名稱,這是不能缺少的。
##shell十三問之3:別人echo、你也echo,是問echo知多少?
承接上一章介紹的command line, 這里我們用echo這個命令加以進一步說明。
溫習 標準的command line三個組成部分:command_name option argument
echo是一個非常簡單、直接的Linux命令:
$echo argumentecho將argument送出到標準輸出(stdout),通常是在監視器(monitor)上輸出。
Note:
在linux系統中任何一個進程默認打開三個文件:stdin、stdout、stderr.
stdin 標準輸入
stdout 標準輸出
stderr 標準錯誤輸出
為了更好理解,不如先讓我們先跑一下echo命令好了:
$echo$你會發現只有一個空白行,然后又回到了shell prompt上了。 這是因為echo在預設上,在顯示完argument之后,還會送出以一個換行符號 (new-line charactor). 但是上面的command echo并沒有任何argument,那結果就只剩一個換行符號。 若你要取消這個換行符號, 可以利用echo的-n 選項:
$echo -n $不妨讓我們回到command line的概念上來討論上例的echo命令好了: command line只有command_name(echo)及option(-n),并沒有顯示任何argument。
要想看看echo的argument,那還不簡單接下來,你可以試試如下的輸入:
$echo first line first line $echo -n first line first line $以上兩個echo命令中,你會發現argument的部分顯示在你的屏幕, 而換行符則視 -n 選項的有無而別。 很明顯的,第二個echo由于換行符被取消了, 接下來的shell prompt就接在輸出結果的同一行了... ^_^。
事實上,echo除了-n 選項之外,常用選項有:
- -e: 啟用反斜杠控制字符的轉換(參考下表)
- -E: 關閉反斜杠控制字符的轉換(預設如此)
- -n: 取消行末的換行符號(與-e選項下的\c字符同意)
關于echo命令所支持的反斜杠控制字符如下表:
| \a | ALERT / BELL(從系統的喇叭送出鈴聲) |
| \b | BACKSPACE, 也就是向左退格鍵 |
| \c | 取消行末之換行符號 |
| \E | ESCAPE, 脫字符鍵 |
| \f | FORMFEED, 換頁字符 |
| \n | NEWLINE, 換行字符 |
| \r | RETURN, 回車鍵 |
| \t | TAB, 表格跳位鍵 |
| \v | VERTICAL TAB, 垂直表格跳位鍵 |
| \n | ASCII 八進制編碼(以x開頭的為十六進制),此處的n為數字 |
| \ | 反斜杠本身 |
Note: 上述表格的資料來自O'Reilly出版社的Learning the Bash Shell, 2nd Ed.
或許,我們可以通過實例來了解echo的選項及控制字符:
例一:
$ echo -e "a\tb\tc\n\d\te\tf" a b c d e f $上例中,用\t來分割abc還有def,及用\n將def換至下一行。
例二:
$echo -e "\141\011\142\011\143\012\144\011\145\011\146" a b c d e f與例一中結果一樣,只是使用ASCII八進制編碼。
例三:
$echo -e "\x61\x09\x62\x09\x63\x0a\x64\x09\x65\x09\x66" a b c d e f與例二差不多,只是這次換用ASCII的十六進制編碼。
例四:
$echo -ne "a\tb\tc\nd\te\bf\a" a b c d f $因為e字母后面是退格鍵(\b),因此輸出結果就沒有e了。 在結束的時聽到一聲鈴響,是\a的杰作。 由于同時使用了-n選項,因此shell prompt緊接在第二行之后。 若你不用-n的話,那你在\a后再加個\c,也是同樣的效果。
事實上,在日后的shell操作及shell script設計上, echo命令是最常被使用的命令之一。 比方說,使用echo來檢查變量值:
$ A=B $ echo $A B $ echo $? 0Note: 關于變量的概念,我們留到以下的兩章跟大家說明。
好了,更多的關于command line的格式, 以及echo命令的選項, 請您自行多加練習、運用了...
##shell十三問之4:""(雙引號)與''(單引號)差在哪?
還是回到我們的command line來吧...
經過前面兩章的學習,應該很清楚當你在shell prompt后面敲打鍵盤, 直到按下Enter鍵的時候,你輸入的文字就是command line了, 然后shell才會以進程的方式執行你所交給它的命令。 但是,你又可知道:你在command line中輸入的每一個文字, 對shell來說,是有類別之分的呢?
簡單而言,(我不敢說精確的定義,注1), command line的每一個charactor, 分為如下兩種:
- literal:也就是普通的純文字,對shell來說沒特殊功能;
- meta: 對shell來說,具有特定功能的特殊保留元字符。
Note:
對于bash shell在處理comamnd line的順序說明, 請參考O'Reilly出版社的Learning the Bash Shell,2nd Edition, 第177-180頁的說明,尤其是178頁的流程圖:Figure 7-1 ...
literal沒什么好談的, 像abcd、123456這些"文字"都是literal...(so easy? ^_^) 但meta卻常使我們困惑...(confused?) 事實上,前兩章,我們在command line中已碰到兩個 似乎每次都會碰到的meta:
- IFS:有space或者tab或者Enter三者之一組成(我們常用space)
- CR: 由Enter產生;
IFS是用來拆解command line中每一個詞(word)用的, 因為shell command line是按詞來處理的。 而CR則是用來結束command line用的,這也是為何我們敲Enter鍵, 命令就會跑的原因。
除了常用的IFS與CR, 常用的meta還有:
| = | 設定變量 |
| $ | 作變量或運算替換(請不要與shell prompt混淆) |
| > | 輸出重定向(重定向stdout) |
| < | 輸入重定向(重定向stdin) |
| | | 命令管道 |
| & | 重定向file descriptor或將命令至于后臺(bg)運行 |
| () | 將其內部的命令置于nested subshell執行,或用于運算或變量替換 |
| {} | 將期內的命令置于non-named function中執行,或用在變量替換的界定范圍 |
| ; | 在前一個命令執行結束時,而忽略其返回值,繼續執行下一個命令 |
| && | 在前一個命令執行結束時,若返回值為true,繼續執行下一個命令 |
| || | 在前一個命令執行結束時,若返回值為false,繼續執行下一個命令 |
| ! | 執行histroy列表中的命令 |
| ... | ... |
假如我們需要在command line中將這些保留元字符的功能關閉的話, 就需要quoting處理了。
在bash中,常用的quoting有以下三種方法:
- hard quote:''(單引號),凡在hard quote中的所有meta均被關閉;
- soft quote:""(雙引號),凡在soft quote中大部分meta都會被關閉,但某些會保留(如$);
- escape: \ (反斜杠),只有在緊接在escape(跳脫字符)之后的單一meta才被關閉;
Note:
在soft quote中被豁免的具體meta清單,我不完全知道, 有待大家補充,或通過實踐來發現并理解。
下面的例子將有助于我們對quoting的了解:
$ A=B C #空白符未被關閉,作為IFS處理 $ C:command not found. $ echo $A$ A="B C" #空白符已被關掉,僅作為空白符 $ echo $A B C在第一個給A變量賦值時,由于空白符沒有被關閉, command line 將被解釋為: A=B 然后碰到<IFS>,接著執行C命令 在第二次給A變量賦值時,由于空白符被置于soft quote中, 因此被關閉,不在作為IFS; A=B<space>C 事實上,空白符無論在soft quote還是在hard quote中, 均被關閉。Enter鍵字符亦然:
$ A=`B > C > ' $ echo "$A" B C在上例中,由于enter被置于hard quote當中,因此不再作為CR字符來處理。 這里的enter單純只是一個斷行符號(new-line)而已, 由于command line并沒得到CR字符, 因此進入第二個shell prompt(PS2, 以>符號表示), command line并不會結束,直到第三行, 我們輸入的enter并不在hard quote里面, 因此沒有被關閉, 此時,command line碰到CR字符,于是結束,交給shell來處理。
上例的Enter要是被置于soft quote中的話,CR字符也會同樣被關閉:
$ A="B > C > " $ echo $A B C然而,由于 echo $A時的變量沒有置于soft quote中, 因此,當變量替換完成后,并作命令行重組時,enter被解釋為IFS, 而不是new-line字符。
同樣的,用escape亦可關閉CR字符:
$ A=B\ > C\ > $ echo $A BC上例中的,第一個enter跟第二個enter均被escape字符關閉了, 因此也不作為CR來處理,但第三個enter由于沒有被escape, 因此,作為CR結束command line。 但由于enter鍵本身在shell meta中特殊性,在 \ escape字符后面 僅僅取消其CR功能, 而不保留其IFS功能。
你或許發現光是一個enter鍵所產生的字符,就有可能是如下這些可能:
- CR
- IFS
- NL(New Line)
- FF(Form Feed)
- NULL
- ...
至于,什么時候解釋為什么字符,這個我就沒法去挖掘了, 或者留給讀者君自行慢慢摸索了...^-^
至于soft quote跟hard quote的不同,主要是對于某些meta的關閉與否,以$來做說明:
$ A=B\ C $ echo "$A" B C $ echo '$A' $A在第一個echo命令行中,$被置于soft quote中,將不被關閉, 因此繼續處理變量替換, 因此,echo將A的變量值輸出到屏幕,也就是"B C"的結果。
在第二個echo命令行中,$被置于hard quote中,則被關閉, 因此,$只是一個$符號,并不會用來做變量替換處理, 因此結果是$符號后面接一個A字母:$A.
練習與思考: 如下結果為何不同?
tips: 單引號和雙引號,在quoting中均被關閉了。
$ A=B\ C $ echo '"$A"' #最外面的是單引號 "$A" $ echo "'$A'" #最外面的是雙引號 'B C'在CU的shell版里,我發現很多初學者的問題, 都與quoting的理解有關。 比方說,若我們在awk或sed的命令參數中, 調用之前設定的一些變量時,常會問及為何不能的問題。
要解決這些問題,關鍵點就是:區分出 shell meta 與 command meta
前面我們提到的那些meta,都是在command line中有特殊用途的, 比方說{}就是將一系列的command line置于不具名的函數中執行(可簡單視為command block), 但是,awk卻需要用{}來區分出awk的命令區段(BEGIN,MAIN,END). 若你在command line中如此輸入:
$ awk {print $0} 1.txt由于{}在shell中并沒有關閉,那shell就將{print $0}視為command block, 但同時沒有;符號作命令分隔,因此,就出現awk語法錯誤結果。
要解決之,可用hard quote:
awk '{print $0}'上面的hard quote應好理解,就是將原來的 {、<space>、$、}這幾個shell meta關閉, 避免掉在shell中遭到處理,而完整的成為awk的參數中command meta。
Note:
awk中使用的$0 是awk中內建的field nubmer,而非awk的變量, awk自身的變量無需使用$.
要是理解了hard quote的功能,在來理解soft quote與escape就不難:
awk "{print \$0}" 1.txt awk \{print \$0\} 1.txt然而,若要你改變awk的$0的0值是從另一個shell變量中讀進呢? 比方說:已有變量$A的值是0, 那如何在command line中解決 awk的$$A呢? 你可以很直接否定掉hard quote的方案:
$ awk '{print $$A}' 1.txt那是因為$A的$在hard quote中是不能替換變量的。
聰明的讀者(如你!),經過本章的學習,我想,你應該可以理解為 為何我們可以使用如下操作了吧:
A=0 awk "{print \$$A}" 1.txt awk \{print\ \$$A\} 1.txt awk '{print $'$A'}' 1.txt awk '{print $'"$A"'}' 1.txt或許,你能給出更多方案... ^_^
更多練習:
- http://bbs.chinaunix.net/forum/viewtopic.php?t=207178 一個關于read命令的小問題: 很早以前覺得很奇怪:執行read命令,然后讀取用戶輸入給變量賦值, 但如果輸入是以空格鍵開始的話,這空格會被忽略,比如:
原因: 變量a的值,從終端輸入的值是以IFS開頭,而這些IFS將被shell解釋器忽略(trim)。 應該與shell解釋器分詞的規則有關;
read a #輸入:\ \ \ abc echo "$a" #只輸出abc需要將空格字符轉義
Note:
IFS Internal field separators, normally space, tab, and newline (see Blank Interpretation section). ...... Blank Interpretation After parameter and command substitution, the results of substitution
are scanned for internal field separator characters (those found in IFS) and split into distinct arguments where such characters are found. Explicit null arguments ("" or '') are retained.
Implicit null arguments(those resulting from parameters that have no values) are removed. (refre to: man sh)
解決思路:
若你還是不理解,那來驗證一下下面這個例子:
$ A=" abc" $ echo $A abc $ echo "$A" #note1abc $ old_IFS=$IFS $ IFS=; $ echo $Aabc $ IFS=$old_IFS $ echo $A abcNote:
問題二:為什么多做了幾個分號,我想知道為什么會出現空格呢?
$ a=";;;test" $ IFS=";" $ echo $a test $ a=" test" $ echo $a test $ IFS=" " $ echo $a test解答:
這個問題,出在IFS=;上。 因為這個;在問題一中的command line上是一個meta, 并非";"符號本身。 因此,IFS=;是將IFS設置為 null charactor (不是space、tab、newline)。
要不是試試下面這個代碼片段:
$ old_IFS=$IFS $ read A ;a;b;c $ echo $A ;a;b;c $ IFS=";" #Note2 $ echo $A a b cNote:
要關閉;可用";"或者';'或者\;。
- http://bbs.chinaunix.net/forum/viewtopic.php?t=216729
思考問題二:文本處理:讀文件時,如何保證原汁原味。
cat file | while read i doecho $i done文件file的行中包含若干空,經過read只保留不重復的空格。 如何才能所見即所得。
cat file | while read i doecho "X${i}X" done從上面的輸出,可以看出read,讀入是按整行讀入的; 不能原汁原味的原因:
以上代碼可以解決原因2中的,command line的分詞和重組導致meta字符丟失; 但仍然解決不了原因1中,read讀取行時,忽略行起始的IFS meta字符。
回過頭來看上面這個問題:為何要原汁原味呢? cat命令就是原汁原味的,只是shell的read、echo導致了某些shell的meta字符丟失;
如果只是IFS meta的丟失,可以采用如下方式: 將IFS設置為null,即IFS=;, 在此再次重申此處;是shell的meta字符,而不是literal字符; 因此要使用literal的 ;應該是\; 或者關閉meta 的(soft/hard) quoting的";"或者';'。
因此上述的解決方案是:
old_IFS=$IFS IFS=; #將IFS設置為null cat file | while read i doecho "$i" done IFS=old_IFS #恢復IFS的原始值現在,回過頭來看這個問題,為什么會有這個問題呢; 其本源的問題應該是沒有找到解決原始問題的最合適的方法, 而是采取了一個迂回的方式來解決了問題;
因此,我們應該回到問題的本源,重新審視一下,問題的本質。 如果要精準的獲取文件的內容,應該使用od或者hexdump會更好些。
##shell十三問之5:問var=value 在export前后的差在哪?
這次讓我們暫時丟開command line, 先了解一下bash變量(variable)吧...
所謂的變量,就是利用一個固定的"名稱"(name), 來存取一段可以變化的"值"(value)。
###1. 變量設定(set) 在bash中, 你可以用"="來設定或者重新定義變量的內容:
name=value在設定變量的時候,得遵守如下規則:
- 等號左右兩邊不能使用分隔符號(IFS),也應避免使用shell的保留元字符(meta charactor);
- 變量的名稱(name)不能使用$符號;
- 變量的名稱(name)的首字符不能是數字(number)。
- 變量的名稱(name)的長度不可超過256個字符。
- 變量的名稱(name)及變量的值的大小寫是有區別的、敏感的(case sensitive,)
如下是一些變量設定時常見的錯誤:
A= B #=號前后不能有IFS 1A=B #變量名稱不能以數字開頭 $A=B #變量的名稱里有$ a=B #這跟a=b是不同的,(這不是錯誤,提醒windows用戶)如下則是可以接受的設定:
A=" B" #IFS被關閉,參考前面的quoting章節 A1=B #并非以數字開頭 A=$B #$可用在變量的值內 This_Is_A_Long_Name=b #可用_連接較長的名稱或值,且有大小區別;###2. 變量替換(substitution) shell 之所以強大,其中的一個因素是它可以在命令行中對變量作 替換(substitution)處理。 在命令行中使用者可以使用$符號加上變量名稱(除了用=定義變量名稱之外), 將變量值給替換出來,然后再重新組建命令行。
比方:
$ A=ls $ B=la $ C=/tmp $ $A -$B $C以上命令行的第一個$是shell prompt, 并不在命令行之內。 必須強調的是,我們所提的變量替換,只發生在command line上面。 (是的,請讓我們再次回到命令行吧!) 仔細分析,最后那行 command line,不難發現在被執行前(在輸入CR字符之前), $符號對每一個變量作替換處理(將變量的值替換出來再重組命令行), 最后會得出如下命令行:
ls -la /tmp還記得第二章,我請大家"務必理解"的那兩句嗎? 若你忘了,我這里重貼一遍:
Note:
若從技術的細節來看,shell會依據IFS(Internal Field Seperator) 將command line所輸入的文字拆解為"字段"(word/field)。 然后再針對特殊字符(meta)先作處理,最后重組整行command line。
這里的$就是command line中最經典的meta之一了, 就是作變量替換的。在日常的shell操作中, 我們常會使用echo命令來查看特定的變量的值, 例如:
$ echo $A -$B $C我們已學過,echo命令只單純將其argument送至"標準輸出"(stdout, 通常是我們的屏幕)。 所以上面的命令會在屏幕上得到如下結果:
ls -al /tmp這是由于echo命令在執行時,會先將$A (ls)、$B (la)跟$C (/tmp)給替換出來; 利用shell對變量的替換處理能力,我們在設定變量時就更為靈活了:
A=B B=$A這樣,B的變量值就可繼承A變量"當時"的變量值了。 不過,不要以"數學邏輯"來套用變量的設定,比方說:
A=B B=C這樣,并不會讓A的變量值變成C。再如:
A=B B=$A A=C同樣也不會讓B的值變成C。
上面是單純定義了兩個不同名稱的變量: A 與 B, 它們的取值分別是C與B。
若變量被重復定義的話,則原有值為新值所取代。(這不正是"可變的量"嗎?^_^) 當我們在設定變量的時候,請記住這點:用一個名稱存儲一個數值, 僅此而已。
此外, 我們也可以利用命令行的變量替換能力來"擴充"(append)變量的值:
A=B:C:D A=$A:E這樣, 第一行我們設定A的值為"B:C:D", 然后,第二行再將值擴充為"B:C:D:E"。
上面的擴充的范例,我們使用分隔符號(:)來達到擴充的目的, 要是沒有分隔符的話,如下是有問題的:
A=BCD B=$AE因為第二次是將A的值繼承$AE的替換結果,而非$A再加E。 要解決此問題,我們可用更嚴謹的替換處理:
A=BCD A=${A}E上例中,我們使用{}將變量名稱范圍給明確定義出來, 如此一來, 我們就可以將A的變量值從BCD給擴充為BCDE。
Tips: 關于${name}事實上還可以做到更多的變量處理能力, 這些均屬于比較進階階段的變量處理,現階段暫不介紹了, 請大家自行參考資料。
###3. export 變量
嚴格來說,我們在當前shell中所定義的變量,均屬于 "本地變量"(local variable), 只有經過export命令的 "輸出"處理,才能成為"環境變量"(environment variable):
$ A=B $ export A或者
$ export A=B經過export輸出處理之后,變量A就能成為一個環境變量 供其后的命令使用。在使用export的時候,請別忘記 shell在命令行對變量的"替換"(substitution)處理。 比方說:
$ A=B $ B=C $ export $A上面的命令并未將A輸出為"環境變量",而是將B導出 這是因為在這個命令行中,$A會首先被替換為B,然后在"塞回" 作export的參數。
要理解這個export,事實上需要從process(進程)的角度來理解才能透徹。 我們將于下一章為大家說明process(進程)的概念,敬請留意。
####4. 取消變量(unset) 要取消一個變量,在bash中可使用unset命令來處理:
unset A與export一樣,unset命令行,也同樣會作 變量替換(這其實是shell的功能之一), 因此:
$ A=B $ B=C $ unset $A事實上,所取消的是變量B而不是A。
此外,變量一旦經過unset取消之后, 其結果是將整個變量拿掉,而不是取消變量的值。
如下兩行其實是很不一樣的:
$ A= $ unset A第一行只是將變量A設定為"空值"(null value), 但第二行則是讓變量A不存在。 雖然用眼睛來看, 這兩種變量的狀態在如下的命令結果中都是一樣的:
$ A= $ echo $A$ unset A $ echo $A請學員務必能識別null value 與 unset的本質區別, 這在一些進階的變量處理上是很嚴格的。
比方說:
$ str= #設為null $ var=${str=expr} #定義var $ echo $var$ echo $str$ unset str #取消str $ var=${str=expr} #定義var $ echo $var expr $ echo $str expr聰明的讀者(yes, you!),稍加思考的話, 應該不難發現為何同樣的var=${str=expr} 在str為null與unset之下的不同吧? 若你看不出來,那可能是如下原因之一:
- 你太笨了
- 不了解 var=${str=expr} 這個進階處理
- 對本篇說明還沒有來得及消化吸收
- 我講得不好
不知,您選哪個呢?...... ^_^.
##shell十三問之6:exec跟source差在哪?
這次讓我們從CU shell版的一個實例帖子來談起吧: (論壇改版后,原鏈接已經失效)
例中的提問原文如下:
帖子提問:
cd /etc/aa/bb/cc可以執行 但是把這條命令放入shell腳本后,shell腳本不執行! 這是什么原因?
意思是:運行shell腳本,并沒有移動到/etc/aa/bb/cc目錄。
我當時如何回答暫時別去深究,先讓我們了解一下進程 (process)的概念好了。
首先,我們所執行的任何程序,都是父進程(parent process)產生的一個 子進程(child process),子進程在結束后,將返回到父進程去。 此現象在Linux中被稱為fork。
(為何要稱為fork呢? 嗯,畫一下圖或許比較好理解...^_^)
當子進程被產生的時候,將會從父進程那里獲得一定的資源分配、及 (更重要的是)繼承父進程的環境。
讓我們回到上一章所談到的"環境變量"吧: 所謂環境變量其實就是那些會傳給子進程的變量。 簡單而言, "遺傳性"就是區分本地變量與環境變量的決定性指標。 然而,從遺傳的角度來看,我們不難發現環境變量的另一個重要特征: 環境變量只能從父進程到子進程單向傳遞。 換句話說:在子進程中環境如何變更,均不會影響父進程的環境。
接下來,在讓我們了解一下shell腳本(shell script)的概念. 所謂shell script 講起來很簡單,就是將你平時在shell prompt輸入的多行 command line, 依序輸入到一個文件文件而已。
再結合以上兩個概念(process + script),那應該不難理解如下的這句話的意思了: 正常來說,當我們執行一個shell script時,其實是先產生一個sub-shell的子進程, 然后sub-shell再去產生命令行的子進程。 然則,那讓我們回到本章開始時,所提到的例子在重新思考:
帖子提問:
cd /etc/aa/bb/cc可以執行 但是把這條命令放入shell腳本后,shell腳本不執行! 這是什么原因?
意思是:運行shell腳本,并沒有移動到/etc/aa/bb/cc目錄。
我當時的答案是這樣的:
因為,我們一般跑的shell script是用sub-shell去執行的。 從process的概念來看,是 parent process產生一個child process去執行, 當child結束后,返回parent, 但parent的環境是不會因child的改變而改變的。 所謂的環境變量元數很多,如effective id(euid),variable, working dir等等... 其中的working dir($PWD) 正是樓主的疑問所在: 當用sub-shell來跑script的話,sub-shell的$pwd會因為cd而變更, 但返回primary shell時,$PWD是不會變更的。
能夠了解問題的原因及其原理是很好的,但是? 如何解決問題,恐怕是我們更應該感興趣的是吧?
那好,接下來,再讓我們了解一下source命令好了。 當你有了fork的概念之后,要理解soruce就不難:
所謂source,就是讓script在當前shell內執行、 而不是產生一個sub-shell來執行。 由于所有執行結果均在當前shell內執行、而不是產生一個sub-shell來執行。
因此, 只要我們原本單獨輸入的script命令行,變成source命令的參數, 就可輕而易舉地解決前面提到的問題了。
比方說,原本我們是如此執行script的:
$ ./my_script.sh現在改成這樣既可:
$ source ./my_script.sh或者:
$ . ./my_script.sh說到這里,我想,各位有興趣看看/etc底下的眾多設定的文件, 應該不難理解它們被定義后,如何讓其他script讀取并繼承了吧?
若然,日后,你有機會寫自己的script, 應也不難專門指定一個設定的文件以供不同的script一起"共用"了... ^_^
okay,到這里,若你搞懂fork與source的不同, 那接下來再接受一個挑戰:
那exec又與source/fork有何不同呢?
哦...要了解exec或許較為復雜,尤其是扯上File Decscriptor的話... 不過,簡單來說:
exec 也是讓script在同一個進程上執行,但是原有進程則被結束了。 簡言之,原有進程能否終止,就是exec與source/fork的最大差異了。
嗯,光是從理論去理解,或許沒那么好消化, 不如動手"實踐+思考"來得印象深刻哦。
下面讓我們為兩個簡單的script,分別命名為1.sh以及2.sh
1.sh
#!/bin/bash A=B echo "PID for 1.sh before exec/source/fork:$$"export A echo "1.sh: \$A is $A"case $1 inexec)echo "using exec..."exec ./2.sh ;;source)echo "using source...". ./2.sh ;;*)echo "using fork by default..."./2.sh ;; esacecho "PID for 1.sh after exec/source/fork:$$" echo "1.sh: \$A is $A"2.sh
#!/bin/bashecho "PID for 2.sh: $$" echo "2.sh get \$A=$A from 1.sh"A=C export A echo "2.sh: \$A is $A"然后分別跑如下參數來觀察結果:
$ ./1.sh fork $ ./1.sh source $ ./1.sh exec好了,別忘了仔細比較輸出結果的不同及背后的原因哦... 若有疑問,歡迎提出來一起討論討論~~~~
happy scripting! ^_^
##shell十三問之7:()與{}差在哪?
嗯,這次輕松一下,不講太多... ^_^
先說一下,為何要用()或者{}好了。
許多時候,我們在shell操作上,需要在 一定的條件下執行多個命令,也就是說, 要么不執行,要么就全執行,而不是每次 依序的判斷是否要執行下一個命令。
或者,要從一些命令執行的先后次序中得到結果, 如算術運算的2*(3+4)那樣...
這時候,我們就可以引入"命令群組"(command group) 的概念:將許多命令集中處理。
在shell command line中,一般人或許不太計較()與 {}這兩對符號的差異,雖然兩者都可以將多個命令當作群組處理, 但若從技術細節上,卻是很不一樣的:
- () 將command group置于sub-shell(子shell)中去執行,也稱 nested sub-shell。
- {} 則是在同一個shell內完成,也稱non-named command group。
若你對上一章的fork與source的概念還記得的話, 那就不難理解兩者的差異了。
要是在 command group中扯上變量及其他環境的修改, 我們可以根據不同的需求來使用()或{}。 通常而言, 若所作的修改是臨時的,且不想影響原有或以后的設定, 那我們就使用nested sub-shell, 即(); 反之,則用non-named command group, 即{}。
是的,光從command line來看,() 與 {}差別就講完了,夠輕松吧~~~, ^_^
然而,這兩個meta用在其他command meta或領域中(如Regular Expression), 還是有很多差別的。 只是,我不打算再去說明了,留給讀者慢慢發掘好了...
我這里只想補充一個概念,就是function。 所謂function,就是用一個名字去命名一個command group, 然后再調用這個名字去執行command group。
從non-named command group來推斷, 大概你也可以推測到我要說的是{}了吧?(yes! 你真聰明 ^_^)
在bash中,function的定義方式有兩種:
- 方式一:
- 方式二:
用哪一種方式無所謂, 只是碰到所定義的名稱與現有的命令或者別名沖突的話, 方式二或許會失敗。 但方式二起碼可以少打個function這一串英文字符, 對懶人來說(如我),有何樂而不為呢?...^_^
function 在一定程度上來說,也可以稱為"函數", 但請不要與傳統編程所使用的"函數"(library)搞混了, 畢竟兩者差異很大。 唯一相同的是,我們都可以隨時用"已定義的名稱"來調用它們...
若我們在shell操作中,需要不斷地重復某些命令, 我們首先想到的,或許是將命令寫成shell腳本(shell script)。 不過,我們也可以寫成function, 然后在command line中打上function_name就可當一般的shell script使用了。
若只是你在shell中定義的function, 除了用unset function_name取消外, 一旦你退出shell, function也跟著消失。 然而,在script中使用function卻有許多好處, 除了提高整體script的執行性能外(因為已經載入), 還可以節省許多重復的代碼......
簡單而言,若你會將多個命令寫成script以供調用的話, 那你可以將function看成script中script。... ^_^
而且通過上一章節介紹的source命令, 我們可以自行定義許許多多好用的function, 在集中寫在特定文件中, 然后,在其他的script中用source將它們載入,并反復執行。
若你是RedHat Linux的使用者, 或許,已經猜出 /etc/rc.d/init.d/functions這個文件時啥作用了~~~ ^_^
okay,說要輕松點的嘛,那這次就暫時寫到這吧。 祝大家學習愉快,^_^
##shell十三問之8: $(())與$()還有${}差在哪?
我們上一章介紹了()與{}的不同, 這次讓我們擴展一下,看看更多的變化: $()與${}又是啥玩意兒呢?
在bash shell中, $()與``(反引號)都是用來做 命令替換(command substitution)的。
所謂的命令替換與我們第五章學過的變量替換差不多, 都是用來重組命令行: 完成 `` 或者$()里面的 命令,將其結果替換出來, 再重組命令行。
例如:
$ echo the last sunday is $(date -d "last sunday" +%Y-%m-%d)如此便可方便得到上一個星期天的日期了...^_^
在操作上, 用$()或``都無所謂, 只是我個人比較喜歡用$(),理由是:
``(反引號)很容易與''(單引號)搞混亂,尤其對初學者來說。 有時在一些奇怪的字形顯示中,兩種符號是一模一樣的(只取兩點)。 當然了有經驗的朋友還是一眼就能分辨兩者。只是,若能更好的避免混亂, 又何樂而不為呢? ^_^
在多次的復合替換中, ``需要額外的轉義(escape, )處理,而$()則比較直觀。 例如,一個錯誤的使用的例子:
原來的本意是要在command2 `command3` , 先將command3替換出來給command2處理, 然后再將command2的處理結果,給command1來處理。 然而真正的結果在命令行中卻是分成了`command2`與 ``。
正確的輸入應該如下:
command1 `command2 \`command3\` `要不然換成$()就沒有問題了:
command1 $(commmand2 $(command3))只要你喜歡,做多少層的替換都沒有問題~~~^_^
不過,$()并不是沒有弊端的... 首先,``基本上可用在所有的unix shell中使用, 若寫成 shell script,其移植性比較高。 而$()并不是每一種shell都能使用,我只能說, 若你用bash2的話,肯定沒問題... ^_^
接下來,再讓我們看看${}吧...它其實就是用來做 變量替換用的啦。 一般情況下,$var與${var}并沒有啥不一樣。 但是用${}會比較精準的界定變量名稱的范圍, 比方說:
$ A=B $ echo $AB原本是打算先將$A的結果替換出來, 然后在其后補一個字母B; 但命令行上, 真正的結果卻是替換變量名稱為AB的值出來... 若使用${}就沒有問題了:
$ A=B $ echo ${A}B $ BB不過,假如你只看到${}只能用來界定變量名稱的話, 那你就實在太小看bash了。
為了完整起見,我這里再用一些例子加以說明${}的一些 特異功能: 假設我們定義了一個變量file為:
file=/dir1/dir2/dir3/my.file.txt我們可以用${}分別替換獲得不同的值:
####1. shell字符串的非貪婪(最小匹配)左刪除
${file#*/} #其值為:dir1/dir2/dir3/my.file.txt拿掉第一個/及其左邊的字符串,其結果為: dir1/dir2/dir3/my.file.txt 。
${file#*.} #其值為:file.txt拿掉第一個.及其左邊的字符串,其結果為: file.txt 。
####2. shell字符串的貪婪(最大匹配)左刪除:
${file##*/} #其值為:my.file.txt拿掉最后一個/及其左邊的字符串,其結果為: my.file.txt
${file##*.} #其值為:txt拿掉最后一個.及其左邊的字符串,其結果為: txt
####3. shell字符串的非貪婪(最小匹配)右刪除:
${file%/*} #其值為:/dir1/dir2/dir3拿掉最后一個/及其右邊的字符串,其結果為: /dir1/dir2/dir3。
${file%.*} #其值為:/dir1/dir2/dir3/my.file拿掉最后一個.及其右邊的字符串,其結果為: /dir1/dir2/dir3/my.file。
####4. shell字符串的貪婪(最大匹配)右刪除:
${file%%/*} #其值為:其值為空。拿掉第一個/及其右邊的字符串,其結果為: 空串。
${file%%.*} #其值為:/dir1/dir2/dir3/my。拿掉第一個.及其右邊的字符串,其結果為: /dir1/dir2/dir3/my。
Tips:
記憶方法:
#是去掉左邊(在鍵盤上#在$的左邊);
%是去掉右邊(在鍵盤上%在$的右邊);
單個符號是最小匹配;
兩個符號是最大匹配;
####5. shell字符串取子串:
${file:0:5} #提取最左邊的5個字符:/dir1${file:5:5} #提取第5個字符及其右邊的5個字符:/dir2shell字符串取子串的格式:${s:pos:length}, 取字符串s的子串:從pos位置開始的字符(包括該字符)的長度為length的的子串; 其中pos為子串的首字符,在s中位置; length為子串的長度;
Note: 字符串中字符的起始編號為0.
####6. shell字符串變量值的替換:
${file/dir/path} #將第一個dir替換為path:/path1/dir2/dir3/my.file.txt ${file//dir/path} #將全部的dir替換為path:/path1/path2/path3/my.file.txtshell字符串變量值的替換格式:
-
首次替換: ${s/src_pattern/dst_pattern} 將字符串s中的第一個src_pattern替換為dst_pattern。
-
全部替換: ${s//src_pattern/dst_pattern} 將字符串s中的所有出現的src_pattern替換為dst_pattern.
####7. ${}還可針對變量的不同狀態(沒設定、空值、非空值)進行賦值:
-
${file-my.file.txt} #如果file沒有設定,則使用 使用my.file.txt作為返回值, 否則返回${file};(空值及非空值時,不作處理。);
-
${file:-my.file.txt} #如果file沒有設定或者${file}為空值, 均使用my.file.txt作為其返回值,否則,返回${file}.(${file} 為非空值時,不作處理);
-
${file+my.file.txt} #如果file已設定(為空值或非空值), 則使用my.file.txt作為其返回值,否則不作處理。(未設定時,不作處理);
-
${file:+my.file.txt} #如果${file}為非空值, 則使用my.file.txt作為其返回值,否則,(未設定或者為空值時)不作處理。
-
${file=my.file.txt} #如果file為設定,則將file賦值為my.file.txt,同時將${file}作為其返回值;否則,file已設定(為空值或非空值),則返回${file}。
-
${file:=my.file.txt} #如果file未設定或者${file}為空值, 則my.file.txt作為其返回值, 同時,將${file}賦值為my.file.txt,否則,(非空值時)不作處理。
-
${file?my.file.txt} #如果file沒有設定,則將my.file.txt輸出至STDERR, 否側, 已設定(空值與非空值時),不作處理。
-
${file:?my.file.txt} #若果file未設定或者為空值,則將my.file.txt輸出至STDERR,否則, 非空值時,不作任何處理。
Tips:
以上的理解在于,你一定要分清楚,unset與null以及non-null這三種狀態的賦值; 一般而言,與null有關,若不帶:, null不受影響; 若帶 :, 則連null值也受影響。
####8. 計算shell字符串變量的長度:${#var}
${#file} #其值為27, 因為/dir1/dir2/dir3/my.file.txt剛好為27個字符。####9. bash數組(array)的處理方法
接下來,為大家介紹一下bash的數組(array)的處理方法。 一般而言, A="a b c def" 這樣的變量只是將$A替換為一個字符串, 但是改為 A=(a b c def), 則是將$A定義為數組....
#####1). 數組替換方法可參考如下方法:
${A[@]} #方法一 ${A[*]} #方法二以上兩種方法均可以得到:a b c def, 即數組的全部元素。
#####2). 訪問數組的成員:
${A[0]}其中,${A[0]}可得到a, 即數組A的第一個元素, 而 ${A[1]}則為數組A的第二元素,依次類推。
#####3). 數組的length:
${#A[@]} #方法一 ${#A[*]} #方法二 ``` 以上兩種方法均可以得到數組的長度: 4, 即數組的所有元素的個數。回憶一下,針對字符串的長度計算,使用`${#str_var}`; 我們同樣可以將該方法應用于數組的成員: ```shell ${#A[0]}其中,${#A[0]}可以得到:1,即數組A的第一個元素(a)的長度; 同理,${#A[3]}可以得到: 3, 即數組A的第4個元素(def)的長度。
#####4). 數組元素的重新賦值:
A[3]=xyz將數組A的第四個元素重新定義為xyz。
Tips:
諸如此類的...
能夠善用bash的$()與${}可以大大提高及 簡化shell在變量上的處理能力哦~~~^_^
####10. $(())作用:
好了,最后為大家介紹$(())的用途吧: $(())是用來作整數運算的。
在bash中, $(())的整數運算符號大致有這些:
- +- * / #分別為"加、減、乘、除"。
- % #余數運算,(模數運算)
- & | ^ ! #分別為"AND、OR、XOR、NOT"運算。
例如:
$ a=5; b=7; c=2; $ echo $(( a + b * c )) 19 $ echo $(( (a + b)/c )) 6 $ echo $(( (a * b) % c )) 1在$(())中的變量名稱, 可以在其前面加 $符號來替換, 也可以不用,如: $(( $a + $b * $c )) 也可以得到19的結果。
此外,$(())還可作不同進制(如二進制、八進制、十六進制)的運算, 只是輸出結果均為十進制的。
echo $(( 16#2a )) #輸出結果為:42,(16進制的2a)以一個實用的例子來看看吧 : 假如當前的umask是022,那么新建文件的權限即為:
$ umask 022 $ echo "obase=8; $(( 8#666 & (8#777 ^ 8#$(umask)) ))" | bc 644事實上,單純用(())也可以重定義變量值,或作testing:
a=5; ((a++)) #可將$a 重定義為6 a=5; ((a--)) #可將$a 重定義為4 a=5; b=7; ((a< b)) #會得到0 (true)返回值。常見的用于(())的測試符號有如下這些:
| < | 小于號 |
| > | 大于號 |
| <= | 小于或等于 |
| >= | 大于或等于 |
| == | 等于 |
| != | 不等于 |
Note:
使用(())作整數測試時, 請不要跟[]的整數測試搞混亂了。
更多的測試,我們將于第10章為大家介紹。
怎樣? 好玩吧... ^_^
okay,這次暫時說這么多...
上面的介紹,并沒有詳列每一種可用的狀態, 更多的,就請讀者參考手冊文件(man)吧...
##shell十三問之9:$@與$*差在哪?
要說$@與$*之前, 需得先從shell script的positional parameter談起...
我們都已經知道變量(variable)是如何定義和替換的, 這個不再多講了。
1. shell script的positional parameter
但是,我們還需要知道有些變量是shell內定的, 且其名稱是我們不能隨意修改的。 其中,就有positional parameter在內。
在shell script中,我們可用$0, $1, $2, $3 ... 這樣的變量分別提取命令行中的如下部分:
script_name parameter1 parameter2 parameter3 ...我們很容易就能猜出, $0就是代表 shell script名稱(路徑)本身, 而$1就是其后的第一個參數,如此類推...
須得留意的是IFS的作用, 也就是IFS被quoting處理后, 那么positional parameter也會改變。
如下例:
my.sh p1 "p2 p3" p4由于p2與p3之間的空白鍵被soft quoting所關閉了, 因此,my.sh的中$2是"p2 p3",而$3則是p4...
還記得前兩章,我們提到function時, 我們不是說過,它是script中的script嗎?^_^
是的,function一樣可以讀取自己的(有別于script的) positional parameter, 唯一例外的是$0而已。
舉例而言: 假設my.sh里有一個函數(function)叫my_fun, 若在script中跑my_fun fp1 fp2 fp3, 那么,function內的$0就是my.sh,而$1是fp1而不是p1了...
不如寫個簡單的my.sh script 看看吧:
#!/bin/bashmy_fun() {echo '$0 inside function is '$0echo '$1 inside function is '$1echo '$2 inside function is '$2 }echo '$0 outside function is '$0 echo '$1 outside function is '$1 echo '$2 outside function is '$2my_fun fp1 "fp2 fp3"然后在command line中跑一下 script就知道了:
chmod 755 my.sh./my.sh p1 "p2 p3" $0 outside function is ./my.sh $1 outside function is p1 $2 outside function is p2 p3 $0 inside function is ./my.sh $1 inside function is fp1 $2 inside function is fp2 fp3然而,在使用positional parameter的時候, 我們要注意一些陷阱哦:
$10不是替換第10個參數, 而是替換第一個參數,然后在補一個0于其后;
也就是說, my.sh one two three four five six seven eight nine ten 這樣的command line, my.sh里的$10不是ten而是one0 哦...小心小心 要抓到ten的話,有兩種方法:
-
方法一:使用我們上一章介紹的${}, 也就是用${10}即可。
-
方法二:就是shift了。
用通俗的說法來說, 所謂的shift就是取消positional parameter中最左邊的參數($0不受影響)。 其預設值為1,也就是shift 或shift 1 都是取消$1, 而原本的$2則變成$1, $3則變成$2... 那親愛的讀者,你說要shift掉多少個參數, 才可用$1取得到${10} 呢? ^_^
okay,當我們對positional parameter有了基本的概念之后, 那再讓我們看看其他相關變量吧。
2. shell script的positional parameter的number
先是$#, 它可抓出positional parameter的數量。 以前面的my.sh p1 "p2 p3"為例: 由于"p2 p3"之間的IFS是在soft quote中, 因此,$#就可得到的值是2. 但如果p2與p3沒有置于quoting中話, 那$#就可得到3的值了。 同樣的規則,在function中也是一樣。
因此,我們常在shell script里用如下方法, 測試script是否有讀進參數:
[ $# = 0 ]假如為0, 那就表示script沒有參數,否則就是帶有參數...
3. shell script中的$@與$*
接下來就是**$@與$*: 精確來講,兩者只有在soft quote中才有差異, 否則,都表示“全部參數” ($0除外)**。
若在comamnd line上, 跑my.sh p1 "p2 p3" p4的話, 不管$@還是$*, 都可得到 p1 p2 p3 p4就是了。
但是,如果置于soft quote中的話:
- "$@"則可得到 "p1" "p2 p3" "p4" 這三個不同字段(word);
- "$*"則可得到 "p1 p2 p3 p4" 這一整個單一的字段。
我們修改一下前面的my.sh,使之內容如下:
#!/bin/bashmy_fun() {echo "$#" }echo 'the number of parameter in "$@" is ' $(my_fun "$@") echo 'the number of parameter in "$*" is ' $(my_fun "$*")然后再執行:
./my.sh p1 "p2 p3" p4就知道,$@與$*差在哪了... ^_^
##shell十三問之10:&& 與 || 差在哪?
好不容易,進入了兩位數的章節了... 一路走來,很辛苦吧?也很快樂吧? ^_^
在解答本章題目之前,先讓我們了解一個概念: return value。
我們在shell下跑的每一個command或function, 在結束的時候都會傳回父進程一個值,稱為 return value。
在shell command line中可用$?, 這個變量得到最"新"的一個return value, 也就是剛剛結束的那個進程傳回的值。
Return Value(RV)的取值為0-255之間, 由進程或者script的作者自行定義:
-
若在script里,用exit RV 來指定其值; 若沒有指定, 在結束時,以最后一個命令的RV,為script的RV值。
-
若在function里,則用return RV 來代替exit RV即可。
Return Value的作用:用來判斷進程的退出狀態(exit status). 進程的退出狀態有兩種:
- 0值為"真"(true)
- 非0值為"假"(false)
舉個例子來說明好了: 假設當前目錄內有一個my.file的文件, 而no.file是不存在的:
$ touch my.file $ ls my.file $ echo $? #first echo 0 $ ls no.file ls: no.file: No such file or directory $ echo $? #second echo 1 $ echo $? #third echo 0上例的:
- 第一個echo是關于ls my.file的RV,可得到0的值,因此為true。
- 第二個echo是關于ls no.file的RV,得到非0的值,因此為false。
- 第三個echo是關于echo $?的RV,得到0值, 因此為true。
請記住: 每一個command在結束時,都會返回return value,不管你跑什么命令... 然而,有一個命令卻是“專門”用來測試某一條而返回return value, 以供true或false的判斷, 它就是test命令。
若你用的是bash, 請在command line下, 打man test,或者 man bash 來了解這個test的用法。 這是你可用作參考的最精準的文件了,要是聽別人說的,僅作參考就好...
下面,我只簡單作一些輔助說明,其余的一律以 man為準: 首先,test的表達式,我們稱為expression,其命令格式有兩種:
test expression或者
[ expression ]Note:
請務必注意 [] 之間的空白鍵!
用哪一種格式無所謂,都是一樣的效果。 (我個人比較喜歡后者...)
其次,bash的test目前支持的測試對象只有三種:
- string:字符串,也就是純文字。
- integer:整數(0或正整數、不含負數或小數)
- file: 文件
請初學者,一定要搞清楚這三者的差異, 因為test所使用的expression是不一樣的。
以A=123這個變量為例:
-
[ "$A" = 123 ] #是字符串測試,測試$A是不是1、2、3這三個字符。
-
[ "$A" -eq 123 ] #是整數測試,以測試$A是否等于123.
-
[-e "$A" ] #文件測試,測試123這份文件是否存在.
第三, 當expression測試為“真”時, test就返回0(true)的return value; 否則,返回非0(false).
若在 expression 之前加一個!(感嘆號),則在expression為假時,return value為0, 否則, return value 為非0值。
同時,test也允許多重復合測試:
- expression1 -a expression2 #當兩個expression都為true,返回0,否則,返回非0;
- expression1 -o expression2 #當兩個expression均為false時,返回非0,否則,返回0;
例如:
[ -d "$file" -a -x "$file" ]表示當$file是一個目錄,且同時具有x權限時,test才會為true。
第四,在command line中使用test時,請別忘記命令行的“重組”特性, 也就是在碰到meta時,會先處理meta,在重新組建命令行。 (這個概念在第2章和第4章進行了反復強調)
比方說, 若test碰到變量或者命令替換時, 若不能滿足 expression的格式時,將會得到語法錯誤的結果。
舉例來說好了:
關于[ string1 = string2 ]這個test格式, 在等號兩邊必須要有字符串,其中包括空串(null串,可用soft quote或者hard quote取得)。
假如$A目前沒有定義,或被定義為空字符串的話, 那如下的用法將會失敗:
$ unset A $ [ $A = abc ] [: =: unary oprator expected這是因為命令行碰到$這個meta時,會替換$A的值, 然后,再重組命令行,那就變成了: [ = abc ], 如此一來,=的左邊就沒有字符串存在了, 因此,造成test的語法錯誤。 但是,下面這個寫法則是成立的。
$ [ "$A" = abc ] $ echo $? 1這是因為命令行重組后的結果為: [ "" = abc ], 由于等號的左邊我們用soft quote得到一個空串, 而讓test的語法得以通過...
讀者諸君,請務必留意這些細節哦, 因為稍一不慎,將會導致test的結果變了個樣。 若您對test還不是很有經驗的話, 那在使用test時,不妨先采用如下這一個"法則":
** 若在test中碰到變量替換,用soft quote是最保險的***。
若你對quoting不熟的話,請重新溫習第四章的內容吧...^_^
okay, 關于更多的test的用法,老話一句:請看其man page (man test)吧!^_^
雖然洋洋灑灑讀了一大堆,或許你還在嘀咕...那...那個return value有啥用?
問得好: 告訴你:return value的作用可大了, 若你想要你的shell變"聰明"的話,就全靠它了: 有了return value, 我們可以讓shell根據不同的狀態做不同的事情...
這時候,才讓我來揭曉本章的答案吧~~~~^_^
&& 與 || 都是用來"組建" 多個command line用的;
- command1 && command2 # command2只有在command1的RV為0(true)的條件下執行。
- command1 || command2 # command2 只有在command1的RV為非0(false)的條件下執行。
以例子來說好了:
$ A=123 $ [ -n "$A" ] && echo "yes! it's true." yes! it's true. $ unset A $ [ -n "$A" ] && echo "yes! it's true." $ [ -n "$A" ] || echo "no, it's Not true." no, it's Not trueNote:
[ -n string ]是測試string長度大于0, 則為true。
上例中,第一個&&命令之所以會執行其右邊的echo命令, 是因為上一個test返回了0的RV值; 但第二個,就不會執行,因為test返回了非0的結果... 同理,||右邊的echo會被執行,卻正是因為左邊的test返回非0所引起的。
事實上,我們在同一個命令行中,可用多個&& 或 || 來組建呢。
$ A=123 $ [ -n "$A" ] && echo "yes! it's true." || echo "no, it's Not ture." yes! it's true. $ unset A $ [ -n "$A" ] && echo "yes! it's true." || echo "no, it's Not ture." no, it's Not true怎樣,從這一刻開始,你是否覺得我們的shell是“很聰明”的呢? ^_^
好了,最后布置一道練習題給大家做做看: 下面的判斷是:當$A被賦值時,在看看其是否小于100,否則輸出too big!
$ A=123 $ [ -n "$A" ] && [ "$A" -lt 100 ] || echo 'too big!' $ too big!若我取消A,照理說,應該不會輸出文字啊,(因為第一個條件不成立)。
$ unset A $ [ -n "$A" ] && [ "$A" -lt 100 ] || echo 'too big!' $ too big!為何上面的結果也可得到呢? 又如何解決呢?
Tips:
修改的方法有很多種, 其中一種方法可以利用第7章中介紹過 command group...
快告訴我答案,其余免談....
解決方法1:sub-shell:
$ unset A $ [ -n "$A" ] && ( [ "$A" -lt 100 ] || echo 'too big!' )解決方法二:command group:
$ unset A $ [ -n "$A" ] && { [ "$A" -lt 100 ] || echo 'too big!'}##shell十三問之11:>與< 差在哪?
這次的題目,之前我在CU的shell版說明過了: (原帖的連接在論壇改版后,已經失效) 這次我就不重寫了,將帖子的內容“抄”下來就是了...
1. 文件描述符(fd, File Descriptor)
談到I/O redirection,不妨先讓我們認識一下File Descriptor(fd,文件描述符)。
進程的運算,在大部分情況下,都是進行數據(data)的處理, 這些數據從哪里,讀進來?又輸出到哪里呢? 這就是file descriptor(fd)的功用了。
在shell的進程中,最常使用的fd大概有三個,分別為:
- 0:standard Input (STDIN)
- 1: standard output(STDOUT)
- 2: standard Error output (STDERR)
在標準情況下,這些fd分別跟如下設備(device)關聯:
- stdin(0): keyboard
- stdout(1): monitor
- stderr(2): monitor
Tips: linux中的文件描述符(fd)用整數表示。 linux中任何一個進程都默認打開三個文件, 這三個文件對應的文件描述符分別是:0, 1, 2; 即stdin, stdout, stderr.
我們可以用如下命令測試一下:
$ mail -s test root this is a test mail。 please skip. ^d (同時按下ctrl 跟d鍵)很明顯,mail進程所讀進的數據,就是從 stdin 也就是keyboard讀進的。 不過,不見得每個進程的stdin都跟mail一樣 從keyboard讀進,因為進程的作者可以從文件參數讀進stdin, 如:
$ cat /etc/passwd但,要是cat之后沒有文件參數則如何呢? 哦, 請你自己玩玩看...^_^
$ catTips:
請留意數據輸出到哪里去了, 最后別忘了按ctrl+d(^d), 退出stdin輸入。
至于stdout與stderr,嗯...等我有空再續吧...^_^ 還是,有哪位前輩來玩接龍呢?
相信,經過上一個練習后, 你對stdin與stdout應該不難理解了吧? 然后,讓我們看看stderr好了。
事實上,stderr沒什么難理解的: 說白了就是“錯誤信息”要往哪里輸出而已... 比方說, 若讀進的文件參數不存在的, 那我們在monitor上就看到了:
$ ls no.such.file ls: no.such.file: No such file or directory若同一個命令,同時成生stdout與stderr呢? 那還不簡單,都送到monitor來就好了:
$ touch my.file $ ls my.file on.such.file ls: no.such.file: No such file or directory my.fileokay, 至此,關于fd及其名稱、還有相關聯的設備, 相信你已經沒問題了吧?
2. I/O 重定向(I/O Redirection)
那好,接下來讓我們看看如何改變這些fd的預設數據通道。
- 用< 來改變讀進的數據通道(stdin),使之從指定的文件讀進。
- 用> 來改變輸出的數據通道(stdout,stderr),使之輸出到指定的文件。
#####2.1 輸入重定向n<(input redirection)
比方說:
$ cat < my.file就是從my.file讀入數據
$ mail -s test root < /etc/passwd則是從/etc/passwd讀入...
這樣一來,stdin將不再是從keyboard讀入, 而是從指定的文件讀入了...
嚴格來說,<符號之前需要指定一個fd的(之前不能有空白),但因為0是<的預設值,因此,<與0<是一樣的*。
okay,這樣好理解了吧?
那要是用兩個<,即<<又是啥呢? 這是所謂的here document, 它可以讓我們輸入一段文本, 直到讀到<< 后指定的字符串。
比方說:
$ cat <<EOF first line here second line here third line here EOF這樣的話, cat會讀入3個句子, 而無需從keyboard讀進數據且要等到(ctrl+d, ^d)結束輸入。
#####2.2 重定向輸出>n(output redirection)
當你搞懂了0< 原來就是改變stdin的數據輸入通道之后, 相信要理解如下兩個redirection就不難了:
- 1> #改變stdout的輸出通道;
- 2> #改變stderr的輸出通道;
兩者都是將原來輸出到monitor的數據, 重定向輸出到指定的文件了。
由于1是>的預設值, 因此,1>與>是相同的,都是改變stdout.
用上次的ls的例子說明一下好了:
$ ls my.file no.such.file 1>file.out ls: no.such.file: No such file or directory這樣monitor的輸出就只剩下stderr的輸出了, 因為stdout重定向輸出到文件file.out去了。
$ ls my.file no.such.file 2>file.err my.file這樣monitor就只剩下了stdout, 因為stderr重定向輸出到文件file.err了。
$ ls my.file no.such.file 1>file.out 2>file.err這樣monitor就啥也沒有了, 因為stdout與stderr都重定向輸出到文件了。
呵呵,看來要理解>一點也不難啦是不? 沒騙你吧? ^_^ 不過有些地方還是要注意一下的。
$ ls my.file no.such.file 1>file.both 2>file.both假如stdout(1)與stderr(2)都同時在寫入file.both的話, 則是采取"覆蓋"的方式:后來寫入覆蓋前面的。
讓我們假設一個stdout與stderr同時寫入到file.out的情形好了;
- 首先stdout寫入10個字符
- 然后stderr寫入6個字符
那么,這時原本的stdout輸出的10個字符, 將被stderr輸出的6個字符覆蓋掉了。
那如何解決呢?所謂山不轉路轉,路不轉人轉嘛, 我們可以換一個思維: 將stderr導進stdout 或者將stdout導進到stderr, 而不是大家在搶同一份文件,不就行了。 bingo就是這樣啦:
- 2>&1 #將stderr并進stdout輸出
- 1>&2 或者 >&2 #將stdout并進stderr輸出。
于是,前面的錯誤操作可以改寫為:
$ ls my.file no.such.file 1>file.both 2>&1 $ ls my.file no.such.file 2>file.both >&2這樣,不就皆大歡喜了嗎? ~~~ ^_^
不過,光解決了同時寫入的問題還不夠, 我們還有其他技巧需要了解的。 故事還沒有結束,別走開廣告后,我們在回來....
#####2.3 I/O重定向與linux中的/dev/null
okay,這次不講I/O Redirection, 請佛吧... (有沒有搞錯?網中人是否頭殼燒壞了?...)嘻~~~^_^
學佛的最高境界,就是"四大皆空"。 至于是空哪四大塊,我也不知,因為我還沒有到那個境界.. 這個“空”字,卻非常值得反復把玩: ---色即是空,空即是色 好了,施主要是能夠領會"空"的禪意,那離修成正果不遠了。
在linux的文件系統中,有個設備文件: /dev/null. 許多人都問過我,那是什么玩意兒? 我跟你說好了,那就是"空"啦。
沒錯空空如也的空就是null了... 請問施主是否忽然有所頓悟了呢? 然則恭喜了。
這個null在 I/O Redirection中可有用的很呢?
- 將fd 1跟fd 2重定向到/dev/null去,就可忽略stdout, stderr的輸出。
- 將fd 0重定向到/dev/null,那就是讀進空(nothing).
比方說,我們在執行一個進程時,會同時輸出到stdout與stderr, 假如你不想看到stderr(也不想存到文件), 那就可以:
$ ls my.file no.such.file 2>/dev/null my.file若要相反:只想看到stderr呢? 還不簡單將stdout,重定向的/dev/null就行:
$ ls my.file no.such.file >/dev/null ls: no.such.file: No such file or directory那接下來,假如單純的只跑進程,而不想看到任何輸出呢? 哦,這里留了一手,上次沒講的法子,專門贈與有緣人... ^_^ 除了用 >/dev/null 2>&1之外,你還可以如此:
$ ls my.file no.such.file &>/dev/nullTips:
將&>換成>&也行!
#####2.4 重定向輸出append (>>)
okay? 請完佛,接下來,再讓我們看看如下情況:
$ echo "1" > file.out $ cat file.out 1 $ echo "2" > file.out $ cat file.out 2看來,我們在重定向stdout或stderr進一個文件時, 似乎永遠只能獲得最后一次的重定向的結果. 那之前的內容呢?
呵呵,要解決這個問題,很簡單啦,將>換成>> 就好了;
$ echo "3" >> file.out $ cat file.out 2 3如此一來,被重定向的文件的之前的內容并不會丟失, 而新的內容則一直追加在最后面去。so easy?...
但是,只要你再次使用>來重定向輸出的話, 那么,原來文件的內容被truncated(清洗掉)。 這是,你要如何避免呢? ----備份, yes,我聽到了,不過,還有更好的嗎? 既然與施主這么有緣分,老衲就送你一個錦囊妙法吧:
$ set -o noclobber $ echo "4" > file.out -bash:file: cannot overwrite existing file.那,要如何取消這個限制呢? 哦,將set -o換成 set +o就行了:
$ set +o noclobber $ echo "5" > file.out $ cat file.out 5再問:那有辦法不取消而又“臨時”改寫目標文件嗎? 哦,佛曰:不可告也。 啊,~開玩笑的,開玩笑啦~^_^, 哎,早就料到人心是不足的了
$ set -o noclobber $ echo "6" >| file.out $ cat file.out 6留意到沒有: 在>后面加個|就好, 注意: >與|之間不能有空白哦...
#####2.5 I/O Redirection的優先級
呼....(深呼吸吐納一下吧)~~~ ^_^ 再來還有一個難題要你去參透呢:
$ echo "some text here" >file $ cat < file some text here $cat < file >file.bak $cat < file.bak some text here $cat < file >file嗯?注意到沒有? ---怎么最后那個cat命令看到file是空的呢? why? why? why?
前面提到:$cat < file > file之后, 原本有內容的文件,結果卻被清空了。 要理解這個現象其實不難, 這只是priority的問題而已: ** 在IO Redirection中, stdout與stderr的管道先準備好, 才會從stdin讀入數據。** 也就是說,在上例中,>file會將file清空, 然后才讀入 < file。 但這時候文件的內容已被清空了,因此就變成了讀不進任何數據。
哦,~原來如此~^_^ 那...如下兩例又如何呢?
$ cat <> file $ cat < file >>file嗯...同學們,這兩個答案就當練習題嘍, 下課前交作業。
Tips: 我們了解到>file能夠快速把文件file清空; 或者使用:>file同樣可以清空文件, :>file與>file的功能: 若文件file存在,則將file清空; 否則,創建空文件file (等效于touch file); 二者的差別在于>file的方式不一定在所有的shell的都可用。
exec 5<>file; echo "abcd" >&5; cat <&5 將file文件的輸入、輸出定向到文件描述符5, 從而描述符5可以接管file的輸入輸出; 因此,cat <>file等價于cat < file。
而cat < file >>file則使file內容成幾何級數增長。
好了, I/O Redirection也快講完了, sorry,因為我也只知道這么多而已啦~嘻~^_^ 不過,還有一樣東東是一定要講的,各位觀眾(請自行配樂~!#@$%): 就是pipe line也。
#####2.6 管道(pipe line)
談到pipe line,我相信不少人都不會陌生: 我們在很多command line上常看到|符號就是pipe line了。
不過,pipe line究竟是什么東東呢? 別急別急...先查一下英文字典,看看pipe是什么意思? 沒錯他就是“水管”的意思... 那么,你能想象一下水管是怎樣一個根接一根的嗎? 又, 每根水管之間的input跟output又如何呢? 靈光一閃:原來pipe line的I/O跟水管的I/O是一模一樣的: 上一個命令的stdout接到下一個命令的stdin去了 的確如此。不管在command line上使用了多少個pipe line, 前后兩個command的I/O是彼此連接的 (恭喜:你終于開放了 ^_^ )
不過...然而...但是... ...stderr呢? 好問題不過也容易理解: 若水管漏水怎么辦? 也就是說:在pipe line之間, 前一個命令的stderr是不會接進下一個命令的stdin的, 其輸出,若不用2>file的話,其輸出在monitor上來。 這點請你在pipe line運用上務必要注意的。
那,或許你有會問: 有辦法將stderr也喂進下一個命令的stdin嗎? (貪得無厭的家伙),方法當然是有的,而且,你早已學習過了。 提示一下就好:**請問你如何將stderr合并進stdout一同輸出呢? 若你答不出來,下課后再來問我...(如果你臉皮足夠厚的話...)
或許,你仍意猶未盡,或許,你曾經碰到過下面的問題: 在cmd1 | cmd2 | cmd3 | ... 這段pipe line中如何將cmd2的輸出保存到一個文件呢?
若你寫成cmd1 | cmd2 >file | cmd3的話, 那你肯定會發現cmd3的stdin是空的,(當然了,你都將 水管接到別的水池了) 聰明的你或許會如此解決:
cmd1 | cmd2 >file; cmd3 < file是的,你可以這樣做,但最大的壞處是: file I/O會變雙倍,在command執行的整個過程中, file I/O是最常見的最大效能殺手。 凡是有經驗的shell操作者,都會盡量避免或降低file I/O的頻度。
那上面問題還有更好的方法嗎? 有的,那就是tee命令了。 所謂的tee命令是在不影響原本I/O的情況下, 將stdout賦值到一個文件中去。 因此,上面的命令行,可以如此執行:
cmd1 | cmd2 | tee file | cmd3在預設上,tee會改寫目標文件, 若你要改為追加內容的話,那可用-a參數選項。
基本上,pipe line的應用在shell操作上是非常廣泛的。 尤其是在text filtering方面, 如,cat, more, head, tail, wc, expand, tr, grep, sed, awk...等等文字處理工具。 搭配起pipe line 來使用,你會覺得 command line 原來活得如此精彩的。 常讓人有“眾里尋他千百度,驀然回首,那人卻在燈火闌珊處”之感...
好了,關于I/O Redirection的介紹就到此告一段落。 若日后,有空的話,在為大家介紹其他在shell上好玩的東西。
##shell十三問之12:你要if還是case呢?
還記得我們在第10章所介紹的return value嗎?
是的,接下來的介紹的內容與之有關, 若你的記憶也被假期所抵消的話, 那建議您還是回去溫習溫習再回來...
若你記得return value,我想你也應該記得了 && 與 || 什么意思吧? 用這兩個符號再搭配 command group的話, 我們可讓shell script變得更加聰明哦。 比方說:
cmd1 && {cmd2cmd3; } || {cmd4cmd5 }意思是說: 若 cmd1的return value為true的話, 然后執行cmd2與cmd3, 否則執行cmd4與cmd5.
事實上, 我們在寫shell script的時候, 經常需要用到這樣、那樣的條件 以作出不同的處理動作。 用&&與||的確可以達成條件執行的結果, 然而,從“人類語言”上來理解, 卻不是那么直觀。 更多時候,我們還是喜歡用if...then...else... 這樣的的keyword來表達條件執行。
在bash shell中,我們可以如此修改上一段代碼:
if cmd1 thencmd2cmd3 elsecmd4cmd5 fi這也是我們在shell script中最常用的if判斷式: 只要if后面的command line返回true的return value (我們常用test命令返回的return value), 然則就執行then后面的命令,否則,執行else之后的命令, fi則是用來結束判斷式的keyword。
在if的判斷式中,else部分可以不用,但then是必需的。 (若then后不想跑任何command,可用:這個null command代替)。 當然,then或else后面,也可以再使用更進一層的條件判斷式, 這在shell script的設計上很常見。 若有多項條件需要"依序"進行判斷的話, 那我們則可使用elif這樣的keyword:
if cmd1; thencmd2; elif cmd3; thencmd4 elsecmd5 fi意思是說: 若cmd1為true,然則執行cmd2; 否則在測試cmd3,若為true則執行cmd4; 倘若cmd1與cmd3均不成立,那就執行cmd5。
if判斷式的例子很常見,你可從很多shell script中 看得到,我這里不再舉例子了...
接下來為要為大家介紹的是case判斷式。 雖然if判斷式已可應付大部分的條件執行了, 然而,在某些場合中,卻不夠靈活, 尤其是在string式樣的判斷上,比方如下:
QQ() {echo -n "Do you want to continue? (Yes/No): "read YNif [ "$YN" = Y -o "$YN" = y -o "$YN" = "Yes" -o "$YN" = "yes" -o "$YN" = YES]thenQQelseexit 0fi }QQ從例中,我們看得出來, 最麻煩的部分是在判斷YN的值可能有好幾種樣式。
聰明的你或許會如此修改:
QQ() {echo -n "Do you want to continue? (Yes/No): "read YNif echo "$YN" | grep -q '^[Yy]\([Ee][Ss]\)*$'thenQQelseexit 0fi }QQ也就是用Regular Expression來簡化代碼。 (我們有機會,再來介紹RE) 只是...是否有其他更方便的方法呢? 有的,就是用case判斷式即可:
QQ() {echo -n "Do you want to continue? (Yes/No): "read YNcase "$YN" in[Yy]|[Yy][Ee][Ss])QQ;;*)exit 0;;esac }QQ我們常用的case的判斷式來判斷某一變量 在不同的值(通常是string)時,作出不同的處理, 比方說, 判斷script參數,以執行不同的命令。
若你有興趣,且用linux系統的話, 不妨挖一挖/etc/init.d/*中的那堆script中的case用法. 如下就是一例:
case "$1" instart)start;;stop)stop;;status)rhstatus;;restart|reload)restart;;condrestart)[ -f /var/lock/subsys/syslog ] && restart || :;;*)echo $"Usage: $0 {start|stop|status|restart|condrestart}"exit 1 esac(若你對 postional parameter的印象已經模糊了,請重看第9章吧。)
okay,是十三問還剩一問而已,過幾天再來搞定之...^_^
##shell十三問之13: for what? while與until差在哪?
終于,來到了shell十三問的最后一問了... 長長吐一口氣~~~~
最后要介紹的是shell script設計中常見的循環(loop). 所謂的loop就是script中的一段在一定條件下反復執行的代碼。
bash shell中常用的loop有如下三種:
- for
- while
- until
###1. for loop
for loop 是從一個清單列表中讀進變量的值, 并依次的循環執行do到done之間的命令行。 例:
for var in one two three four five doecho -----------------echo '$var is '$varecho done上例的執行結果將會是:
我們不難看出,在for loop中,變量值的多寡,決定循環的次數。 然而,變量在循環中是否使用則不一定,得視設計需求而定。 倘若for loop沒有使用in這個keyword來制變量清單的話,其值將從 $@(或$*)中繼承:
for var; do...... doneTips:
若你忘記了`positional parameter, 請溫習第9章...
for loop用于處理“清單”(list)項目非常方便, 其清單除了明確指定或從postional parameter取得之外, 也可以從變量替換或者命令替換取得... (再一次提醒:別忘了命令行的“重組”特性) 然而,對于一些“累計變化”的項目(整數的加減),for也能處理:
for ((i = 1; i <= 10; i++)) doecho "num is $i" done###2. while loop
除了for loop, 上面的例子, 我們也可改用while loop來做到:
num=1 while [ "$num" -le 10 ]; doecho "num is $num"num=$(($num + 1)) donewhile loop的原理與for loop稍有不同: 它不是逐次處理清單中的變量值, 而是取決于while 后面的命令行的return value:
- 若為true, 則執行do與done之間的命令, 然后重新判斷while后的return value。
- 若為false,則不再執行do與done之間的命令而結束循環。
分析上例:
我們不難發現: 若while的測試結果永遠為true的話,那循環將一直永久執行下去:
while:; doecho looping... done上面的**:是bash的null command,不做任何動作, 除了返回true的return value**。 因此這個循環不會結束,稱作死循環。
死循環的產生有可能是故意設計的(如跑daemon), 也可能是設計的錯誤。
若要結束死循環,可通過signal來終止(如按下ctrl-c). (關于process與signal,等日后有機會再補充,十三問略過。)
####3.until loop
一旦你能夠理解while loop的話,那就能理解until loop: **與while相反, until是在return value 為false時進入循環,否則,結束。 因此,前面的例子,我們也可以輕松的用until來寫:
num=1 until [ ! "$num" -le 10 ]; doecho "num is $num"num=$(($num + 1)) done或者:
num=1until [ "$num" -gt 10 ]; doecho "num is $num"num=$(($num + 1)) doneokay, 關于bash的三個常用的loop暫時介紹到這里。
###4. shell loop中的break與continue
在結束本章之前,再跟大家補充兩個loop有關的命令:
- break
- continue 這兩個命令常用在復合式循環里, 也就是do ... done之間又有更進一層的loop, 當然,用在單一循環中也未嘗不可啦... ^_^
break用來中斷循環,也就是強迫結束循環。 若break后面指定一個數值n的話,則從里向外中斷第n個循環, 預設值為 break 1,也就是中斷當前循環。 在使用break時,需要注意的是,它與return及exit是不同的:
- break是結束loop;
- return是結束function;
- exit是結束script/shell;
而continue則與break相反:強迫進入下一次循環動作.
若你理解不來的話,那你可簡單的看成: 在continue在done之間的句子略過而返回到循環的頂端...
與break相同的是:continue后面也可以指定一個數值n, 以決定繼續哪一層(從里往外計算)的循環, 預設值為 continue 1,也就是繼續當前的循環。
在shell script設計中,若能善用loop, 將能大幅度提高script在復雜條件下的處理能力。 請多加練習吧...
shell是十三問的總結語
好了,該是到了結束的時候了。 婆婆媽媽地跟大家啰嗦了一堆shell的基礎概念。
目的不是要告訴大家“答案”,而是要帶給大家“啟發”...
在日后的關于shell的討論中,我或許經常用"連接"的方式 指引十三問中的內容。
以便我們在進行技術探討時,彼此能有一些討論的基礎, 而不至于各說各話、徒費時力。
但更希望十三問能帶給你更多的思考與樂趣, 至為重要的是通過實踐來加深理解。
是的,我很重視實踐與獨立思考這兩項學習要素。
若你能夠掌握其中的真諦,那請容我說聲: 恭喜十三問你沒白看了 ^_^
p.s. 至于補充問題部分,我暫時不寫了。 而是希望:
Good luck and happy studing!
##shell十三問原作者**網中人**簽名中的bash的fork bomb
最后,Markdown整理者補上本書的原作者網中人的個性簽名:
** 君子博學而日叁省乎己,則知明而行無過矣。**
一個能讓系統shell崩潰的shell 片段:
:() { :|:& }; : # <--- 這個別亂跑!好奇會死人的! echo '十人|日一|十十o' | sed 's/.../&\n/g' # <--- 跟你講就不聽,再跑這個就好了...原來是一個bash的fork炸彈:ref:http://en.wikipedia.org/wiki/Fork_bomb
:() {:|:& } :代碼分析:
(即除最后一行外)
定義了一個 shell 函數,函數名是:,
而這個函數體執行一個后臺命令:|:
即冒號命令(或函數,下文會解釋)的輸出 通過管道再傳給冒號命令做輸入
最后一行執行“:”命令
在各種shell中運行結果分析:
這個代碼只有在 bash 中執行才會出現不斷創建進程而耗盡系統資源的嚴重后果;
在 ksh (Korn shell), sh (Bourne shell)中并不會出現,
在 ksh88 和傳統 unix Bourne shell 中冒號不能做函數名,
即便是在 unix-center freebsd 系統中的 sh 和 pdksh(ksh93 手邊沒有,沒試)中冒號可以做函數名,但還是不會出現那個效果。
原因是 sh、ksh 中內置命令的優先級高于函數,所以執行“:”, 總是執行內置命令“:”而不是剛才定義的那個恐怖函數。
但是在 bash 中就不一樣,bash 中函數的優先級高于內置命令, 所以執行“:”結果會導致不斷的遞歸,而其中有管道操作, 這就需要創建兩個子進程來實現,這樣就會不斷的創建進程而導致資源耗盡。
眾所周知,bash是一款極其強大的shell,提供了強大的交互與編程功能。
這樣的一款shell中自然不會缺少“函數”這個元素來幫助程序進行模塊化的高效開發與管理。 于是產生了由于其特殊的特性,bash擁有了fork炸彈。
Jaromil在2002年設計了最為精簡的一個fork炸彈的實現。
所謂fork炸彈是一種惡意程序,它的內部是一個不斷在fork進程的無限循環.
fork炸彈并不需要有特別的權限即可對系統造成破壞。
fork炸彈實質是一個簡單的遞歸程序。
由于程序是遞歸的,如果沒有任何限制,
這會導致這個簡單的程序迅速耗盡系統里面的所有資源.
##shell十三問之14: [^ ] 跟[! ]差在哪? (wildcard)
這個題目說穿了, 就是要探討Wildcard與Regular Expression的差別的。 這也是很多初學shell的朋友很容易混淆的地方。
首先,讓我們回到十三問之第2問, 再一次將我們提到的command line format 溫習一次:
command_name options arguments同時,也再來理解一下,我在第5章所提到的變量替換的特性:
先替換,再重組 command line!有了這個兩個基礎后,再讓我們來看Wildcard是什么回事吧。
Part-I Wildcard (通配符)
首先,
`Wildcard` 也是屬于 `command line` 的處理工序,作用于 `arguments` 里的 `path` 之上。沒錯,它不用在command_name,也不用在options上。 而且,若argument不是path的話,那也與wildcard無關。
換句更為精確的定義來講,
`wildcard`是一種命令行的路徑擴展(path expansion)功能。提到這個擴展,那就不要忘了 command line的“重組”特性了!
是的,這與變量替換(variable subtitution)及 命令替換(command substitution)的重組特性是一樣的。
也就是在wildcard進行擴展后, 命令行會先完成重組,才會交給shell來處理。
了解了wildcard的擴展與重組特性后, 接下來,讓我們了解一些常見的wildcard吧。
| * | 匹配0個或多個字符 |
| ? | 匹配任意單一字符 |
| [list] | 匹配list中任意單一字符 |
| [!list] | 匹配不在list中任意單一字符 |
| {string1,string2,...} | 匹配string1或者stsring2或者(...)中其一字符串 |
Note: list 中可以指定單個字符,如abcd, 也可以指定ASCII字符的起止范圍,如 a-d。 即[abcd] 與 [a-d] 是等價的,稱為一個自定義的字符類。
例如:
a*b # a 與 b 之間可以有任意個字符(0個或多個),如aabcb, axyzb, a012b,ab等。 a?b # a 與 b 之間只能有一個字符,但該字符可以任意字符,如 aab, abb, acb, azb等。 a[xyz]b # a 與 b 之間只能有一個字符,但這個字符只能是x或者y或者z,如:axb, ayb, azb這三個。 a[!0-9]b# a 與 b 之間只能有一個字符,但這個字符不能是阿拉伯數字,如aab,ayb,a-b等。 a{abc,xyz,123}b # a 與 b之間只能是abc或者xyz或者123這三個字串之一,擴展后是aabcb,axyzb,a123b。[! ] 中的! 只有放在第一位時,才有取反的功效。 eg: [!a]* 表示當前目錄下不以a開頭的路徑名稱; /tmp/[a\!]*表示/tmp目錄下所有以a 或者 ! 開頭的路徑名稱;
思考:為何!前面要加\呢?提示是十三問之4.
[ - ]中-左右兩邊均有字符時,才表示一個范圍,否則,僅作-(減號)字符來處理。 舉例: /tmp/*[-z]/[a-zA-Z]* 表示/tmp 目錄下所有以z或者-結尾的子目錄中, 以英文字母(不分大小寫)開頭的目錄名稱。
以*或?開頭的wildcard不能匹配隱藏文件(即以.開頭的文件名)。 eg: *.txt并不能匹配.txt但能匹配1.txt這樣的路徑名。 但1*txt及1?txt均可匹配1.txt這樣的路徑名。
基本上,要掌握wildcard并不難, 只要多加練習,再勤于思考,就能靈活運用了。
再次提醒:
別忘了wildcard的"擴展" + "重組" 這個重要特性,而且只作用在 argument的path上。比方說, 假如當前目錄下有: a.txt b.txt c.txt 1.txt 2.txt 3.txt 這幾個文件。
當我們在命令行中執行ls -l [0-9].txt的命令行時, 因為wildcard處于argument的位置上,
于是根據匹配的路徑,擴展為: 1.txt 2.txt 3.txt, 在重組出ls -l 1.txt 2.txt 3.txt 這樣的命令行。
因此,你在命令行上敲 ls -l [0-9].txt 與 ls -l 1.txt 2.txt 3.txt 輸出的結果是一樣, 原因就是在于此。
##shell十三問之15: [^ ] 跟[! ]差在哪? (RE: Regular Expression)
Part-II Regular Expression (正則表達式)
接下來的Regular Expression(RE) 可是個大題目,要講的很多。 我這里當然不可能講得很全。 只希望能帶給大家一個基本的入門概念,就很足夠了...
先來考一下英文好了:What is expression? 簡單來說,就是"表達",也就是人們在溝通的時候所要陳述的內容。
然而,生活中,表達方要清楚的將意思描述清楚, 而讓接收方完整無誤地領會,可不是件容易的事情。
因而才會出現那么多的"誤會", 真可嘆句"表達不易"啊......
同樣的情形也發生在計算機的數據處理過程中, 尤其是當我們在描述一段"文字內容"的時候.... 那么,我們不禁要問: 有何方法可以讓大家的誤會降至最低程度, 而讓表達的精確度達到最高程度呢? 答案就是"標準化"了, 也就是我們這里要談的Regular Expression啦...^_^
然而,在進入RE介紹之前,不妨先讓我們溫習一下shell十三問之第4問, 那就是關于quoting的部分。
關鍵是要能夠區分 shell command line上的meta與literal的這兩種不同的字符類型。
然后,我這里也跟你講: RE 表達式里字符也分meta與literal這兩種。
呵,不知親愛的讀者是否被我搞混亂了呢?... ^_^
這也難怪啦,因為這的確是最容易混淆的地方, 剛學RE的朋友很多時候,都死在這里! 因此,請特別小心理解哦...
簡單而言,除非你將RE寫在特定程序使用的腳本里, 否則,我們的RE也是通過 command line輸入的。 然而, 不少RE所使用的meta字符,跟shell 的meta字符是沖突的。
比方說, *這個字符,在RE里是一個modifier(修飾符);而在command line上,確是wildcard(通配符)。
那么,我們該如何解決這樣的沖突呢? 關鍵就是看你對shell十三問的第4問中所提的quoting是否足夠理解了!
若你明白到 shell quoting 就是用來在command line上關閉shell meta這一基本原理, 那你就能很輕松的解決 RE meta與shell meta的沖突問題了: 用shell quoting 關閉掉shell meta就是了。 就這么簡單... ^_^
再以剛提到*字符為例, 若在command line的path中沒有quoting處理的話, 如abc* 就會被作為wildcard expression來擴充及重組了。 若將其置于quoting中,即"abc*",則可以避免wildcard expand的處理。
好了,說了大半天,還沒有進入正式的RE介紹呢.... 大家別急,因為我的教學風格就是要先建立基礎,循序漸進的... ^_^ 因此, 我這里還要再啰嗦一個觀念,才會到RE的說明啦...(哈...別打我...)
當我們在談到RE時,千萬別跟wildcard搞混在一起! 尤其是
在command line的位置里,wildcard只作用于argument的path上; 而RE卻只用于"字符串處理" 的程序中,這與路徑名一點關系也沒有。Tips: RE 所處理的字符串,通常是指純文本或通過stdin讀進的內容。
okay,夠了夠了,我已看到一堆人開始出現不耐煩的樣子了... ^_^ 現在,就讓我們登堂入室,揭開RE的神秘面紗吧, 這樣可以放過我了吧? 哈哈...
在RE的表達式里,主要分為兩種字符:literal與meta。 所謂literal就是在RE里不具有特殊功能的字符,如abc,123等; 而meta,在RE里具有特殊的功能。 要關閉之,需要在meta之前使用escape()轉義字符。
然而,在介紹meta之前,先讓我們來認識一下字符組合(character set)會更好些。
一、所謂的char set就是將多個連續的字符作為一個集合。 例如:
| abc | 表示abc三個連續的字符,但彼此獨立而非集合。(可簡單視為三個char set) |
| (abc) | 表示abc這三個連續字符的集合。(可簡單視為一個char set) |
| abc|xyz | 表示abc或xyz這連個char set之一 |
| [abc] | 表示單一字符,可為a或b或c;與wildcard的[abc]原理相同,稱之為字符類。 |
| [^abc] | 表示單一字符,不為a或b或c即可。(與wildcard [!abc]原理相同) |
| . | 表示任意單個字符,(與wildcard的?原理相同) |
note: abc|xyz 表示abc或xyz這連個char set之一
在認識了RE的char set這個概念之后,然后,在讓我們多認識幾個RE中常見的meta字符:
二、 錨點(anchor): 用以標識RE在句子中的位置所在。 常見的有:
| ^ | 表示句首。如,^abc表示以abc開頭的句子。 |
| $ | 表示句尾。如,abc$表示以abc結尾的句子。 |
| < | 表示詞首。如,<abc表示以abc開頭的詞。 |
| > | 表示詞尾。如,abc>表示以abc結尾的詞。 |
三、 修飾符(modifier):獨立表示時本身不具意義,專門用以修飾前一個char set出現的次數。 常見的有:
| * | 表示前一個char set出現0次或多次,即任意次。如ab*c表示a與c之間可以有0個或多個b。 |
| ? | 表示前一個char set出現0次或1次,即至多出現1次。如ab?c 表示a與c之間可以有0個或1個b。 |
| + | 表示前一個char set出現1次或多次,即至少出現1次。如ab+c 表示a與c之間可以有1個或多個b。 |
| {n} | 表示前一個char set出現n次。如ab{n}c 表示a與c之間可以有n個b。 |
| {n, } | 表示前一個char set至少出現n次。如ab{n}c 表示a與c之間至少有n個b。 |
| {n, m} | 表示前一個char set至少出現n次,至多出現m次。如ab{n,m}c 表示a與c之間至少有n個b,至多有m個b。 |
然而,當我們在識別modifier時,卻很容易忽略"邊界(boundary)字符"的重要性。
以ab{3,5}c為例,這里的a與c就是邊界字符了。 若沒有邊界字符的幫忙,我們很容易做出錯誤的解讀。 比方說: 我們用ab{3,5}這個RE(少了c這個邊界字符) 可以抓到"abbbbbbbbbb"(a后面有10個b)的字符串嗎? 從剛才的modifier的說明,我們一般認為,我們要的b是3到5個, 若超出了此范圍,就不是我們所要表達的。 因此,我們或許會很輕率地認為這個RE抓不到結果(上述"abbbbbbbbbb"字符串)。
然而,答案卻是可以的!為什么呢? 讓我們重新解讀ab{3,5}這個RE看看: 我們要表達的是a后接3到5個b即可,但3到5個b后面,我們卻沒有規定什么, 因此,在RE后面可以是任意的字符串,當然包括b也可以啦!(明白了嗎?)
同樣,我們用b{3,5}c也同樣可以抓到"abbbbbbbbbbc" 這樣的字符串。
但當我們用ab{3,5}c這樣的RE時, 由于同時有a與c這連個邊界字符,就截然不同了!
有空在思考一下,為何我們用下面這些RE都抓到abc這樣的字符串呢?
x* ax*, abx*, ax*b abcx*, abx*c, ax*bc bx*c, bcx*, x*bc但, 若我們在這些RE前后分別加^與$這樣的anchor,那又如何呢?
剛學RE時,只要能掌握上面這些基本的meta的大概就可以入門了。 一如前述,RE是一種規范化的文字表達式, 主要用于某些文字處理工具之間,如: grep, perl, vi,awk,sed,等等, 常用于表示一段連續的字符串,查找和替換。
然而每種工具對RE表達式的具體解讀或有一些細微差別, 不過節本原理還是一致的。 只要掌握RE的基本原理,那就一理通百理了, 只是在實踐時,稍加變通即可。
比方以grep來說, 在Linux上,你可以找到grep,egrep,fgrep這些程序, 其差異大致如下:
grep: 傳統的grep程序,在沒有任何選項(options)的情況下,只輸出符合RE字串的句子, 其常見的選項如下:
| -v | 反模式, 只輸出“不含”RE的字符串的行。 |
| -r | 遞歸模式,可同時處理所有層級的子目錄里的文件 |
| -q | 靜默模式,不輸出任何結果(stderr 除外,常用于獲取return value,符合為true,否則,為false. |
| -i | 忽略大小寫 |
| -w | 整詞匹配,類似 <RE> |
| -n | 同時輸出行號 |
| -l | 輸出匹配RE的文件名 |
| -o | 只輸出匹配RE的字符串。(gna新版獨有,不見得所有版本支持) |
| -E | 切換為egrep |
egrep:為grep的擴充版本,改良了許多傳統grep不能或者不便的操作,
- grep下不支持?與+這兩種meta,但egrep支持;
- grep 不支持a|b或(abc|xyz)這類“或一”的匹配,但egrep支持;
- grep 在處理{n,m}時,需要\{ 與 \}處理,但egrep不需。
等諸如此類的。我個人建議能用egrep就不用grep啦...^_^
fgrep: 不作RE處理,表達式僅作一般的字符串處理,所有的meta均市區功能。
好了,關于RE的入門,我們暫時就介紹到這里。 雖然有點亂,且有些觀念也不恨精確, 不過,姑且算是對大家的一個交差吧...^_^ 若這兩天有時間的話,我在舉些范例來分析一下,以幫助大家更好的理解。 假如更有可能的話,也順道為大家介紹一下sed這個工具。
Part-III eval
講到command line的重組特性, 真的需要我們好好的加以解釋的。
如此便能抽絲剝繭的一層層的將整個command line分析的 一清二楚,而不至于含糊。
假如這個重組的特性理解了,那我們介紹一個好玩的命令:eval.
我們在變量替換的過程中,常會碰到所謂的復式變量的問題: 如:
a=1 A1=abc我們都知道echo $A1就可以得到abc的結果。 然而,我們能否用$A$a來取代$A1,而同一樣替換為abc呢?
這個問題我們可用很輕松的用eval來解決:
eval echo \$A$a說穿了,eval 只不過是在命令行完成替換重組后, 在來一次替換重組罷了... 就是這么簡單啦~~~ ^_^
##shell十三問之16:學習總結與原帖目錄
本人(markdown譯者)是解決工作中shell腳本的一個問題, 偶爾的一次機會遇到了CU論壇中這樣一個神貼:shell十三問.
shell十三問是CU的shell版的臺灣的網中人是2003年用繁體發布的。 第一次讀到shell十三問,由于是繁體,第一感覺有點抵觸, 但是還是耐著性子讀完了一貼,沒想到竟然讀懂了, 而且還被網中人的幽默的寫作風格,獨到的思維方式, 循序漸進的認識事物的過程所折服。
盡管帖子是10多年前寫的,今天看來也幾乎沒有一點過時的感覺。 從這個方面來說,shell十三問應該shell的(思想)精華本質所在, 就像武功的內功心法,可能我說的點過, 但是我曾經看過一本shell腳本學習指南,看完后的感覺,還是有感念很朦朧, 而shell十三問是我最容易理解和接受的,這也是我整理的Markdown版本初衷。 為什么不讓好東西讓更多的人熟知呢,恰好年前項目管理開始遷移到git上, 在git上認識一個好東西Markdown,用它可以很簡單地整理出條例清晰篇章。 在年假的時候,覺得這個假期該做點什么, 畢竟馬總都說了,改變世界,不如改變自己。
本人整理的 [簡體中文Markdown版本的shell十三問][shell-markdown] 的鏈接地址: https://github.com/wzb56/13_questions_of_shell
網中人的CU原帖shell十三問地址:http://bbs.chinaunix.net/thread-218853-1-1.html
我簡單將原文整理如下:
我在CU的日子并不長,有幸在shell版上與大家結緣。 除了跟前輩學習到不少技巧之外,也常看到不少朋友的問題。 然而,在眾多問題中,我發現許多瓶頸都源于shell的基礎而已。 每次要解說,卻總有千言萬語不知從何而起之感......
這次,我不是來回答,而是準備了關于shell基礎的十三個問題要問大家。 希望的shell的學習者們能夠通過尋找答案的過程,好好的將shell基礎打扎實一點。
當然了,這些問題我也會逐一解說一遍。 只是,我不敢保證什么時候能夠完成這趟任務。
除了時間關系外,個人功力實在有限,很怕匆忙間誤導觀眾就糟糕了。 若能拋磚引玉,誘得,其他前輩出馬補充,那才是功德一件。
###shell十三問:
為何叫做 shell?
shell prompt(PS1) 與 Carriage Return(CR) 的關系? (2008-10-30 02:05 最后更新)
別人 echo、你也 echo ,是問 echo 知多少?( 2008-10-30 02:08 最后更新)
" "(雙引號) 與 ' '(單引號)差在哪? (2008-10-30 02:07 最后更新)
var=value 在export前后差在哪? (2008-10-30 02:12 最后更新)
exec 跟 source 差在哪? (2008-10-30 02:17 最后更新)
( ) 與 { } 差在哪?
$(( )) 與 $( ) 還有${ } 差在哪? (2008-10-30 02:20 最后更新)
$@ 與 $* 差在哪?
&& 與 || 差在哪? (2008-10-30 02:21 最后更新)
> 與 < 差在哪? (2008-10-30 02:24 最后更新)
[你要 if 還是 case 呢?] 12 (2008-10-30 02:25最后更新)
for what? while 與 until 差在哪? (2008-10-30 02:26最后更新)
[^ ] 跟 [! ] 差在哪?
Part-I: Wildcard (2008-10-30 02:25 最後更新)
Part-II Regular Expression (2008-10-30 02:26 最后更新)
說明:
歡迎大家補充/擴充問題。
我接觸電腦的中文名稱時是在臺灣,因此一些術語或與大陸不同,請自行轉換。
我會不定時"逐題"說明(以 Linux 上的 bash 為環境) 同時,也會在任何時候進行無預警的修改。請讀者自行留意。
本人于本系列所發表的任文章均可自由以電子格式(非印刷)引用、修改、轉載, 且不必注明出處(若能注明 CU 更佳)。當然,若有錯漏或不當結果,本人也不負任何責任。
若有人愿意整理成冊且付印者,本人僅保留著作權,版權收益之 30% 須捐贈于 CU 論壇管理者,剩余不究。
建議參考談論:
shaoping0330 兄關于變量替換的補充:(鏈接在改版后已經失效)
shaoping0330 兄關于 RE 的說明:
關于 nested subshell 的討論:(鏈接在改版后已經失效)
關于 IFS 的討論:
- 感謝 lkydeer 兄整理 word/pdf 版本方便大家參考:
轉載于:https://my.oschina.net/michao/blog/758884
總結
以上是生活随笔為你收集整理的shell十三问--shell教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SONY Xperia SP M35 解
- 下一篇: 正则表达式总结(待续)