Shell 字符串操作

2020-12-24 14:52 更新

前言

忙活了一個(gè)禮拜,終于等到周末,可以空下來寫點(diǎn)東西。

之前已經(jīng)完成《數(shù)值運(yùn)算》《布爾運(yùn)算》,這次輪到介紹字符串操作 。咱們先得弄明白兩個(gè)內(nèi)容:

  • 什么是字符串?
  • 對字符串有哪些操作?

下面是"在線新華字典"的解釋:

字符串:簡稱“串”。有限字符的序列。數(shù)據(jù)元素為字符的線性表,是一種數(shù)據(jù)的邏輯結(jié)構(gòu)。在計(jì)算機(jī)中可有不同的存儲結(jié)構(gòu)。在串上可進(jìn)行求子串、插入字符、刪除字符、置換字符等運(yùn)算。

而字符呢?

字符:計(jì)算機(jī)程序設(shè)計(jì)及操作時(shí)使用的符號。包括字母、數(shù)字、空格符、提示符及各種專用字符等。

照這樣說,之前介紹的數(shù)值運(yùn)算中的數(shù)字,布爾運(yùn)算中的真假值,都是以字符的形式呈現(xiàn)出來的,是一種特別的字符,對它們的運(yùn)算只不過是字符操作的特例罷了。而這里將研究一般字符的運(yùn)算,它具有非常重要的意義,因?yàn)閷ξ覀儊碚f,一般的工作都是處理字符而已。這些運(yùn)算實(shí)際上將圍繞上述兩個(gè)定義來做,它們包括:

  • 找出字符或者字符串的類型,是數(shù)字、字母還是其他特定字符,是可打印字符,還是不可打印字符(一些控制字符)。

  • 找出組成字符串的字符個(gè)數(shù)和字符串的存儲結(jié)構(gòu)(比如數(shù)組)。

  • 對串的常規(guī)操作:求子串、插入字符、刪除字符、置換字符、字符串的比較等。

  • 對串的一些比較復(fù)雜而有趣的操作,這里將在最后介紹一些有趣的范例。

字符串的屬性

字符串的類型

字符有可能是數(shù)字、字母、空格、其他特殊字符,而字符串有可能是它們中的一種或者多種的組合,在組合之后還可能形成具有特定意義的字符串,諸如郵件地址,URL地址等。

范例:數(shù)字或者數(shù)字組合

$ i=5;j=9423483247234;
$ echo $i | grep -q "^[0-9]$"
$ echo $?
0
$ echo $j | grep -q "^[0-9]\+$"
$ echo $?
0

范例:字符組合(小寫字母、大寫字母、兩者的組合)

$ c="A"; d="fwefewjuew"; e="fewfEFWefwefe"
$ echo $c | grep -q "^[A-Z]$"
$ echo $d | grep -q "^[a-z]\+$"
$ echo $e | grep -q "^[a-zA-Z]\+$"

范例:字母和數(shù)字的組合

$ ic="432fwfwefeFWEwefwef"
$ echo $ic | grep -q "^[0-9a-zA-Z]\+$"

范例:空格或者 Tab 鍵等

$ echo " " | grep " "
$ echo -e "\t" | grep "[[:space:]]" #[[:space:]]會(huì)同時(shí)匹配空格和TAB鍵
$ echo -e " \t" | grep "[[:space:]]"
$ echo -e "\t" | grep "" #為在鍵盤上按下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

說明:

  • /dev/null/dev/zero 設(shè)備非常有趣,都猶如黑洞,什么東西掉進(jìn)去都會(huì)消失殆盡;后者還是個(gè)能源箱,總能從那里取到0,直到退出
  • [[:space:]]grep 用于匹配空格或 TAB 鍵字符的標(biāo)記,其他標(biāo)記請查幫助:man grep
  • 上面都是用 grep 來進(jìn)行模式匹配,實(shí)際上 sedawk 都可用來做模式匹配,關(guān)于匹配中用到的正則表達(dá)式知識,請參考后面的相關(guān)資料
  • 如果想判斷字符串是否為空,可判斷其長度是否為零,可通過 test 命令的 -z 選項(xiàng)來實(shí)現(xiàn),具體用法見 test 命令,man test

范例:判斷字符是否為可打印字符

$ echo "\t\n" | grep "[[:print:]]"
\t\n
$ echo $?
0
$ echo -e "\t\n" | grep "[[:print:]]"
$ echo $?
1

字符串的長度

除了組成字符串的字符類型外,字符串還有哪些屬性呢?組成字符串的字符個(gè)數(shù)。

下面我們來計(jì)算字符串的長度,即所有字符的個(gè)數(shù),并簡單介紹幾種求字符串中指定字符個(gè)數(shù)的方法。

范例:計(jì)算某個(gè)字符串的長度

即計(jì)算所有字符的個(gè)數(shù),計(jì)算方法五花八門,擇其優(yōu)著而用之:

$ 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

范例:計(jì)算某些指定字符或者字符組合的個(gè)數(shù)

$ 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

范例:統(tǒng)計(jì)單詞個(gè)數(shù)

更多相關(guān)信息見《數(shù)值計(jì)算》單詞統(tǒng)計(jì) 相關(guān)范例。

$ echo $var | wc -w
5
$ echo "$var" | tr " " "\n" | grep get | uniq -c
1
$ echo "$var" | tr " " "\n" | grep get | wc -l
1

說明:

${} 操作符在 Bash 里頭是一個(gè)“大?!保軇偃蜗喈?dāng)多的工作,具體就看網(wǎng)中人的《Shell十三問》之$(())$() 還有${}差在哪?"吧。

字符串的顯示

接下來討論如何控制字符在終端的顯示。

范例:在屏幕控制字符顯示位置、顏色、背景等

$ echo -e "\033[31;40m" #設(shè)置前景色為黑色,背景色為紅色
$ echo -e "\033[11;29H Hello, World\!" #在屏幕的第11行,29列開始打印字符串Hello,World!

范例:在屏幕的某個(gè)位置動(dòng)態(tài)顯示當(dāng)前系統(tǒng)時(shí)間

$ while :; do echo -e "\033[11;29H "$(date "+%Y-%m-%d %H:%M:%S"); done

范例:過濾掉某些控制字符串

col 命令過濾某些控制字符,在處理諸如 scriptscreen 等截屏命令的輸出結(jié)果時(shí),很有用。

$ screen -L
$ cat /bin/cat
$ exit
$ cat screenlog.0 | col -b   # 把一些控制字符過濾后,就可以保留可讀的操作日志

字符串的存儲

在我們看來,字符串是一連串的字符而已,但是為了操作方便,我們往往可以讓字符串呈現(xiàn)出一定的結(jié)構(gòu)。在這里,我們不關(guān)心字符串在內(nèi)存中的實(shí)際存儲結(jié)構(gòu),僅僅關(guān)系它呈現(xiàn)出來的邏輯結(jié)構(gòu)。比如,這樣一個(gè)字符串: get the length of me",我們可以從不同的方面來呈現(xiàn)它。

  • 通過字符在串中的位置來呈現(xiàn)它

這樣我們就可以通過指定位置來找到某個(gè)子串。這在 C 語言中通??梢岳弥羔榿碜?。而在 Shell 編程中,有很多可用的工具,諸如 expr,awk 都提供了類似方法來實(shí)現(xiàn)子串的查詢動(dòng)作。兩者都幾乎支持模式匹配 match 和完全匹配 index。這在后面的字符串操作中將詳細(xì)介紹。

  • 根據(jù)某個(gè)分割符來取得字符串的各個(gè)部分

這里最常見的就是行分割符、空格或者 TAB 分割符了,前者用來當(dāng)行號,我們似乎已經(jīng)司空見慣了,因?yàn)槲覀兊木庉嬈骶瓦@樣“莫名”地處理著行分割符(在 UNIX 下為 \\n,在其他系統(tǒng)下有一些不同,比如 Windows 下為 \r\n )。而空格或者 TAB 鍵經(jīng)常用來分割數(shù)據(jù)庫的各個(gè)字段,這似乎也是司空見慣的事情。

正因?yàn)檫@樣,所以產(chǎn)生了大量優(yōu)秀的行編輯工具,諸如 grepawk,sed 等。在“行內(nèi)”(姑且這么說吧,就是處理單行,即字符串中不再包含行分割符)的字符串分割方面,cutawk 提供了非常優(yōu)越的“行內(nèi)”(處理單行)處理能力。

  • 更方便地處理用分割符分割好的各個(gè)部分

同樣是用到分割符,但為了更方便的操作分割以后的字符串的各個(gè)部分,我們抽象了“數(shù)組”這么一個(gè)數(shù)據(jù)結(jié)構(gòu),從而讓我們更加方便地通過下標(biāo)來獲取某個(gè)指定的部分。 bash 提供了這么一種數(shù)據(jù)結(jié)構(gòu),而優(yōu)秀的 awk 也同樣提供了它,我們這里將簡單介紹它們的用法。

范例:把字符串拆分成字符串?dāng)?shù)組

  • Bash 提供的數(shù)組數(shù)據(jù)結(jié)構(gòu),以數(shù)字為下標(biāo)的,和 C 語言從 0 開始的下標(biāo)一樣

$ var="get the length of me"
$ var_arr=($var)    #把字符串var存放到字符串?dāng)?shù)組var_arr中,默認(rèn)以空格作為分割符
$ echo ${var_arr[0]} ${var_arr[1]} ${var_arr[2]} ${var_arr[3]} ${var_arr[4]}
get the length of me
$ echo ${var_arr[@]}    #整個(gè)字符串,可以用*代替@,下同
get the length of me
$ echo ${#var_arr[@]}   #類似于求字符串長度,`#`操作符也可用來求數(shù)組元素個(gè)數(shù)
5

也可以直接給某個(gè)數(shù)組元素賦值

$ var_arr[5]="new_element"
$ echo ${var_arr[5]}
6
$ echo ${var_arr[5]}
new_element

Bash 實(shí)際上還提供了一種類似于“數(shù)組”的功能,即 for i in,它可以很方便地獲取某個(gè)字符串的各個(gè)部分,例如:

$ for i in $var; do echo -n $i"_"; done
get_the_length_of_me_
  • awk 里的數(shù)組,注意比較它和 Bash 里的數(shù)組的異同

split 把一行按照空格分割,存放到數(shù)組 var\_arr 中,并返回?cái)?shù)組長度。注意:這里第一個(gè)元素下標(biāo)不是 0,而是 1

$ echo $var | awk '{printf("%d %s\n", split($0, var_arr, " "), var_arr[1]);}'
5 get

實(shí)際上,上述操作很類似 awk 自身的行處理功能: awk 默認(rèn)把一行按照空格分割為多個(gè)域,并可以通過 $1,$2,$3``... 來獲取,$0 表示整行。

這里的 NF 是該行的域的總數(shù),類似于上面數(shù)組的長度,它同樣提供了一種通過類似“下標(biāo)”訪問某個(gè)字符串的功能。

$ 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 的“數(shù)組”功能何止于此呢,看看它的 for 引用吧,注意,這個(gè)和 Bash 里頭的 for 不太一樣,i 不是元素本身,而是下標(biāo):

$ echo $var | awk '{split($0, var_arr, " "); for(i in var_arr) printf("%s ",var_arr[i]);}'
of me get the length
4 5 1 2 3

另外,從上述結(jié)果可以看到,經(jīng)過 for 處理后,整個(gè)結(jié)果沒有按照原理的字符順序排列,不過如果僅僅是迭代出所有元素這個(gè)同樣很有意義。

awk 還有更“厲害”的處理能力,它的下標(biāo)可以不是數(shù)字,可以是字符串,從而變成了“關(guān)聯(lián)”數(shù)組,這種“關(guān)聯(lián)”在某些方面非常方便。比如,把某個(gè)文件中的某個(gè)系統(tǒng)調(diào)用名根據(jù)另外一個(gè)文件中的函數(shù)地址映射表替換成地址,可以這么實(shí)現(xiàn):

$ 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函數(shù)刪除某個(gè)數(shù)組元素。如果某些場合有需要的話,別忘了awk還支持二維數(shù)組。

字符串常規(guī)操作

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

取子串

取子串的方法主要有:

  • 直接到指定位置求子串
  • 字符匹配求子串

范例:按照位置取子串

比如從什么位置開始,取多少個(gè)字符

$ var="get the length of me"
$ echo ${var:0:3}
get
$ echo ${var:(-2)}   # 方向相反呢
me

$ echo `expr substr "$var" 5 3` #記得把$var引起來,否則expr會(huì)因?yàn)榭崭穸馕鲥e(cuò)誤
the

$ echo $var | awk '{printf("%s\n", substr($0, 9, 6))}'
length

awk$var 按照空格分開為多個(gè)變量,依次為 $1,$2,$3,$4,$5

$ echo $var | awk '{printf("%s\n", $1);}'
get
$ echo $var | awk '{printf("%s\n", $5);}'
me

差點(diǎn)略掉cut小工具,它用起來和awk類似,-d指定分割符,如同awk-F指定分割符一樣;-f指定“域”,如同awk的$數(shù)字。

$ echo $var | cut -d" " -f 5

范例:匹配字符求子串

用 Bash 內(nèi)置支持求字串:

$ echo ${var%% *} #從右邊開始計(jì)算,刪除最左邊的空格右邊的所有字符
get
$ echo ${var% *} #從右邊開始計(jì)算,刪除第一個(gè)空格右邊的所有字符
get the length of
$ echo ${var##* }  #從左邊開始計(jì)算,刪除最右邊的空格左邊的所有字符
me
$ echo ${var#* }  #從左邊開始計(jì)算,刪除第一個(gè)空格左邊的所有字符
the length of me

刪除所有 空格+字母組合 的字符串:

$ echo $var | sed 's/ [a-z]*//g'
get
$ echo $var | sed 's/[a-z]* //g'
me

sed 有按地址(行)打印(p)的功能,記得先用 tr 把空格換成行號:

$ echo $var | tr " " "\n" | sed -n 1p
get
$ echo $var | tr " " "\n" | sed -n 5p
me

tr 也可以用來取子串,它可以類似#% 來“拿掉”一些字符串來實(shí)現(xiàn)取子串:

$ echo $var | tr -d " "
getthelengthofme
$ echo $var | tr -cd "[a-z]" #把所有的空格都拿掉了,僅僅保留字母字符串,注意-c和-d的用法
getthelengthofme

說明:

  • %#刪除字符的方向不一樣,前者在右,后者在左,%%%, ### 的方向是前者是最大匹配,后者是最小匹配。(好的記憶方法見網(wǎng)中人的鍵盤記憶法:#$,% 是鍵盤依次從左到右的三個(gè)鍵)
  • tr-c 選項(xiàng)是 complement 的縮寫,即 invert,而 -d 選項(xiàng)是刪除,tr -cd "[a-z]" 這樣一來就變成保留所有的字母

對于字符串的截取,實(shí)際上還有一些命令,如果 head,tail 等可以實(shí)現(xiàn)有意思的功能,可以截取某個(gè)字符串的前面、后面指定的行數(shù)或者字節(jié)數(shù)。例如:

$ echo "abcdefghijk" | head -c 4
abcd
$ echo -n "abcdefghijk" | tail -c 4
hijk

查詢子串

子串查詢包括:

  • 返回符合某個(gè)模式的子串本身
  • 返回子串在目標(biāo)串中的位置

準(zhǔn)備:在進(jìn)行下面的操作之前,請準(zhǔn)備一個(gè)文件 test.txt,里頭有內(nèi)容 "consists of",用于下面的范例。

范例:查詢子串在目標(biāo)串中的位置

expr index貌似僅僅可以返回某個(gè)字符或者多個(gè)字符中第一個(gè)字符出現(xiàn)的位置

$ var="get the length of me"
$ expr index "$var" t
3

awk卻能找出字串,match還可以匹配正則表達(dá)式

$ echo $var | awk '{printf("%d\n", match($0,"the"));}'
5

范例:查詢子串,返回包含子串的行

awk,sed 都可以實(shí)現(xiàn)這些功能,但是 grep 最擅長

$ grep "consists of" test.txt   # 查詢文件包含consists of的行,并打印這些行
$ grep "consists[[:space:]]of" -n -H test.txt # 打印文件名,子串所在行的行號和該行的內(nèi)容
$ grep "consists[[:space:]]of" -n -o test.txt # 僅僅打印行號和匹配到的子串本身的內(nèi)容

$ awk '/consists of/{ printf("%s:%d:%s\n",FILENAME, FNR, $0)}' test.txt  #看到?jīng)]?和grep的結(jié)果一樣
$ sed -n -e '/consists of/=;/consists of/p' test.txt #同樣可以打印行號

說明:

  • awk,grep,sed 都能通過模式匹配查找指定字符串,但它們各有所長,將在后續(xù)章節(jié)中繼續(xù)使用和比較它們,進(jìn)而發(fā)現(xiàn)各自優(yōu)點(diǎn)
  • 在這里姑且把文件內(nèi)容當(dāng)成了一個(gè)大的字符串,在后面章節(jié)中將專門介紹文件操作,所以對文件內(nèi)容中存放字符串的操作將會(huì)有更深入的分析和介紹

子串替換

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

范例:把變量 var 中的空格替換成下劃線

{} 運(yùn)算符,還記得么?網(wǎng)中人的教程

$ var="get the length of me"
$ echo ${var/ /_}        #把第一個(gè)空格替換成下劃線
get_the length of me
$ echo ${var// /_}       #把所有空格都替換成下劃線
get_the_length_of_me

awk,awk 提供了轉(zhuǎn)換的最小替換函數(shù) sub 和全局替換函數(shù) 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'   #看到?jīng)]有,簡短兩個(gè)命令就實(shí)現(xiàn)了最小匹配和最大匹配g <= global
get_the_length_of_me

有忘記 tr 命令么?可以用替換單個(gè)字符的:

$ echo $var | tr " " "_"
get_the_length_of_me
$ echo $var | tr '[a-z]' '[A-Z]'   #這個(gè)可有意思了,把所有小寫字母都替換為大寫字母
GET THE LENGTH OF ME

說明: sed 還有很有趣的標(biāo)簽用法呢,下面再介紹吧。

有一種比較有意思的字符串替換是:整個(gè)文件行的倒置,這個(gè)可以通過 tac 命令實(shí)現(xiàn),它會(huì)把文件中所有的行全部倒轉(zhuǎn)過來。在某種意義上來說,排序?qū)嶋H上也是一個(gè)字符串替換。

插入子串

在指定位置插入子串,這個(gè)位置可能是某個(gè)子串的位置,也可能是從某個(gè)文件開頭算起的某個(gè)長度。通過上面的練習(xí),我們發(fā)現(xiàn)這兩者之間實(shí)際上是類似的。

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

范例:在 var 字符串的空格之前或之后插入一個(gè)下劃線

用{}:

$ var="get the length of me"
$ echo ${var/ /_ }        #在指定字符串之前插入一個(gè)字符串
get_ the length of me
$ echo ${var// /_ }
get_ the_ length_ of_ me
$ echo ${var/ / _}        #在指定字符串之后插入一個(gè)字符串
get _the length of me
$ echo ${var// / _}
get _the _length _of _me

其他的還用演示么?這里主要介紹sed怎么用來插入字符吧,因?yàn)樗臉?biāo)簽功能很有趣說明:() 將不匹配到的字符串存放為一個(gè)標(biāo)簽,按匹配順序?yàn)?code>\1,\2...

$ echo $var | sed -e 's/\( \)/_\1/'
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 的標(biāo)簽的順序是不是 \1,\2...,看到?jīng)]?\2\1 調(diào)換位置后,theget 的位置掉換了:

$ echo $var | sed -e 's/\([a-z]*\) \([a-z]*\) /\2 \1 /g'
the get of length me

sed 還有專門的插入指令,ai,分別表示在匹配的行后和行前插入指定字符

$ echo $var | sed '/get/a test'
get the length of me
test
$ echo $var | sed '/get/i test'
test
get the length of me

刪除子串

刪除子串:應(yīng)該很簡單了吧,把子串替換成“空”(什么都沒有)不就變成了刪除么。還是來簡單復(fù)習(xí)一下替換吧。

范例:把 var 字符串中所有的空格給刪除掉。

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

再用 {}

$ 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

如果要?jiǎng)h除第一個(gè)空格后面所有的字符串該怎么辦呢?還記得 {}#% 用法么?如果不記得,回到這節(jié)的開頭開始復(fù)習(xí)吧。(實(shí)際上刪除子串和取子串未嘗不是兩種互補(bǔ)的運(yùn)算呢,刪除掉某些不想要的子串,也就同時(shí)取得另外那些想要的子串——這個(gè)世界就是一個(gè)“二元”的世界,非常有趣)

子串比較

這個(gè)很簡單:還記得 test 命令的用法么? man test 。它可以用來判斷兩個(gè)字符串是否相等。另外,有發(fā)現(xiàn)“字符串是否相等”和“字符串能否跟另外一個(gè)字符串匹配 " 兩個(gè)問題之間的關(guān)系嗎?如果兩個(gè)字符串完全匹配,那么這兩個(gè)字符串就相等了。所以呢,上面用到的字符串匹配方法,也同樣可以用到這里。

子串排序

差點(diǎn)忘記這個(gè)重要內(nèi)容了,子串排序可是經(jīng)常用到,常見的有按字母序、數(shù)字序等正序或反序排列。 sort 命令可以用來做這個(gè)工作,它和其他行處理命令一樣,是按行操作的,另外,它類似 cutawk,可以指定分割符,并指定需要排序的列。

$ 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

子串進(jìn)制轉(zhuǎn)換

如果字母和數(shù)字字符用來計(jì)數(shù),那么就存在進(jìn)制轉(zhuǎn)換的問題。在《數(shù)值計(jì)算》一節(jié),已經(jīng)介紹了 bc 命令,這里再復(fù)習(xí)一下。

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

說明: ibase 指定輸入進(jìn)制,obase 指出輸出進(jìn)制,這樣通過調(diào)整 ibaseobase,你想怎么轉(zhuǎn)就怎么轉(zhuǎn)啦!

子串編碼轉(zhuǎn)換

什么是字符編碼?這個(gè)就不用介紹了吧,看過那些亂七八糟顯示的網(wǎng)頁么?大多是因?yàn)闉g覽器顯示時(shí)的”編碼“和網(wǎng)頁實(shí)際采用的”編碼“不一致導(dǎo)致的。字符編碼通常是指:把一序列”可打印“字符轉(zhuǎn)換成二進(jìn)制表示,而字符解碼呢則是執(zhí)行相反的過程,如果這兩個(gè)過程不匹配,則出現(xiàn)了所謂的”亂碼“。

為了解決”亂碼“問題呢?就需要進(jìn)行編碼轉(zhuǎn)換。在 Linux 下,我們可以使用 iconv 這個(gè)工具來進(jìn)行相關(guān)操作。這樣的情況經(jīng)常在不同的操作系統(tǒng)之間移動(dòng)文件,不同的編輯器之間交換文件的時(shí)候遇到,目前在 Windows 下常用的漢字編碼是 gb2312,而在 Linux 下則大多采用 utf8 。

$ nihao_utf8=$(echo "你好")
$ nihao_gb2312=$(echo $nihao_utf8 | iconv -f utf8 -t gb2312)

字符串操作進(jìn)階

實(shí)際上,在用 Bash 編程時(shí),大部分時(shí)間都是在處理字符串,因此把這一節(jié)熟練掌握非常重要。

正則表達(dá)式

范例:處理 URL 地址

URL 地址(URL(Uniform Resoure Locator:統(tǒng)一資源定位器)是WWW頁的地址)幾乎是我們?nèi)粘I畹耐姘椋覀円呀?jīng)到了無法離開它的地步啦,對它的操作很多,包括判斷 URL 地址的有效性,截取地址的各個(gè)部分(服務(wù)器類型、服務(wù)器地址、端口、路徑等)并對各個(gè)部分進(jìn)行進(jìn)一步的操作。

下面我們來具體處理這個(gè)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\./-]*"

截取服務(wù)器類型

$ 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

截取文件類型(擴(kuò)展名)

$ echo $url | sed -e 's/.*[0-9].\(.*\)/\1/g'
tar.gz

范例:匹配某個(gè)文件中的特定范圍的行

先準(zhǔn)備一個(gè)測試文件README

Chapter 7 -- Exercises

7.1 please execute the program: mainwithoutreturn, and print the return value
of it with the command "echo $?", and then compare the return of the printf
function, they are the same.

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.

7.3 there is no another effective method in most OS. because argc and argv are
not global variables like environ.

然后開始實(shí)驗(yàn),

打印出答案前指定行范圍:第 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

其實(shí),因?yàn)檫@個(gè)文件內(nèi)容格式很有特色,有更簡單的辦法

$ 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.

有了上面的知識,就可以非常容易地進(jìn)行這些工作啦:修改某個(gè)文件的文件名,比如調(diào)整它的編碼,下載某個(gè)網(wǎng)頁里頭的所有 pdf 文檔等。這些就作為練習(xí)自己做吧。

處理格式化的文本

平時(shí)做工作,大多數(shù)時(shí)候處理的都是一些“格式化”的文本,比如類似 /etc/passwd 這樣的有固定行和列的文本,也有類似 tree 命令輸出的那種具有樹形結(jié)構(gòu)的文本,當(dāng)然還有其他具有特定結(jié)構(gòu)的文本。

關(guān)于樹狀結(jié)構(gòu)的文本的處理,可以參考我早期寫的另外一篇博客文章:源碼分析:靜態(tài)分析 C 程序函數(shù)調(diào)用關(guān)系圖

實(shí)際上,只要把握好特性結(jié)構(gòu)的一些特點(diǎn),并根據(jù)具體的應(yīng)用場合,處理起來就不會(huì)困難。

下面來介紹具體文本的操作,以 /etc/passwd 文件為例。關(guān)于這個(gè)文件的幫忙和用法,請通過 man 5 passwd 查看。下面對這個(gè)文件以及相關(guān)的文件進(jìn)行一些有意義的操作。

范例:選取指定列

選取/etc/passwd文件中的用戶名和組ID兩列

$ cat /etc/passwd | cut -d":" -f1,4

選取/etc/group文件中的組名和組ID兩列

$ cat /etc/group | cut -d":" -f1,3

范例:文件關(guān)聯(lián)操作

如果想找出所有用戶所在的組,怎么辦?

$ 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 命令用來連接兩個(gè)文件,有點(diǎn)類似于數(shù)據(jù)庫的兩個(gè)表的連接。 -t 指定分割符,-1 4 -2 3 指定按照第一個(gè)文件的第 4 列和第二個(gè)文件的第 3 列,即組 ID 進(jìn)行連接,-o``1.1,2.1 表示僅僅輸出第一個(gè)文件的第一列和第二個(gè)文件的第一列,這樣就得到了我們要的結(jié)果,不過,可惜的是,這個(gè)結(jié)果并不準(zhǔn)確,再進(jìn)行下面的操作,你就會(huì)發(fā)現(xiàn):

$ 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

可以看到這個(gè)結(jié)果才是正確的,所以以后使用 join 千萬要注意這個(gè)問題,否則采取更保守的做法似乎更能保證正確性,更多關(guān)于文件連接的討論見參考后續(xù)資料。

上面涉及到了處理某格式化行中的指定列,包括截取(如 SQLselect 用法),連接(如 SQLjoin 用法),排序(如 SQLorder by 用法),都可以通過指定分割符來拆分某個(gè)格式化的行,另外,“截取”的做法還有很多,不光是 cut,awk,甚至通過 IFS 指定分割符的 read 命令也可以做到,例如:

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

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

到這里,需要做一個(gè)簡單的練習(xí),如何把按照列對應(yīng)的用戶名和用戶 ID 轉(zhuǎn)換成按照行對應(yīng)的,即把類似下面的數(shù)據(jù):

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

轉(zhuǎn)換成:

$ cat a
root    bin     daemon
0       1       2

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

參考方法:

  • 正轉(zhuǎn)換:先截取用戶名一列存入文件 user,再截取用戶 ID 存入 id,再把兩個(gè)文件用 paste -s 命令連在一起,這樣就完成了正轉(zhuǎn)換
  • 逆轉(zhuǎn)換:先把正轉(zhuǎn)換得到的結(jié)果用 split -1 拆分成兩個(gè)文件,再把兩個(gè)拆分后的文件用 tr 把分割符 \t 替換成 \n,只有用 paste 命令把兩個(gè)文件連在一起,這樣就完成了逆轉(zhuǎn)換。

參考資料

后記

  • 這一節(jié)本來是上個(gè)禮拜該弄好的,但是這些天太忙了,到現(xiàn)在才寫好一個(gè)“初稿”,等到有時(shí)間再補(bǔ)充具體的范例。這一節(jié)的范例應(yīng)該是最最有趣的,所有得好好研究一下幾個(gè)有趣的范例。
  • 寫完上面的部分貌似是 1 點(diǎn)多,剛 check 了一下錯(cuò)別字和語法什么的,再添加了一節(jié),即“字符串的存儲結(jié)構(gòu)”,到現(xiàn)在已經(jīng)快 half past 2 啦,晚安,朋友們。
  • 26 號,添加“子串進(jìn)制轉(zhuǎn)換”和“子串編碼轉(zhuǎn)換”兩小節(jié)以及一個(gè)處理 URL 地址的范例。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號