shell編程范例之字符串操作
falcon<zhangjinw@gmail.com>
2007-11-17
忙活了一個禮拜,終于等到周末,可以空下來寫點東西。
這次介紹 _字符串操作_ 了,這里先得明白兩個東西,什么是字符串,對字符串有哪些操作?
下面是"在線新華字典"的解釋:
Quote: |
字符串: 簡稱“串”。有限字符的序列。數據元素為字符的線性表,是一種數據的邏輯結構。在計算機中可有不同的存儲結構。在串上可進行求子串、插入字符、刪除字符、置換字符等運算。
|
而字符呢?
Quote: |
字符: 計算機程序設計及操作時使用的符號。包括字母、數字、空格符、提示符及各種專用字符等。
|
照
這樣說,之前介紹的數值操作中的數字,邏輯運算中的真假值,都是以字符的形式呈現出來的,是一種特別的字符,對它們的運算只不過是字符操作的特例罷了。而
這里將研究一般字符的運算,它具有非常重要的意義,因為對我們來說,一般的工作都是處理字符而已。這些運算實際上將圍繞上述兩個定義來做。
第一、找出字符或者字符串的類型,是數字、字母還是其他特定字符,是可打印字符,還是不可打印字符(一些控制字符)。
第二、找出組成字符串的字符個數和字符串的存儲結構(比如數組)。
第三、對串的常規操作:求子串、插入字符、刪除字符、置換字符、字符串的比較等。
第四、對串的一些比較復雜而有趣的操作,這里將在最后介紹一些有趣的范例。
1. 字符串的屬性
1.1 字符串的類型
字符有可能是數字、字母、空格、其他特殊字符,而字符串有可能是它們任何一種或者多種的組合,在組合之后還可能形成一個具有特定意義的字符串,諸如郵件地址,URL地址等。
概要示例: 下面我們來看看如何判斷字符的類型。
Quote: |
// 數字或者數字組合(能夠返回結果,即程序提出狀態是0,說明屬于這種類型,反之不然) $ i=5;j=9423483247234; $ echo $i | grep [0-9]* 5 $ echo $j | grep [0-9]* 9423483247234 $ echo $j | grep [0-9]* >/dev/null $ echo $? 0 // 字符組合(小寫字母、大寫字母、兩者的組合) $ c="A"; d="fwefewjuew"; e="fewfEFWefwefe" $ echo $c | grep [A-Z] A $ echo $d | grep "[a-z]*" fwefewjuew $ echo $e | grep "[a-zA-Z]*" fewfEFWefwefe // 字母和數字的組合 $ ic="432fwfwefeFWEwefwef" $ echo $ic | grep "[0-9a-zA-Z]*" 432fwfwefeFWEwefwef // 空格或者Tab鍵等 $ echo " " | grep " " $ echo -e "\t" | grep "[[:space:]]" #[[:space:]]會同時匹配空格和TAB鍵
$ echo -e " \t" | grep "[[:space:]]"
$ echo -e "\t" | grep "<tab>" #<tab>為在鍵盤上按下TAB鍵,而不是字符<tab> // 匹配郵件地址 $ echo "test2007@lzu.cn" | grep "[0-9a-zA-Z\.]*@[0-9a-zA-Z\.]" test2007@lzu.cn // 匹配URL地址(以http鏈接為例) $ echo "http://news.lzu.edu.cn/article.jsp?newsid=10135" | grep "http://[0-9a-zA-Z\./=?]*" http://news.lzu.edu.cn/article.jsp?newsid=10135
|
說明:
[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判斷某個字符是否為可打印字符 $ echo "\t\n" | grep "[[:print:]]" \t\n $ echo $? 0 $ echo -e "\t\n" | grep "[[:print:]]" $ echo $? 1 // 用echo的-e選項在屏幕控制字符顯示位置、顏色、背景等 $ echo -e "\33[31;40m" #設置前景色為黑色,背景色為紅色 $ echo -e "\33[11;29H Hello, World\!" #在屏幕的第11行,29列開始打印字符串Hello,World! // 在屏幕的某個位置動態顯示當前系統時間 $ while :; do echo -e "\33[11;29H "$(date "+%Y-%m-%d %H:%M:%S"); done // 用col命令過濾掉某些控制字符,在處理諸如script,screen等截屏命令的輸出結果時,很有用 $ screen -L $ cat /bin/cat $ exit $ cat screenlog.0 | col -b # 把一些控制字符過濾后,就可以保留可讀的操作日志
|
更多關于字符在終端的顯示控制方法,請參考資料[20]和字符顯示實例[21]:用shell實現的一個動態時鐘。
1.2 字符串的長度
概要示例: 除了組成字符串的字符類型外,字符串還有哪些屬性呢?組成字符串的字符個數。下面我們來計算字符串的長度,即所有字符的個數,并簡單介紹幾種求字符串中指定字符個數的方法。
Quote: |
// 計算某個字符串的長度,即所有字符的個數[這計算方法是五花八門,擇其優著而用之] $ var="get the length of me" $ echo ${var} # 這里等同于$var get the length of me $ echo ${#var} 20 $ expr length "$var" 20 $ echo $var | awk '{printf("%d\n", length($0));}' 20 $ echo -n $var | wc -c 20 // 計算某些指定一個字符或者多個字符的個數 $ echo $var | tr -cd g | wc -c 2 $ echo -n $var | sed -e 's/[^g]//g' | wc -c 2 $ echo -n $var | sed -e 's/[^gt]//g' | wc -c 5 // 如果要統計單詞個數,更多相關信息見《shell編程之數值計算》之 _單詞統計_ 實例。 $ echo $var | wc -w 5 $ echo "$var" | tr " " "\n" | grep get | uniq -c 1 $ echo "$var" | tr " " "\n" | grep get | wc -l 1
|
說明:
${}操作符在Bash里頭一個“大牛”,能勝任相當多的工作,具體就看看網中人的《shell十三問》之《Shell十三問》之"$(( )) 與 $( ) 還有${ } 差在哪?" 吧。
1.3 字符串的存儲
在
我們看來,字符串是一連串的字符而已,但是為了操作方便,我們往往可以讓字符串呈現出一定的結構。在這里,我們不關心字符串在內存中的實際存儲結構,僅僅
關系它呈現出來的邏輯結構。比如,這樣一個字符串:"get the length of me",我們可以從不同的方面來呈現它。
1.3.1 通過字符在串中的位置來呈現它
這
樣我們就可以通過指定位置來找到某個子串。這在c語言里頭通常可以利用指針來做。而在shell編程中,有很多可用的工具,諸如expr,awk都提供了
類似的方法來實現子串的查詢動作。兩者都幾乎支持模式匹配(match)和完全匹配(index)。這在后面的字符串操作中將詳細介紹。
1.3.2 根據某個分割符來取得字符串的各個部分
這
里最常見的就是行分割符、空格或者TAB分割符了,前者用來當行號,我們似乎已經司空見慣了,因為我們的編輯器就這樣“莫名”地處理著行分割符(在
unix下為\n,在其他系統下有一些不同,比如windows下為\r\n)。而空格或者TAB鍵經常用來分割數據庫的各個字段,這似乎也是司空見慣的
事情。
正是因為這樣,所以產生了大量優秀的行編輯工具,諸如grep,awk,sed等。在“行內”(姑且這么說吧,就是處理單行,即字符串里頭不再包含行分割符)的字符串分割方面,cut和awk提供了非常優越的“行內”(處理單行)處理能力。
1.3.3 更方便地處理用分割符分割好的各個部分
同樣是用到分割符,但為了更方便的操作分割以后的字符串的各個部分,我們抽象了“數組”這么一個數據結構,從而讓我們更加方便地通過下標來獲取某個指定的部分。bash提供了這么一種數據結構,而優秀的awk也同樣提供了它,我們這里將簡單介紹它們的用法。
概要示例:利用數組存放"get the length of me"的用空格分開的各個部分。
Quote: |
//1. bash提供的數組數據結構,它是以數字為下標的,和C語言從0開始的下標一樣 $ var="get the length of me" $ var_arr=($var) #這里把字符串var存放到字符串數組var_arr中了,默認以空格作為分割符 $ echo ${var_arr[0]} ${var_arr[1]} ${var_arr[2]} ${var_arr[3]} ${var_arr[4]} get the length of me $ echo ${var_arr[@]} #這個就是整個字符串所有部分啦,這里可以用*代替@,下同 get the length of me $ echo ${#var_arr[@]} #記得上面求某個字符串的長度么,#操作符,如果想求某個數組元素的字符串長度,那么就把@換成下標吧 5 // 你也可以直接給某個數組元素賦值 $ var_arr[5]="new_element" $ echo ${var_arr[5]} 6 $ echo ${var_arr[5]} new_element // bash里頭實際上還提供了一種類似于“數組”的功能,即"for i in 用指定分割符分開的字符串" 的用法 // 即,你可以很方便的獲取某個字符串的某個部分 $ for i in $var; do echo -n $i" "; done; get the length of me
//2. awk里頭的數組,注意比較它和bash提供的數組的異同 // split把一行按照空格分割,存放到數組var_arr中,并返回數組的長度。注意:這里的第一個元素下標不是0,而是1 $ echo $var | awk '{printf("%d %s\n", split($0, var_arr, " "), var_arr[1]);}' 5 get // 實際上,上面的操作很類似awk自身的行處理功能:awk默認把一行按照空格分割為多個域,并可以通過$1,$2,$3...來獲取,$0表示整行 // 這里的NF是該行的域的總數,類似于上面數組的長度,它同樣提供了一種通過“下標”訪問某個字符串的功能 $ echo $var | awk '{printf("%d | %s %s %s %s %s | %s\n", NF, $1, $2, $3, $4, $5, $0);}' 5 | get the length of me | get the length of me // awk的“數組”功能何止于此呢,看看它的for引用吧,注意,這個和bash里頭的for不太一樣,i不是元素本身,而是下標 $ echo $var | awk '{split($0, var_arr, " "); for(i in var_arr) printf("%s ",var_arr);}' get the length of me $ echo $var | awk '{split($0, var_arr, " "); for(i in var_arr) printf("%s ",i);}' 1 2 3 4 5 // awk還有更“厲害”的處理能力,它的下標可以不是數字,而可以是字符串,從而變成了“關聯”數組,這種“關聯”的作用在某些方便將讓我們非常方便 // 比如,我們這里就實現一個非凡的應用,把某個文件中的某個系統調用名替換成地址,如果你真正用起它,你會感慨它的“鬼斧神工”的。 // 這就是我在一個場合最好才發現的隨好的實現方案:有興趣看看awk手冊帖子中我在3樓回復的實例吧。 $ cat symbol sys_exit sys_read sys_close $ ls /boot/System.map* $ awk '{if(FILENAME ~ "System.map") map[$3]=$1; else {printf("%s\n", map[$1])}}' /boot/System.map-2.6.20-16-generic symbol c0129a80 c0177310 c0175d80 // 另外,awk還支持刪除某個數組元素,如果你不用了就可以用delete函數給刪除掉。如果某些場合有需要的話,別忘了awk還支持二維數組。
|
okay,就介紹到這里啦。為什么要介紹這些內容?再接著看下面的內容,你就會發現,那些有些的工具是怎么產生和發展起來的了,如果累了,看看最后一篇參考資料吧,它介紹了一些linux命令名字的由來,說不定可以幫助你理解本節下面的部分呢。
2. 字符串常規操作
字符串操作包括取子串、查詢子串、插入子串、刪除子串、子串替換、子串比較、子串排序、子串進制轉換、子串編碼轉換等。
2.1 取子串
概要示例:取子串的方法主要有:直接到指定位置求子串,字符匹配求子串。
Quote: |
// 按照位置取子串,比如從什么位置開始,取多少個字符 $ var="get the length of me" $ echo ${var:0:3} get $ echo ${var:(-2)} # 方向相反呢 me $ echo `expr substr "$var" 5 3` #記得把$var引起來,否則expr會因為空格而解析錯誤 the $ echo $var | awk '{printf("%s\n", substr($0, 9, 6))}' length
// 匹配字符求子串 $ echo ${var%% *} #從右邊開始計算,刪除最左邊的空格右邊的所有字符 get $ echo ${var% *} #從右邊開始計算,刪除第一個空格右邊的所有字符 get the length of $ echo ${var##* } #從左邊開始計算,刪除最右邊的空格左邊的所有字符 me $ echo ${var#* } #從左邊開始計算,刪除第一個空格左邊的所有字符 the length of me
$ echo $var | awk '{printf("%s\n", $1);}' # awk把$var按照空格分開為多個變量,依次為$1,$2,$3,$4,$5 get $ echo $var | awk '{printf("%s\n", $5);}' me
$ echo $var | cut -d" " -f 5 #差點把cut這個小東西忘記啦,用起來和awk類似, -d指定分割符,如同awk用-F指定分割符一樣,-f指定“域”,如同awk的$數字。
$ echo $var | sed 's/ [a-z]*//g' #刪除所有 空格+字母串 的字符串,所以get后面的全部被刪除了 get $ echo $var | sed 's/[a-z]* //g' me
$ echo $var | tr " " "\n" | sed -n 1p #sed有按地址(行)打印(p)的功能,記得先用tr把空格換成行號 get $ echo $var | tr " " "\n" | sed -n 5p me
// tr也可以用來取子串哦,它也可以類似#和%來“拿掉”一些字符串來實現取子串 $ echo $var | tr -d " " getthelengthofme $ echo $var | tr -cd "[a-z]" #把所有的空格都拿掉了,僅僅保留字母字符串,注意-c和-d的用法 getthelengthofme
|
說明:
[1] %和#的區別是,刪除字符的方向不一樣,前者在右,后者在左,%%和%,##和#的方向是前者是最大匹配,后者是最小匹配。(好的記憶方法見網中人的鍵盤記憶法:#$%是鍵盤依次從左到右的三個鍵)
[2] tr的-c選項是complement的縮寫,即invert,而-d選項是刪除的意思,tr -cd "[a-z]"這樣一來就變成保留所有的字母啦。
對于字符串的截取,實際上還有一些命令,如果head,tail等可以實現有意思的功能,可以截取某個字符串的前面、后面指定的行數或者字節數。例如:
Quote: |
$ echo "abcdefghijk" | head -c 4 abcd $ echo -n "abcdefghijk" | tail -c 4 hijk
|
2.2. 查詢子串
概要示例:子串查詢包括:返回符合某個模式的子串本身和返回子串在目標串中的位置。
準備:在進行下面的操作之前,請把http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1385.html鏈接中的內容復制到一個文本text里頭,用于下面的操作。
Quote: |
// 查詢子串在目標串中的位置 $ var="get the length of me" $ expr index "$var" t #貌似僅僅可以返回某個字符或者多個字符中第一個字符出現的位置 3 $ echo $var | awk '{printf("%d\n", match($0,"the"));}' #awk卻能找出字串,match還可以匹配正則表達式 5
// 查詢子串,返回包含子串的行(awk,sed都可以實現這些功能,但是grep最擅長) $ grep "consists of" text # 查詢text文件包含consists of的行,并打印這些行 $ grep "consists[[:space:]]of" -n -H text # 打印文件名,子串所在行的行號和該行的內容 $ grep "consists[[:space:]]of" -n -o text # 僅僅打印行號和匹配到的子串本身的內容 $ awk '/consists of/{ printf("%s:%d:%s\n",FILENAME, FNR, $0)}' text #看到沒?和grep的結果一樣 $ sed -n -e '/consists of/=;/consists of/p' text #同樣可以打印行號
|
說明:
[1] awk,grep,sed都能通過模式匹配查找指定的字符串,但是它們各有擅長的領域,我們將在后續的章節中繼續使用和比較它們,從而發現各自的優點。
[2] 在這里我們姑且把文件內容當成了一個大的字符串,在后面的章節中我們將專門介紹文件的操作,所以對文件內容中存放字符串的操作將會有更深入的分析和介紹。
2.3. 子串替換
子
串替換就是把某個指定的子串替換成其他的字符串,實際上這里就蘊含了“插入子串”和“刪除子串”的操作。例如,你想插入某個字符串到某個子串之前,就可以
把原來的子串替換成”子串+新的字符串“,如果想刪除某個子串,就把子串替換成空串。不過有些工具提供了一些專門的用法來做插入子串和刪除子串的操作,所
以呆伙還是會專門介紹的。另外,要想替換掉某個子串,一般都是先找到子串(查詢子串),然后再把它替換掉的,實質上很多工具在使用和設計上都體現了這么一
點。
概要示例:下面我們把變量var中的空格替換成下劃線看看。
Quote: |
// 用{}運算符,還記得么?網中人的教程。 $ var="get the length of me" $ echo ${var/ /_} #把第一個空格替換成下劃線 get_the length of me $ echo ${var// /_} #把所有空格都替換成了下劃線了 get_the_length_of_me
// 用awk,awk提供了轉換的最小替換函數sub和全局替換函數gsub,類似/和// $ echo $var | awk '{sub(" ", "_", $0); printf("%s\n", $0);}' get_the length of me $ echo $var | awk '{gsub(" ", "_", $0); printf("%s\n", $0);}' get_the_length_of_me
// 用sed了,子串替換可是sed的特長 $ echo $var | sed -e 's/ /_/' #s <= substitude get_the length of me $ echo $var | sed -e 's/ /_/g' #看到沒有,簡短兩個命令就實現了最小匹配和最大匹配g <= global get_the_length_of_me
// 有忘記tr命令么?可以用替換單個字符的 $ echo $var | tr " " "_" get_the_length_of_me $ echo $var | tr '[a-z]' '[A-Z]' #這個可有意思了,把所有小寫字母都替換為大寫字母 GET THE LENGTH OF ME
|
說明:sed還有很有趣的標簽用法呢,下面再介紹吧。
有一種比較有意思的字符串替換是,整個文件行的倒置,這個可以通過tac命令實現,它會把文件中所有的行全部倒轉過來。在一定意義上來說,排序實際上也是一個字符串替換。
2.4. 插入子串
插入子串:就是在指定的位置插入子串,這個位置可能是某個子串的位置,也可能是從某個文件開頭算起的某個長度。通過上面的練習,我們發現這兩者之間實際上是類似的。
公式:插入子串=把"old子串"替換成"old子串+new子串"或者"new子串+old子串"
概要示例::下面在var字符串的空格之前或之后插入一個下劃線
Quote: |
// 用{} $ var="get the length of me" $ echo ${var/ /_ } #在指定字符串之前插入一個字符串 get_ the length of me $ echo ${var// /_ } get_ the_ length_ of_ me $ echo ${var/ / _} #在指定字符串之后插入一個字符串 get _the length of me $ echo ${var// / _} get _the _length _of _me
// 其他的還用演示么?這里主要介紹sed怎么用來插入字符吧,因為它的標簽功能很有趣 $ echo $var | sed -e 's/\( \)/_\1/' #\(和\)將不匹配到的字符串存放為一個標簽,按匹配順序為\1,\2... get_ the length of me $ echo $var | sed -e 's/\( \)/_\1/g' get_ the_ length_ of_ me $ echo $var | sed -e 's/\( \)/\1_/' get _the length of me $ echo $var | sed -e 's/\( \)/\1_/g' get _the _length _of _me
// 看看sed的標簽的順序是不是\1,\2....,看到沒?\2和\1掉換位置后,the和get的位置掉換了 $ echo $var | sed -e 's/\([a-z]*\) \([a-z]*\) /\2 \1 /g' the get of length me // sed還有專門的插入指令,a和i,分別表示在匹配的行后和行前插入指定字符 $ echo $var | sed '/get/a test' get the length of me test $ echo $var | sed '/get/i test' test get the length of me
|
2.5. 刪除子串
刪除子串:應該很簡單了吧,把子串替換成“空”(什么都沒有)不就變成了刪除么。還是來簡單復習一下替換吧。
概要示例::把var字符串中所有的空格給刪除掉。
鼓勵: 這樣一替換不知道變成什么單詞啦,誰認得呢?但是中文卻是連在一起的,所以中文有多難,你想到了么?原來你也是個語言天才,而英語并不可怕,你有學會它的天賦,只要你有這個打算。
Quote: |
// 再用{} $ echo ${var// /} getthelengthofme // 再用awk $ echo $var | awk '{gsub(" ","",$0); printf("%s\n", $0);}' // 再用sed $ echo $var | sed 's/ //g' getthelengthofme // 還有更簡單的tr命令,tr也可以把" "給刪除掉,看 $ echo $var | tr -d " " getthelengthofme
|
如
果要刪除掉第一個空格后面所有的字符串該怎么辦呢?還記得{}的#和%用法么?如果不記得,回到這一節的還頭開始復習吧。(實際上刪除子串和取子串未嘗不
是兩種互補的運算呢,刪除掉某些不想要的子串,也就同時取得另外那些想要的子串——這個世界就是一個“二元”的世界,非常有趣)
2.6. 子串比較
這
個很簡單:還記得test命令的用法么?man
test。它可以用來判斷兩個字符串是否相等的。另外,你發現了“字符串是否相等”和“字符串能否跟另外一個字符串匹配"兩個問題之間的關系嗎?如果兩個
字符串完全匹配,那么這兩個字符串就相等了。所以呢,上面用到的字符串匹配方法,也同樣可以用到這里。
2.7. 子串排序
差點忘記這個重要的內容了,子串排序可是經常用到的,常見的有按字母序、數字序等正序或反序排列。sort命令可以用來做這個工作,它和其他行處理命令一樣,是按行操作的,另外,它類似cut和awk,可以指定分割符,并指定需要排序的列。
Quote: |
$ var="get the length of me" $ echo $var | tr ' ' '\n' | sort #正序排 get length me of the $ echo $var | tr ' ' '\n' | sort -r #反序排 the of me length get $ cat data.txt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 41 45 44 44 26 44 42 20 20 38 37 25 45 45 45 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 44 20 30 39 35 38 38 28 25 30 36 20 24 32 33 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 41 33 51 39 20 20 44 37 38 39 42 40 37 50 50 46 47 48 49 50 51 52 53 54 55 56 42 43 41 42 45 42 19 39 75 17 17 $ cat data.txt | sort -k 2 -n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 44 20 30 39 35 38 38 28 25 30 36 20 24 32 33 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 41 33 51 39 20 20 44 37 38 39 42 40 37 50 50 42 43 41 42 45 42 19 39 75 17 17 41 45 44 44 26 44 42 20 20 38 37 25 45 45 45 46 47 48 49 50 51 52 53 54 55 56
|
2.7. 子串進制轉換
如果字母和數字字符用來計數,那么就存在進制轉換的問題。在數值計算一節的回復資料里,我們已經介紹了bc命令,這里再簡單的復習一下。
Quote: |
$ echo "ibase=10;obase=16;10" | bc A
|
說明:ibase指定輸入進制,obase指出輸出進制,這樣通過調整ibase和obase,你想怎么轉就怎么轉啦!
2.7. 子串編碼轉換
什
么是字符編碼?這個就不用介紹了吧,看過那些亂七八糟顯示的網頁么?大多是因為瀏覽器顯示時的”編碼“和網頁實際采用的”編碼“不一致導致的。字符編碼通
常是指把一序列”可打印“字符轉換成二進制表示,而字符解碼呢則是執行相反的過程,如果這兩個過程不匹配,則出現了所謂的”亂碼“。
為了
解決”亂碼“問題呢?就需要進行編碼轉換。在linux下,我們可以使用iconv這個工具來進行相關操作。這樣的情況經常在不同的操作系統之間移動文
件,不同的編輯器之間交換文件的時候遇到,目前在windows下常用的漢字編碼是gb2312,而在linux下則大多采用utf8。
Quote: |
$ nihao_gb2312=$(echo "你好" | iconv -f utf8 -t gb2312) $ echo $nihao_gb2312 ???? $ nihao_utf8=$(echo $nihao_gb2312 | iconv -f gb2312 -t utf8) $ PS1="$ " $ echo $nihao_utf8 你好
|
說明:我的終端默認編碼是utf8,所以結果如上。
3. 字符串操作范例
實際上,在用Bash編程時,大部分時間都是在處理字符串,因此把這一節熟練掌握非常重要。
3.1 處理一個非常有意義的字符串:URL地址
范例演示:處理URL地址
URL
地址(URL(Uniform Resoure
Locator:統一資源定位器)是WWW頁的地址)幾乎是我們日常生活的玩伴,我們已經到了無法離開它的地步啦,對它的操作很多,包括判斷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" // 匹配URL地址,判斷URL地址的有效性 $ echo $url | grep "ftp://[a-z]*:[a-z]*@[a-z\./-]*" // 截取服務器類型 $ echo ${url%%:*} ftp $ echo $url | cut -d":" -f 1 ftp // 截取域名 $ tmp=${url##*@} ; echo ${tmp%%/*} mirror.lzu.edu.cn // 截取路徑 $ tmp=${url##*@} ; echo ${tmp%/*} mirror.lzu.edu.cn/software // 截取文件名 $ basename $url scim-1.4.7.tar.gz $ echo ${url##*/} scim-1.4.7.tar.gz // 截取文件類型(擴展名) $ echo $url | sed -e 's/.*[0-9].\(.*\)/\1/g' tar.gz
|
有了上面的知識,我們就可以非常容易地進行這些工作啦:修改某個文件的文件名,比如調整它的編碼,下載某個網頁里頭的所有pdf文檔等。這些就作為練習自己做吧,如果遇到問題,可以在回帖交流。相應地可以參考這個例子:
[1] 用腳本下載某個網頁中的英文原著(pdf文檔)
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1228.html
3.2 處理格式化的文本:/etc/passwd
平時做工作,大多數時候處理的都是一些“格式化”的文本,比如類似/etc/passwd這樣的有固定行和列的文本,也有類似tree命令輸出的那種具有樹形結構的文本,當然還有其他具有特定結構的文本。
關于樹狀結構的文本的處理,可以考慮看看這兩個例子:
[1] 用AWK轉換樹形數據成關系表
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1260.html
[2] 用Graphviz進行可視化操作──繪制函數調用關系圖
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1425.html
實際上,只要把握好特性結構的一些特點,并根據具體的應用場合,處理起來就不會困難。
下面我們來介紹具體有固定行和列的文本的操作,以/etc/passwd文件為例。關于這個文件的幫忙和用戶,請通過man 5 passwd查看。下面我們對這個文件以及相關的文件進行一些有意義的操作。
Quote: |
// 選取/etc/passwd文件中的用戶名和組ID兩列 $ cat /etc/passwd | cut -d":" -f1,4 // 選取/etc/group文件中的組名和組ID兩列 $ cat /etc/group | cut -d":" -f1,3 // 如果想找出所有用戶所在的組,怎么辦? $ join -o 1.1,2.1 -t":" -1 4 -2 3 /etc/passwd /etc/group root:root bin:bin daemon:daemon adm:adm lp:lp pop:pop nobody:nogroup falcon:users //
先解釋一下:join命令用來連接兩個文件,有點類似于數據庫的兩個表的連接。-t指定分割符,"-1 4 -2
3"指定按照第一個文件的第4列和第二個文件的第3列,即組ID進行連接,"-o
1.1,2.1"表示僅僅輸出第一個文件的第一列和第二個文件的第一列,這樣就得到了我們要的結果,不過,可惜的是,這個結果并不準確,再進行下面的操
作,你就會發現: $ cat /etc/passwd | sort -t":" -n -k 4 > /tmp/passwd $ cat /etc/group | sort -t":" -n -k 3 > /tmp/group $ join -o 1.1,2.1 -t":" -1 4 -2 3 /tmp/passwd /tmp/group halt:root operator:root root:root shutdown:root sync:root bin:bin daemon:daemon adm:adm lp:lp pop:pop nobody:nogroup falcon:users games:users // 可以看到這個結果才是正確的,所以以后使用join千萬要注意這個問題,否則采取更保守的做法似乎更能保證正確性,更多關于文件連接的討論見參考資料[14]
|
上
面涉及到了處理某格式化行中的指定列,包括截取(如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轉換成按照行對應的,即把類似下面的數據:
Quote: |
$ cat /etc/passwd | cut -d":" -f1,3 --output-delimiter=" " root 0 bin 1 daemon 2
|
轉換成:
Quote: |
$ cat a root bin daemon 0 1 2
|
并轉換回去,有什么辦法呢?記得諸如tr,paste,split等命令都可以使用。
參考方法:
*正轉換:先截取用戶名一列存入文件user,再截取用戶ID存入id,再把兩個文件用paste -s命令連在一起,這樣就完成了正轉換。
*逆轉換:先把正轉換得到的結果用split -1拆分成兩個文件,再把兩個拆分后的文件用tr把分割符"\t"替換成"\n",只有用paste命令把兩個文件連在一起,這樣就完成了逆轉換。
更多有趣的例子,可以參考該序列第一部分的回復,即參考資料[16]的回復,以及蘭大開源社區鏡像站用的鏡像腳本,即參考資料[17],另外,參考資料[18]關于用Shell實現一個五筆反查小工具也值得閱讀和改進。
*更多例子將逐步補充和完善。
參考和推薦資料:
[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] 磨練構建正則表達式模式的技能
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編程范例之數值運算
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 文本工具簡化數據的提取
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] 在終端動態顯示時間
http://oss.lzu.edu.cn/modules/newbb/viewtopic.php?topic_id=964&forum=26
后記:
[1] 這一節本來是上個禮拜該弄好的,但是這些天太忙了,到現在才寫好一個“初稿”,等到有時間再補充具體的范例。這一節的范例應該是最最有趣的,所有得好好研究一下幾個有趣的范例。
[2] 寫完[1]貌似是1點多,剛check了一下錯別字和語法什么的,再添加了一節,即“字符串的存儲結構”,到現在已經快half past 2啦,晚安,朋友們。
[3] 26號,添加“子串進制轉換”和“子串編碼轉換”兩小節以及一個處理URL地址的范例。
剛找資料搜到《Bash 實例》,有時間可以看看:
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/
剛去linuxsir.org,看到有人寫了一篇《Bash字符串處理》,比較簡潔,推薦看看:http://www.linuxsir.org/bbs/showthread.php?threadid=76402
補充例1: 調整文件名的編碼
如果在Linux系統下,從 windows的ftp服務器或者系統上復制文件,經常遇到文件名的編碼問題,即原文件名的編碼是gb2312,而linux下的文件名編碼則為utf8。怎么辦呢?修改文件名?手動修改么?非也,非也,寫個腳本來做。
先來分析一下,假如原來的文件名是 $FROM,要修改為 $TO,則可以:
$ mv $FROM $TO
原來的文件名是gb2312的編碼,而現在的編碼則是utf8,即
TO=$(echo $FROM | iconv -f gb2312 -t utf8)
這樣,如果執行mv操作,就可以把某個文件名的編碼調整啦。
但是,如果要調整某個目錄下的所有文件的編碼呢?
也很簡單,可以先用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 {}
是不是相當簡單,這種解決問題的方式我們在以后會繼續深入討論,這完全體現了Unix的K.I.S.S的哲學,請仔細體會 :-)
當然,在實際解決問題的時候,需要在cn.sh引入一些出錯的處理機制,比如,如果原編碼并非gb2312,在轉換的時候就可能出錯,這個時候,可以考慮采用一些辦法:
1、比如如果出錯,不改變這個文件名,即在TO=...之后加入一句
[ $? -ne 0 ] && exit 1
2、如果出錯,那么繼續找,直到找出正確的原編碼為止
可以把TO=...所在句修改為一個循環,不斷判斷知道轉換正確為止,這樣比地一種辦法更有效。
sed單行腳本快速參考:
http://sed.sourceforge.net/sed1line_zh-CN.html
需要提到的是這些工具可以用來進行比較復雜有意思的文本處理,包括格式化,過濾字符等:fold, fmt, column, col,
colrm, nl, pr, gettext, msgfmt, tex, groff,
lex等,更多資料請查看相關手冊或者是《高級Bash腳本編程指南》
關于范圍匹配的實例:通過sed或者awk等命令匹配某個文件中的特定范圍的行。(它有著非常重要的意義,比如,你要用腳本把某個C語言程序中的某個符合要求的函數以及定義找出來,這種用法就起作用啦。)
這里是一個測試文件:一本書第7章的課后答案。文件名叫README吧。
Code:[Ctrl+A Select All]實例演示:
Quote: |
// 打印出答案前指定行范圍:第7行到第9行,剛好找出了第2題的答案 $ sed -n 7,9p README 7.2 it will depend on the exection mode, interactive or redirection to a file, if interactive, the "output" action will accur after the \n char with the line buffer mode, else, it will be really "printed" after all of the strings have // 其實,因為這個文件內容格式很有特色,有更簡單的辦法 $ awk '/7.2/,/^$/ {printf("%s\n", $0);}' README 7.2 it will depend on the exection mode, interactive or redirection to a file, if interactive, the "output" action will accur after the \n char with the line buffer mode, else, it will be really "printed" after all of the strings have been stayed in the buffer.
|