shell編程范例之字符串操作
falcon<zhangjinw@gmail.com>
2007-11-17
忙活了一個禮拜,終于等到周末,可以空下來寫點東西。
這次介紹 _字符串操作_ 了,這里先得明白兩個東西,什么是字符串,對字符串有哪些操作?
下面是"在線新華字典"的解釋:
| Quote: |
|
字符串: |
而字符呢?
| Quote: |
|
字符: |
照 這樣說,之前介紹的數(shù)值操作中的數(shù)字,邏輯運算中的真假值,都是以字符的形式呈現(xiàn)出來的,是一種特別的字符,對它們的運算只不過是字符操作的特例罷了。而 這里將研究一般字符的運算,它具有非常重要的意義,因為對我們來說,一般的工作都是處理字符而已。這些運算實際上將圍繞上述兩個定義來做。
第一、找出字符或者字符串的類型,是數(shù)字、字母還是其他特定字符,是可打印字符,還是不可打印字符(一些控制字符)。
第二、找出組成字符串的字符個數(shù)和字符串的存儲結(jié)構(gòu)(比如數(shù)組)。
第三、對串的常規(guī)操作:求子串、插入字符、刪除字符、置換字符、字符串的比較等。
第四、對串的一些比較復雜而有趣的操作,這里將在最后介紹一些有趣的范例。
1. 字符串的屬性
1.1 字符串的類型
字符有可能是數(shù)字、字母、空格、其他特殊字符,而字符串有可能是它們?nèi)魏我环N或者多種的組合,在組合之后還可能形成一個具有特定意義的字符串,諸如郵件地址,URL地址等。
概要示例: 下面我們來看看如何判斷字符的類型。
| Quote: |
|
// 數(shù)字或者數(shù)字組合(能夠返回結(jié)果,即程序提出狀態(tài)是0,說明屬于這種類型,反之不然) |
說明:
[1] /dev/null和/dev/zero是非常有趣的兩個設備,它們都猶如一個黑洞,什么東西掉進去都會消失殆盡;后者則是一個能源箱,你總能從那里取到0,直到你退出。兩者的部分用法見:關于zero及NULL設備的一些問題
[2] [[:space:]]是grep用于匹配空格或者TAB鍵類型字符串的一種標記,其他類似的標記請查看grep的幫助,man grep。
[3] 上面都是用grep來進行模式匹配,實際上sed, awk都可以用來做模式匹配,關于匹配中用到的正則匹配模式知識,大家可以參考正則匹配模式,更多相關資料請看參考資料。
[4] 如果僅僅想判斷字符串是否為空,即判斷字符串的長度是否為零,那么可以簡單的通過test命令的-z選項來判斷,具體用法見test命令,man test.
概要示例: 判斷字符是否可打印?如何控制字符在終端的顯示。
| Quote: |
|
// 用grep判斷某個字符是否為可打印字符 |
更多關于字符在終端的顯示控制方法,請參考資料[20]和字符顯示實例[21]:用shell實現(xiàn)的一個動態(tài)時鐘。
1.2 字符串的長度
概要示例: 除了組成字符串的字符類型外,字符串還有哪些屬性呢?組成字符串的字符個數(shù)。下面我們來計算字符串的長度,即所有字符的個數(shù),并簡單介紹幾種求字符串中指定字符個數(shù)的方法。
| Quote: |
|
// 計算某個字符串的長度,即所有字符的個數(shù)[這計算方法是五花八門,擇其優(yōu)著而用之] |
說明:
${}操作符在Bash里頭一個“大牛”,能勝任相當多的工作,具體就看看網(wǎng)中人的《shell十三問》之《Shell十三問》之"$(( )) 與 $( ) 還有${ } 差在哪?" 吧。
1.3 字符串的存儲
在 我們看來,字符串是一連串的字符而已,但是為了操作方便,我們往往可以讓字符串呈現(xiàn)出一定的結(jié)構(gòu)。在這里,我們不關心字符串在內(nèi)存中的實際存儲結(jié)構(gòu),僅僅 關系它呈現(xiàn)出來的邏輯結(jié)構(gòu)。比如,這樣一個字符串:"get the length of me",我們可以從不同的方面來呈現(xiàn)它。
1.3.1 通過字符在串中的位置來呈現(xiàn)它
這 樣我們就可以通過指定位置來找到某個子串。這在c語言里頭通常可以利用指針來做。而在shell編程中,有很多可用的工具,諸如expr,awk都提供了 類似的方法來實現(xiàn)子串的查詢動作。兩者都幾乎支持模式匹配(match)和完全匹配(index)。這在后面的字符串操作中將詳細介紹。
1.3.2 根據(jù)某個分割符來取得字符串的各個部分
這 里最常見的就是行分割符、空格或者TAB分割符了,前者用來當行號,我們似乎已經(jīng)司空見慣了,因為我們的編輯器就這樣“莫名”地處理著行分割符(在 unix下為\n,在其他系統(tǒng)下有一些不同,比如windows下為\r\n)。而空格或者TAB鍵經(jīng)常用來分割數(shù)據(jù)庫的各個字段,這似乎也是司空見慣的 事情。
正是因為這樣,所以產(chǎn)生了大量優(yōu)秀的行編輯工具,諸如grep,awk,sed等。在“行內(nèi)”(姑且這么說吧,就是處理單行,即字符串里頭不再包含行分割符)的字符串分割方面,cut和awk提供了非常優(yōu)越的“行內(nèi)”(處理單行)處理能力。
1.3.3 更方便地處理用分割符分割好的各個部分
同樣是用到分割符,但為了更方便的操作分割以后的字符串的各個部分,我們抽象了“數(shù)組”這么一個數(shù)據(jù)結(jié)構(gòu),從而讓我們更加方便地通過下標來獲取某個指定的部分。bash提供了這么一種數(shù)據(jù)結(jié)構(gòu),而優(yōu)秀的awk也同樣提供了它,我們這里將簡單介紹它們的用法。
概要示例:利用數(shù)組存放"get the length of me"的用空格分開的各個部分。
| Quote: |
|
//1. bash提供的數(shù)組數(shù)據(jù)結(jié)構(gòu),它是以數(shù)字為下標的,和C語言從0開始的下標一樣 |
okay,就介紹到這里啦。為什么要介紹這些內(nèi)容?再接著看下面的內(nèi)容,你就會發(fā)現(xiàn),那些有些的工具是怎么產(chǎn)生和發(fā)展起來的了,如果累了,看看最后一篇參考資料吧,它介紹了一些linux命令名字的由來,說不定可以幫助你理解本節(jié)下面的部分呢。
2. 字符串常規(guī)操作
字符串操作包括取子串、查詢子串、插入子串、刪除子串、子串替換、子串比較、子串排序、子串進制轉(zhuǎn)換、子串編碼轉(zhuǎn)換等。
2.1 取子串
概要示例:取子串的方法主要有:直接到指定位置求子串,字符匹配求子串。
| Quote: |
|
// 按照位置取子串,比如從什么位置開始,取多少個字符 |
說明:
[1] %和#的區(qū)別是,刪除字符的方向不一樣,前者在右,后者在左,%%和%,##和#的方向是前者是最大匹配,后者是最小匹配。(好的記憶方法見網(wǎng)中人的鍵盤記憶法:#$%是鍵盤依次從左到右的三個鍵)
[2] tr的-c選項是complement的縮寫,即invert,而-d選項是刪除的意思,tr -cd "[a-z]"這樣一來就變成保留所有的字母啦。
對于字符串的截取,實際上還有一些命令,如果head,tail等可以實現(xiàn)有意思的功能,可以截取某個字符串的前面、后面指定的行數(shù)或者字節(jié)數(shù)。例如:
| Quote: |
|
$ echo "abcdefghijk" | head -c 4 |
2.2. 查詢子串
概要示例:子串查詢包括:返回符合某個模式的子串本身和返回子串在目標串中的位置。
準備:在進行下面的操作之前,請把http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1385.html鏈接中的內(nèi)容復制到一個文本text里頭,用于下面的操作。
| Quote: |
|
// 查詢子串在目標串中的位置 |
說明:
[1] awk,grep,sed都能通過模式匹配查找指定的字符串,但是它們各有擅長的領域,我們將在后續(xù)的章節(jié)中繼續(xù)使用和比較它們,從而發(fā)現(xiàn)各自的優(yōu)點。
[2] 在這里我們姑且把文件內(nèi)容當成了一個大的字符串,在后面的章節(jié)中我們將專門介紹文件的操作,所以對文件內(nèi)容中存放字符串的操作將會有更深入的分析和介紹。
2.3. 子串替換
子 串替換就是把某個指定的子串替換成其他的字符串,實際上這里就蘊含了“插入子串”和“刪除子串”的操作。例如,你想插入某個字符串到某個子串之前,就可以 把原來的子串替換成”子串+新的字符串“,如果想刪除某個子串,就把子串替換成空串。不過有些工具提供了一些專門的用法來做插入子串和刪除子串的操作,所 以呆伙還是會專門介紹的。另外,要想替換掉某個子串,一般都是先找到子串(查詢子串),然后再把它替換掉的,實質(zhì)上很多工具在使用和設計上都體現(xiàn)了這么一 點。
概要示例:下面我們把變量var中的空格替換成下劃線看看。
| Quote: |
|
// 用{}運算符,還記得么?網(wǎng)中人的教程。 |
說明:sed還有很有趣的標簽用法呢,下面再介紹吧。
有一種比較有意思的字符串替換是,整個文件行的倒置,這個可以通過tac命令實現(xiàn),它會把文件中所有的行全部倒轉(zhuǎn)過來。在一定意義上來說,排序?qū)嶋H上也是一個字符串替換。
2.4. 插入子串
插入子串:就是在指定的位置插入子串,這個位置可能是某個子串的位置,也可能是從某個文件開頭算起的某個長度。通過上面的練習,我們發(fā)現(xiàn)這兩者之間實際上是類似的。
公式:插入子串=把"old子串"替換成"old子串+new子串"或者"new子串+old子串"
概要示例::下面在var字符串的空格之前或之后插入一個下劃線
| Quote: |
|
// 用{} |
2.5. 刪除子串
刪除子串:應該很簡單了吧,把子串替換成“空”(什么都沒有)不就變成了刪除么。還是來簡單復習一下替換吧。
概要示例::把var字符串中所有的空格給刪除掉。
鼓勵: 這樣一替換不知道變成什么單詞啦,誰認得呢?但是中文卻是連在一起的,所以中文有多難,你想到了么?原來你也是個語言天才,而英語并不可怕,你有學會它的天賦,只要你有這個打算。
| Quote: |
|
// 再用{} |
如 果要刪除掉第一個空格后面所有的字符串該怎么辦呢?還記得{}的#和%用法么?如果不記得,回到這一節(jié)的還頭開始復習吧。(實際上刪除子串和取子串未嘗不 是兩種互補的運算呢,刪除掉某些不想要的子串,也就同時取得另外那些想要的子串——這個世界就是一個“二元”的世界,非常有趣)
2.6. 子串比較
這 個很簡單:還記得test命令的用法么?man test。它可以用來判斷兩個字符串是否相等的。另外,你發(fā)現(xiàn)了“字符串是否相等”和“字符串能否跟另外一個字符串匹配"兩個問題之間的關系嗎?如果兩個 字符串完全匹配,那么這兩個字符串就相等了。所以呢,上面用到的字符串匹配方法,也同樣可以用到這里。
2.7. 子串排序
差點忘記這個重要的內(nèi)容了,子串排序可是經(jīng)常用到的,常見的有按字母序、數(shù)字序等正序或反序排列。sort命令可以用來做這個工作,它和其他行處理命令一樣,是按行操作的,另外,它類似cut和awk,可以指定分割符,并指定需要排序的列。
| Quote: |
|
$ var="get the length of me" |
2.7. 子串進制轉(zhuǎn)換
如果字母和數(shù)字字符用來計數(shù),那么就存在進制轉(zhuǎn)換的問題。在數(shù)值計算一節(jié)的回復資料里,我們已經(jīng)介紹了bc命令,這里再簡單的復習一下。
| Quote: |
|
$ echo "ibase=10;obase=16;10" | bc |
說明:ibase指定輸入進制,obase指出輸出進制,這樣通過調(diào)整ibase和obase,你想怎么轉(zhuǎn)就怎么轉(zhuǎn)啦!
2.7. 子串編碼轉(zhuǎn)換
什 么是字符編碼?這個就不用介紹了吧,看過那些亂七八糟顯示的網(wǎng)頁么?大多是因為瀏覽器顯示時的”編碼“和網(wǎng)頁實際采用的”編碼“不一致導致的。字符編碼通 常是指把一序列”可打印“字符轉(zhuǎn)換成二進制表示,而字符解碼呢則是執(zhí)行相反的過程,如果這兩個過程不匹配,則出現(xiàn)了所謂的”亂碼“。
為了 解決”亂碼“問題呢?就需要進行編碼轉(zhuǎn)換。在linux下,我們可以使用iconv這個工具來進行相關操作。這樣的情況經(jīng)常在不同的操作系統(tǒng)之間移動文 件,不同的編輯器之間交換文件的時候遇到,目前在windows下常用的漢字編碼是gb2312,而在linux下則大多采用utf8。
| Quote: |
|
$ nihao_gb2312=$(echo "你好" | iconv -f utf8 -t gb2312) |
說明:我的終端默認編碼是utf8,所以結(jié)果如上。
3. 字符串操作范例
實際上,在用Bash編程時,大部分時間都是在處理字符串,因此把這一節(jié)熟練掌握非常重要。
3.1 處理一個非常有意義的字符串:URL地址
范例演示:處理URL地址
URL 地址(URL(Uniform Resoure Locator:統(tǒng)一資源定位器)是WWW頁的地址)幾乎是我們?nèi)粘I畹耐姘椋覀円呀?jīng)到了無法離開它的地步啦,對它的操作很多,包括判斷URL地址的 有效性,截取地址的各個部分(服務器類型、服務器地址、端口、路徑等)并對各個部分進行進一步的操作。
下面我們來具體處理這個URL地址:
ftp://anonymous:ftp@mirror.lzu.edu.cn/software/scim-1.4.7.tar.gz
| Quote: |
|
$ url="ftp://anonymous:ftp@mirror.lzu.edu.cn/software/scim-1.4.7.tar.gz" |
有了上面的知識,我們就可以非常容易地進行這些工作啦:修改某個文件的文件名,比如調(diào)整它的編碼,下載某個網(wǎng)頁里頭的所有pdf文檔等。這些就作為練習自己做吧,如果遇到問題,可以在回帖交流。相應地可以參考這個例子:
[1] 用腳本下載某個網(wǎng)頁中的英文原著(pdf文檔)
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1228.html
3.2 處理格式化的文本:/etc/passwd
平時做工作,大多數(shù)時候處理的都是一些“格式化”的文本,比如類似/etc/passwd這樣的有固定行和列的文本,也有類似tree命令輸出的那種具有樹形結(jié)構(gòu)的文本,當然還有其他具有特定結(jié)構(gòu)的文本。
關于樹狀結(jié)構(gòu)的文本的處理,可以考慮看看這兩個例子:
[1] 用AWK轉(zhuǎn)換樹形數(shù)據(jù)成關系表
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1260.html
[2] 用Graphviz進行可視化操作──繪制函數(shù)調(diào)用關系圖
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1425.html
實際上,只要把握好特性結(jié)構(gòu)的一些特點,并根據(jù)具體的應用場合,處理起來就不會困難。
下面我們來介紹具體有固定行和列的文本的操作,以/etc/passwd文件為例。關于這個文件的幫忙和用戶,請通過man 5 passwd查看。下面我們對這個文件以及相關的文件進行一些有意義的操作。
| Quote: |
|
// 選取/etc/passwd文件中的用戶名和組ID兩列 |
上 面涉及到了處理某格式化行中的指定列,包括截取(如SQL的select用法),連接(如SQL的join用法),排序(如SQL的order by用法),都可以通過指定分割符來拆分某個格式化的行,另外,“截取”的做法還有很多,不光是cut,awk,甚至通過IFS指定分割符的read命令 也可以做到,例如:
| Quote: |
|
$ IFS=":"; cat /etc/group | while read C1 C2 C3 C4; do echo $C1 $C3; done |
因此,熟悉這些用法,我們的工作將變得非常靈活有趣。
到這里,需要做一個簡單的練習,如何把按照列對應的用戶名和用戶ID轉(zhuǎn)換成按照行對應的,即把類似下面的數(shù)據(jù):
| Quote: |
|
$ cat /etc/passwd | cut -d":" -f1,3 --output-delimiter=" " |
轉(zhuǎn)換成:
| Quote: |
|
$ cat a |
并轉(zhuǎn)換回去,有什么辦法呢?記得諸如tr,paste,split等命令都可以使用。
參考方法:
*正轉(zhuǎn)換:先截取用戶名一列存入文件user,再截取用戶ID存入id,再把兩個文件用paste -s命令連在一起,這樣就完成了正轉(zhuǎn)換。
*逆轉(zhuǎn)換:先把正轉(zhuǎn)換得到的結(jié)果用split -1拆分成兩個文件,再把兩個拆分后的文件用tr把分割符"\t"替換成"\n",只有用paste命令把兩個文件連在一起,這樣就完成了逆轉(zhuǎn)換。
更多有趣的例子,可以參考該序列第一部分的回復,即參考資料[16]的回復,以及蘭大開源社區(qū)鏡像站用的鏡像腳本,即參考資料[17],另外,參考資料[18]關于用Shell實現(xiàn)一個五筆反查小工具也值得閱讀和改進。
*更多例子將逐步補充和完善。
參考和推薦資料:
[1] 《高級Bash腳本編程指南》之操作字符串
http://www.linuxpk.com/doc/abs/string-manipulation.html
[2] 《高級Bash腳本編程指南》之指定變量的類型
http://www.linuxpk.com/doc/abs/declareref.html
[3] 《Shell十三問》之$(( )) 與 $( ) 還有${ } 差在哪?
http://bbs.chinaunix.net/viewthread.php?tid=218853&extra=&page=7#pid1617953
[4] Regular Expressions - User guide
http://www.zytrax.com/tech/web/regex.htm
[5] Regular Expression Tutorial
http://analyser.oli.tudelft.nl/regex/index.html.en
[6] Grep Tutorial
http://www.panix.com/~elflord/unix/grep.html
[7] Sed Tutorial
http://www.panix.com/~elflord/unix/sed.html
[8] awk Tutorial
http://www.gnulamp.com/awk.html
[9] sed Tutorial
http://www.gnulamp.com/sed.html
[10] An awk Primer
http://www.vectorsite.net/tsawk.html
[11] 一些奇怪的 unix 指令名字的由來
http://www.linuxsir.org/bbs/showthread.php?t=24264
[12] 磨練構(gòu)建正則表達式模式的技能
http://www.ibm.com/developerworks/cn/aix/library/au-expressions.html
[13] 實用正則表達式
http://www.linuxlong.com/forum/bbs-27-1.html
[14] AWK使用手冊 3 樓的回復帖
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=1006&forum=26
[15] 基礎11:文件分類、合并和分割(sort,uniq,join,cut,paste,split)
http://blog.chinaunix.net/u/9465/showart_144700.html
[16] Shell編程范例之數(shù)值運算
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1391.html
[17] 蘭大Mirror鏡像站的鏡像腳本
http://oss.lzu.edu.cn/blog/article.php?tid_1236.html
[18] 一個用Shell寫的五筆反查小工具
http://oss.lzu.edu.cn/blog/blog.php?/do_showone/tid_1017.html
[19] 使用Linux 文本工具簡化數(shù)據(jù)的提取
http://linux.chinaunix.net/docs/2006-09-22/2803.shtml
[20] 如何控制終端:光標位置,字符顏色,背景,清屏...
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=962&forum=13
[21] 在終端動態(tài)顯示時間
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=964&forum=26
后記:
[1] 這一節(jié)本來是上個禮拜該弄好的,但是這些天太忙了,到現(xiàn)在才寫好一個“初稿”,等到有時間再補充具體的范例。這一節(jié)的范例應該是最最有趣的,所有得好好研究一下幾個有趣的范例。
[2] 寫完[1]貌似是1點多,剛check了一下錯別字和語法什么的,再添加了一節(jié),即“字符串的存儲結(jié)構(gòu)”,到現(xiàn)在已經(jīng)快half past 2啦,晚安,朋友們。
[3] 26號,添加“子串進制轉(zhuǎn)換”和“子串編碼轉(zhuǎn)換”兩小節(jié)以及一個處理URL地址的范例。
http://www.ibm.com/developerworks/cn/linux/shell/bash/bash-1/
http://www.ibm.com/developerworks/cn/linux/shell/bash/bash-2/
http://www.ibm.com/developerworks/cn/linux/shell/bash/bash-3/
如果在Linux系統(tǒng)下,從 windows的ftp服務器或者系統(tǒng)上復制文件,經(jīng)常遇到文件名的編碼問題,即原文件名的編碼是gb2312,而linux下的文件名編碼則為utf8。怎么辦呢?修改文件名?手動修改么?非也,非也,寫個腳本來做。
先來分析一下,假如原來的文件名是 $FROM,要修改為 $TO,則可以:
$ mv $FROM $TO
原來的文件名是gb2312的編碼,而現(xiàn)在的編碼則是utf8,即
TO=$(echo $FROM | iconv -f gb2312 -t utf8)
這樣,如果執(zhí)行mv操作,就可以把某個文件名的編碼調(diào)整啦。
但是,如果要調(diào)整某個目錄下的所有文件的編碼呢?
也很簡單,可以先用ls命令把某個目錄下的文件列出來,然后傳遞給xargs命令來擴展命令,比如這樣:
我們先把上面的修改單個文件名的過程放入一個腳本:
cn.sh
[code]
#!/bin/sh
FROM=$1
TO=$(echo $FROM | iconv -f gb2312 -t utf8)
mv $FROM $TO
[/code]
然后呢,在需要修改文件名的目錄下,運行:
$ chmod +x cn.sh
$ ls | xargs -i ./cn.sh {}
而如果要修改某個目錄下包括子目錄中的所有文件名呢?也很簡單,還記得find命令么?
$ find | xargs -i ./cn.sh {}
是不是相當簡單,這種解決問題的方式我們在以后會繼續(xù)深入討論,這完全體現(xiàn)了Unix的K.I.S.S的哲學,請仔細體會 :-)
1、比如如果出錯,不改變這個文件名,即在TO=...之后加入一句
[ $? -ne 0 ] && exit 1
2、如果出錯,那么繼續(xù)找,直到找出正確的原編碼為止
可以把TO=...所在句修改為一個循環(huán),不斷判斷知道轉(zhuǎn)換正確為止,這樣比地一種辦法更有效。
http://sed.sourceforge.net/sed1line_zh-CN.html
這里是一個測試文件:一本書第7章的課后答案。文件名叫README吧。
Code:
[Ctrl+A Select All]
實例演示:
| Quote: |
|
// 打印出答案前指定行范圍:第7行到第9行,剛好找出了第2題的答案 |
