shell 字符串處理匯總(查找,替換等等)


字符串:

  • 簡稱“串”。有限字符的序列。數據元素為字符的線性表,是一種數據的邏輯結構。在計算機中可有不同的存儲結構。在串上可進行求子串、插入字符、刪除字符、置換字符等運算。

字符:

  • 計算機程序設計及操作時使用的符號。包括字母、數字、空格符、提示符及各種專用字符等。

一般字符的運算包括:

 

  • 第一、找出字符或者字符串的類型,是數字、字母還是其他特定字符,是可打印字符,還是不可打印字符(一些控制字符)。
  • 第二、找出組成字符串的字符個數和字符串的存儲結構(比如數組)。
  • 第三、對串的常規操作:求子串、插入字符、刪除字符、置換字符、字符串的比較等。
  • 第四、對串的一些比較復雜而有趣的操作,這里將在最后介紹一些有趣的范例。

1. 字符串的屬性

字符有可能是數字、字母、空格、其他特殊字符,而字符串有可能是它們任何一種或者多種的組合,在組合之后還可能形成一個具有特定意義的字符串,諸如郵件地址,URL地址等。
概要示例: 下面我們來看看如何判斷字符的類型。

 

 
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
31
32
33
34
35
// 數字或者數字組合(能夠返回結果,即程序退出狀態是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,直到你退出。
[2] [[:space:]]是grep用於匹配空格或者TAB鍵類型字符串的一種標記,其他類似的標記請查看grep的幫助,man grep。
[3] 上面都是用grep來進行模式匹配,實際上sed, awk都可以用來做模式匹配,關於匹配中用到的正則匹配模式知識,大家可以參考 正則匹配模式,更多相關資料請看參考資料。
[4] 如果僅僅想判斷字符串是否為空,即判斷字符串的長度是否為零,那么可以簡單的通過test命令的-z選項來判斷,具體用法見test命令,man test.

概要示例: 判斷字符是否可打印?如何控制字符在終端的顯示。

 

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用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 字符串的長度

概要示例: 除了組成字符串的字符類型外,字符串還有哪些屬性呢?組成字符串的字符個數。下面我們來計算字符串的長度,即所有字符的個數,並簡單介紹幾種求字符串中指定字符個數的方法。

 

 
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
// 計算某個字符串的長度,即所有字符的個數[這計算方法是五花八門,擇其優着而用之]
$ 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
 
1
2
stringZ=abcABC123ABCabc
echo ` expr "$stringZ" : '.*' `    # 15
說明:

${}操作符在Bash里頭一個“大牛”,能勝任相當多的工作,具體就看看網中人的《shell十三問》之《Shell十三問》之"$(( )) 與 $( ) 還有${ } 差在哪?" 吧。

在一個文本文件的段落之間插入空行

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# paragraph-space.sh
 
# 在一個單倍行距的文本文件中插入空行.
# Usage: $0 <FILENAME
  
MINLEN=45        # 可能需要修改這個值.
#  假定行的長度小於$MINLEN所指定的長度的時候
#+ 才認為此段結束.
 
while read line  # 提供和輸入文件一樣多的行...
do
    echo "$line"   # 輸入所讀入的行本身.
  
    len=${ #line}
    if [ "$len" -lt "$MINLEN" ]
      then echo    # 在短行(譯者注: 也就是小於$MINLEN個字符的行)后面添加一個空行.
    fi 
done
  
exit 0

索引

expr index $string $substring

在字符串$string中所匹配到的$substring第一次所出現的位置.

 
1
2
3
4
5
6
stringZ=abcABC123ABCabc
echo ` expr index "$stringZ" C12`             # 6
                                             # C 字符的位置.
 
echo ` expr index "$stringZ" 1c`              # 3
  # 'c' (in #3 position) matches before '1'.

這與C語言中的strchr()函數非常相似.

 

1.3 字符串的存儲

在我們看來,字符串是一連串的字符而已,但是為了操作方便,我們往往可以讓字符串呈現出一定的結構。在這里,我們不關心字符串在內存中的實際存儲結構,僅僅關系它呈現出來的邏輯結構。比如,這樣一個字符串:"get the length of me",我們可以從不同的方面來呈現它。

1.3.1 通過字符在串中的位置來呈現它

這樣我們就可以通過指定位置來找到某個子串。這在c語言里頭通常可以利用指針來做。而在shell編程中,有很多可用的工具,諸如expr,awk都提供了類似的方法來實現子串的查詢動作。兩者都幾乎支持模式匹配(match)和完全匹配(index)。這在后面的字符串操作中將詳細介紹。

 

匹配字符串開頭的子串長度

 

expr match "$string" '$substring'

        $substring是一個正則表達式.

 

expr "$string" : '$substring'

        $substring是一個正則表達式.

 
1
2
3
4
5
stringZ=abcABC123ABCabc
#       |------|
 
echo ` expr match "$stringZ" 'abc[A-Z]*.2' `   # 8
echo ` expr "$stringZ" : 'abc[A-Z]*.2' `       # 8


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"的用空格分開的各個部分。

 

 

 
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//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 還支持二維數組。
2. 字符串常規操作

字符串操作包括取子串、查詢子串、插入子串、刪除子串、子串替換、子串比較、子串排序、子串進制轉換、子串編碼轉換等。

2.1 取子串

概要示例:取子串的方法主要有:直接到指定位置求子串,字符匹配求子串。

${string:position}

 

在$string中從位置$position開始提取子串.

 

如果$string是"*"或者"@", 那么將會提取從位置$position開始的位置參數.[1]

 

${string:position:length}

在$string中從位置$position開始提取$length長度的子串.


 

 

如果$string參數是"*"或"@", 那么將會從$position位置開始提取$length個位置參數, 但是由於可能沒有$length個位置參數了, 那么就有幾個位置參數就提取幾個位置參數.

expr substr $string $position $length

在$string中從$position開始提取$length長度的子串.

 

expr match "$string" '\($substring\)'

從$string的開始位置提取$substring,$substring是正則表達式.

 

expr "$string" : '\($substring\)'

從$string的開始位置提取$substring,$substring是正則表達式.

 

expr match "$string" '.*\($substring\)'

從$string的結尾提取$substring,$substring是正則表達式.

 

expr "$string" : '.*\($substring\)'

從$string的結尾提取$substring,$substring是正則表達式.


 

 

 
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
31
32
33
34
35
36
37
38
39
40
41
42
43
// 按照位置取子串,比如從什么位置開始,取多少個字符
$ 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等可以實現有意思的功能,可以截取某個字符串的前面、后面指定的行數或者字節數。例如:

 

 

 
1
2
3
4
$ 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里頭,用於下面的操作。

// 查詢子串在目標串中的位置
$ var="get the length of me"
$ expr index "$var" t        #貌似僅僅可以返回某個字符或者多個字符中第一個字符出現的位置

$ 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 #同樣可以打印行號

 

2.3. 子串替換

子串替換就是把某個指定的子串替換成其他的字符串,實際上這里就蘊含了“插入子串”和“刪除子串”的操作。例如,你想插入某個字符串到某個子串之 前,就可以把原來的子串替換成”子串+新的字符串“,如果想刪除某個子串,就把子串替換成空串。不過有些工具提供了一些專門的用法來做插入子串和刪除子串 的操作,所以呆伙還是會專門介紹的。另外,要想替換掉某個子串,一般都是先找到子串(查詢子串),然后再把它替換掉的,實質上很多工具在使用和設計上都體 現了這么一點。

 

${string/substring/replacement}

使用$replacement來替換第一個匹配的$substring.

 

${string//substring/replacement}

使用$replacement來替換所有匹配的$substring.

 

${string/#substring/replacement}

如果$substring匹配$string的開頭部分, 那么就用$replacement來替換$substring.

 

${string/%substring/replacement}

如果$substring匹配$string的結尾部分, 那么就用$replacement來替換$substring.

 

概要示例:下面我們把變量var中的空格替換成下划線看看。

// 用{}運算符,還記得么?網中人的教程。
$ 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命令實現,它會把文件中所有的行全部倒轉過來。在一定意義上來說,排序實際 上也是一個字符串替換。

 

在替換子串時再提供一些tr的用法:

 

 
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
tr 用來從標准輸入中通過替換或刪除操作,進行字符轉換。 tr 主要用於刪除文件中控制字符或進行字符轉換。使用 tr 時要轉換兩個字符串:字符串1用於查詢,字符串2用於處理各種轉換, tr 剛執行時,字符串1中的字符被映射到字符串2中的字符,然后轉換操作開始。
 
1、用法和選項
用法:
 
tr [選項]... SET1 [SET2]
 
說明:
 
tr 命令用於從標准輸入中替換、縮減和/或刪除字符,並將結果寫到標准輸出。
 
選項:
 
-c, -C, --complement          首先補足SET1
-d, --delete                    刪除匹配SET1 的內容,並不作替換
-s, --squeeze-repeats   如果匹配於SET1 的字符在輸入序列中存在連續的重復,在替換時會被統一縮為一個字符的長度
-t, --truncate-set1       先將SET1 的長度截為和SET2 相等
--help                            顯示此幫助信息並退出
--version                       顯示版本信息並退出
2、SET字符串
 
SET 是一組字符串,一般都可按照字面含義理解。解析序列如下:
 
\NNN          #八進制值為NNN 的字符(1 至3 個數位)  
\\            #反斜杠  
\a            #終端鳴響  
\b            #退格  
\f            #換頁  
\n            #換行  
\r            #回車  
\t            #水平制表符  
\ v            #垂直制表符  
字符1-字符2    #從字符1 到字符2 的升序遞增過程中經歷的所有字符  
[字符*]       #在SET2 中適用,指定字符會被連續復制直到吻合設置1 的長度  
[字符*次數]    #對字符執行指定次數的復制,若次數以 0 開頭則被視為八進制數  
[:alnum:]     #所有的字母和數字  
[:alpha:]     #所有的字母  
[:blank:]     #所有呈水平排列的空白字符  
[:cntrl:]     #所有的控制字符  
[:digit:]     #所有的數字  
[:graph:]     #所有的可打印字符,不包括空格  
[:lower:]     #所有的小寫字母  
[:print:]     #所有的可打印字符,包括空格  
[:punct:]     #所有的標點字符  
[:space:]     #所有呈水平或垂直排列的空白字符  
[:upper:]     #所有的大寫字母  
[:xdigit:]    #所有的十六進制數  
[=字符=]       #所有和指定字符相等的字符 
注意:
 
1、僅在SET1 和SET2 都給出,同時沒有-d 選項的時候才會進行替換。
 
2、僅在替換時才可能用到-t 選項。如果需要SET2 將被通過在末尾添加原來的末字符的方式補充到同SET1 等長。SET2 中多余的字符將被省略。
 
3、只有[:lower:] 和[:upper:]以升序展開字符;在用於替換時的SET2 中以成對表示大小寫轉換。
 
4、-s 作用於SET1,既不替換也不刪除,否則在替換或展開后使用SET2 縮減。

 

tr示例:

 

 
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
1、將文件 file 中出現的 "abc" 替換為 "xyz"
 
# cat file | tr "abc" "xyz" > new_file
 
【注意】這里,凡是在 file 中出現的 "a" 字母,都替換成 "x" 字母, "b" 字母替換為 "y" 字母, "c" 字母替換為 "z" 字母。而不是將字符串 "abc" 替換為字符串 "xyz"
 
  
 
2、使用 tr 命令“統一”字母大小寫
 
(小寫 --> 大寫)
# cat file | tr [a-z] [A-Z] > new_file
 
(大寫 --> 小寫)
# cat file | tr [A-Z] [a-z] > new_file
 
  
 
3、把文件中的數字0-9替換為a-j
 
# cat file | tr [0-9] [a-j] > new_file
 
  
 
4、刪除文件 file 中出現的 "Snail" 字符
 
# cat file | tr -d "Snail" > new_file
 
【注意】這里,凡是在 file 文件中出現的 'S' , 'n' , 'a' , 'i' , 'l' 字符都會被刪除!而不是緊緊刪除出現的"Snail”字符串。
 
  
 
5、刪除文件 file 中出現的換行 '\n' 、制表 '\t' 字符
 
# cat file | tr -d "\n\t" > new_file
 
不可見字符都得用轉義字符來表示的,這個都是統一的。
 
  
 
6、刪除“連續着的”重復字母,只保留第一個
 
# cat file | tr -s [a-zA-Z] > new_file
 
  
 
7、刪除空行
 
# cat file | tr -s "\n" > new_file
 
  
 
8、刪除Windows文件“造成”的 '^M' 字符
 
# cat file | tr -d "\r" > new_file
或者
# cat file | tr -s "\r" "\n" > new_file
 
【注意】這里-s后面是兩個參數 "\r" "\n" ,用后者替換前者
 
  
 
9、用空格符\040替換制表符\011
 
# cat file | tr -s "\011" "\040" > new_file
 
  
 
10、把路徑變量中的冒號 ":" ,替換成換行符 "\n"
 
# echo $PATH | tr -s ":" "\n"

 

 

2.4. 插入子串

插入子串:就是在指定的位置插入子串,這個位置可能是某個子串的位置,也可能是從某個文件開頭算起的某個長度。通過上面的練習,我們發現這兩者之間 實際上是類似的。

公式:插入子串=把"old子串"替換成"old子串+new子串"或者"new子串+old子串"

概要示例::下面在var字符串的空格之前或之后插入一個下划線

// 用{}
$ 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字符串中所有的空格給刪除掉。

鼓勵: 這樣一替換不知道變成什么單詞啦,誰認得呢?但是中文卻是連在一起的,所以中文有多難,你想到了么?原來你也是個語言天才,而英語並不可怕,你有學會它的 天賦,只要你有這個打算。

// 再用{}
$ 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,可以指定分割符,並指定需要排序的列。

$ 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
2.7. 子串進制轉換

如果字母和數字字符用來計數,那么就存在進制轉換的問題。在數值計算一節的回復資料里,我們已經介紹了bc命令,這里再簡單的復習一下。

$ echo "ibase=10;obase=16;10" | bc
A


說明:ibase指定輸入進制,obase指出輸出進制,這樣通過調整ibase和obase,你想怎么轉就怎么轉啦!

2.7. 子串編碼轉換

什么是字符編碼?這個就不用介紹了吧,看過那些亂七八糟顯示的網頁么?大多是因為瀏覽器顯示時的”編碼“和網頁實際采用的”編碼“不一致導致的。字 符編碼通常是指把一序列”可打印“字符轉換成二進制表示,而字符解碼呢則是執行相反的過程,如果這兩個過程不匹配,則出現了所謂的”亂碼“。

為了解決”亂碼“問題呢?就需要進行編碼轉換。在linux下,我們可以使用iconv這個工具來進行相關操作。這樣的情況經常在不同的操作系統之 間移動文件,不同的編輯器之間交換文件的時候遇到,目前在windows下常用的漢字編碼是gb2312,而在linux下則大多采用utf8。

$ 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

$ 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查看。下面我們對這個文件以及相關的文件進行一些有意義的操作。

// 選取/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命令 也可以做到,例如:

$ IFS=":"; cat /etc/group | while read C1 C2 C3 C4; do echo $C1 $C3; done

因此,熟悉這些用法,我們的工作將變得非常靈活有趣。

到這里,需要做一個簡單的練習,如何把按照列對應的用戶名和用戶ID轉換成按照行對應的,即把類似下面的數據:

$ cat /etc/passwd | cut -d":" -f1,3 --output-delimiter=" "
root 0
bin 1
daemon 2

轉換成:

$ cat a
root    bin     daemon 
0       1       2

並轉換回去,有什么辦法呢?記得諸如tr,paste,split等命令都可以使用。

參考方法:
*正轉換:先截取用戶名一列存入文件user,再截取用戶ID存入id,再把兩個文件用paste -s命令連在一起,這樣就完成了正轉換。
*逆轉換:先把正轉換得到的結果用split -1拆分成兩個文件,再把兩個拆分后的文件用tr把分割符"\t"替換成"\n",只有用paste命令把兩個文件連在一起,這樣就完成了逆轉換。

在做shell批處理程序時候,經常會涉及到字符串相關操作。有很多命令語句,如:awk,sed都可以做字符串各種操作。 其實shell內置一系列操作符號,可以達到類似效果,大家知道,使用內部操作符會省略啟動外部程序等時間,因此速度會非常的快。

 

一、判斷讀取字符串值

 

表達式 含義
${var} 變量var的值, 與$var相同
   
${var-DEFAULT} 如果var沒有被聲明, 那么就以$DEFAULT作為其值 *
${var:-DEFAULT} 如果var沒有被聲明, 或者其值為空, 那么就以$DEFAULT作為其值 *
${var=DEFAULT} 如果var沒有被聲明, 那么就以$DEFAULT作為其值 *
${var:=DEFAULT} 如果var沒有被聲明, 或者其值為空, 那么就以$DEFAULT作為其值 *
${var+OTHER} 如果var聲明了, 那么其值就是$OTHER, 否則就為null字符串
${var:+OTHER} 如果var被設置了, 那么其值就是$OTHER, 否則就為null字符串
${var?ERR_MSG} 如果var沒被聲明, 那么就打印$ERR_MSG *
${var:?ERR_MSG} 如果var沒被設置, 那么就打印$ERR_MSG *
${!varprefix*} 匹配之前所有以varprefix開頭進行聲明的變量
${!varprefix@} 匹配之前所有以varprefix開頭進行聲明的變量

 

加入了“*”  的意思是: 如果變量var已經被設置的話, 那么其值就是$var.

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[a@localhost ~]$ echo ${abc- 'ok' }
ok
[a@localhost ~]$ echo $abc
 
[a@localhost ~]$ echo ${abc= 'ok' }
ok
[a@localhost ~]$ echo $abc
ok
 
如果abc 沒有聲明“=" 還會給abc賦值。
[a@localhost ~]$ var1=11;var2=12;var3=
[a@localhost ~]$ echo ${! v @}           
var1 var2 var3
[a@localhost ~]$ echo ${! v *}
var1 var2 var3
 
${!varprefix*}與${!varprefix@}相似,可以通過變量名前綴字符,搜索已經定義的變量,無論是否為空值。
表達式 含義
${#string} $string的長度
${string:position} 在$string中, 從位置$position開始提取子串
${string:position:length} 在$string中, 從位置$position開始提取長度為$length的子串
${string#substring} 從變量$string的開頭, 刪除最短匹配$substring的子串
${string##substring} 從變量$string的開頭, 刪除最長匹配$substring的子串
${string%substring} 從變量$string的結尾, 刪除最短匹配$substring的子串
${string%%substring} 從變量$string的結尾, 刪除最長匹配$substring的子串
${string/substring/replacement} 使用$replacement, 來代替第一個匹配的$substring
${string//substring/replacement} 使用$replacement, 代替所有匹配的$substring
${string/#substring/replacement} 如果$string的前綴匹配$substring, 那么就用$replacement來代替匹配到的$substring
${string/%substring/replacement} 如果$string的后綴匹配$substring, 那么就用$replacement來代替匹配到的$substring

 

說明:"* $substring”可以是一個正則表達式.

 

 
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
1.長度
 
[a@localhost ~]$ test = 'I love china'
[a@localhost ~]$ echo ${ #test}
12
 
${ #變量名}得到字符串長度
 
  
 
2.截取字串
 
[a@localhost ~]$ test = 'I love china'
[a@localhost ~]$ echo ${ test :5}    
e china
[a@localhost ~]$ echo ${ test :5:10}
e china
 
${變量名:起始:長度}得到子字符串
 
  
 
3.字符串刪除
 
[a@localhost ~]$ test = 'c:/windows/boot.ini'
[a@localhost ~]$ echo ${ test #/}
c: /windows/boot .ini
[a@localhost ~]$ echo ${ test #*/}
windows /boot .ini
[a@localhost ~]$ echo ${ test ##*/}
boot.ini
 
[a@localhost ~]$ echo ${ test %/*}
c: /windows
[a@localhost ~]$ echo ${ test %%/*}
 
${變量名 #substring正則表達式}從字符串開頭開始配備substring,刪除匹配上的表達式。
 
${變量名%substring正則表達式}從字符串結尾開始配備substring,刪除匹配上的表達式。
 
注意:${ test ##*/},${test%/*} 分別是得到文件名,或者目錄地址最簡單方法。
 
4.字符串替換
 
[a@localhost ~]$ test = 'c:/windows/boot.ini'
[a@localhost ~]$ echo ${ test /\ // \\}
c:\windows /boot .ini
[a@localhost ~]$ echo ${ test // \ // \\}
c:\windows\boot.ini
 
${變量/查找/替換值} 一個“/”表示替換第一個,” // ”表示替換所有,當查找中出現了:”/”請加轉義符”\/”表示。
性能比較

 

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在shell中,通過 awk , sed , expr 等都可以實現,字符串上述操作。下面我們進行性能比較。
 
[a@localhost ~]$ test = 'c:/windows/boot.ini'                      
[a@localhost ~]$ time for i in $( seq 10000); do a=${ #test};done;          
 
real    0m0.173s
user    0m0.139s
sys     0m0.004s
 
[a@localhost ~]$ time for i in $( seq 10000); do a=$( expr length $ test ); done ;     
 
real    0m9.734s
user    0m1.628s
 
速度相差上百倍,調用外部命令處理,與內置操作符性能相差非常大。在shell編程中,盡量用內置操作符或者函數完成。使用 awk , sed 類似會出現這樣結果。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM