導讀
字串處理是 shell 指令碼的重點部分,因為 shell 指令碼主要的工作是和檔案或者其他程式打交道,資料格式通常是文字,而處理沒有統一格式的文字檔案出奇地複雜,shell 命令中也有很多都是處理文字的。用 bash 處理文字的話,因為自身的功能有限,經常需要呼叫像 awk
、sed
、grep
、cat
、cut
、comm
、dirname
、basename
、expr
、sort
、uniq
、head
、tail
、tac
、tr
、wc
這樣命令,不留神指令碼就成了命令大聚會。命令用法各異,有的很簡單(比如 cut
、tr
、wc
),看一眼 man 就會用;有的很複雜(比如 awk
、sed
、grep
),用了好多年基本也只會用很少一部分功能。互相配合也容易出現各種各樣的問題(比如要命的空格和換行符問題),難以除錯,呼叫命令的開銷也很大。而用好了 zsh 的話,可以大幅減少這些命令的使用(並不能完全避免,因為某些場景確實比較適合用這樣的命令處理,比如處理一個大文字檔案),並且大幅提升指令碼的效能(主要因為減少了程式啟動的開銷,比如一次簡單的字串替換,呼叫外部命令實現比內部實現的時間要多好幾個數量級)。
但也因此 zsh 的字串處理功能很複雜,可以說 zsh 的字串處理功能,要比絕大多數程式語言自帶的字串函式庫或者類庫要強大(在不依賴外部命令的情況)。同時各種用法也比較怪異,很多時候簡潔性和可讀性是有矛盾的,很難兼顧。而 shell 的使用場景決定簡潔性是不能被犧牲掉的,即使用 Python 這樣比較簡潔的語言來處理字串,很多時候也只能寫出冗長的程式碼,而 zsh 經常可以一行搞定(可能有人想到了 Perl,Perl 在處理文字方面確實有比較明顯的優勢,但使用 Perl 的話也要承擔更多的成本),如果再加上適當地使用外部命令,基本可以應付大多數字符串處理場景。因為字串處理的內容比較豐富,我會分多篇文章寫。本篇只涉及最基礎和常用的字串操作,包括字串的拼接、切片、截斷、查詢、遍歷、替換、匹配、大小寫轉換、分隔等等。
字串定義和簡單比較,我已經在前一篇文章提過了,現在直接進入正題。
字串長度
% str=abcde
% echo $#str
5
# 讀取函式或者指令碼的第一個引數的長度
% echo $#1複製程式碼
字串拼接
% str1=abc
% str2=def
% str2+=$str1
% echo $str2
defabc
% str3=$str1$str2
abcdefabc複製程式碼
字串切片
字串切片之前也提過,這裡簡單複習一下。逗號前後不能有空格。字元位置是從 1 開始算起的。
% str=abcdef
% echo $str[2,4]
bcd
% echo $str[2,-1]
bcdef
# $1 是檔案或者函式的第一個引數
echo ${1[2,4]}複製程式碼
字串切片還有另一種風格的方法,即 bash 風格,功能大同小異。通常沒有必要用這個,而且因為字元位置是從 0 開始算,容易混淆。
% str=abcdef
% echo ${str:1:3}
bcd
% echo ${str:1:-1}
bcde複製程式碼
字串截斷
% str=abcdeabcde
# 刪除左端匹配到的內容,最小匹配
% echo ${str#*b}
cdeabcde
# 刪除右端匹配到的內容,最小匹配
% echo ${str%d*}
abcdeabc
# 刪除左端匹配到的內容,最大匹配
% echo ${str##*b}
cde
# 刪除右端匹配到的內容
% echo ${str%%d*}
abc複製程式碼
字串查詢
子字串定位。
% str=abcdef
# 這裡用的是 i 的大寫,不是 L 的小寫
% echo $str[(I)cd]
3
# I 是從右往左找,如果找不到則為 0, 方便用來判斷
% (($str[(I)cd])) && echo good
good
# 找不到則為 0
% echo $str[(I)cdd]
0
# 也可以使用小 i,小 i 是從左往右找,找不到則返回陣列大小 + 1
% echo $str[(i)cd]
3
% echo $str[(i)cdd]
7複製程式碼
遍歷字元
% str=abcd
% for i ({1..$#str}) {
> echo $str[i]
>}
a
b
c
d複製程式碼
字串替換
按內容替換和刪除字元。
% str=abcabc
# 只替換找到的第一個
% echo ${str/bc/ef}
aefabc
# 刪除匹配到的第一個
% echo ${str/bc}
aabc
# 替換所有找到的
% echo ${str//bc/ef}
aefaef
# 刪除匹配到的所有的
% echo ${str//bc}
aa
% str=abcABCabcABCabc
# /# 只從字串開頭開始匹配,${str/#abc} 也同理
% echo ${str/#abc/123}
123ABCabcABCabc
# /% 只從字串結尾開始匹配,echo ${str/%abc} 也同理
% echo ${str/%abc/123}
abcABCabcABC123
% str=abc
# 如果匹配到了則輸出空字串
% echo ${str:#ab*}
# 如果匹配不到,則輸出原字串
% echo ${str:#ab}
abc
# 加 (M) 後效果反轉
% echo ${(M)str:#ab}複製程式碼
按位置刪除字元。
%str=abcdef
# 刪除指定位置字元
% str[1]=
% echo $str
bcdef
# 可以刪除多個
% str[2,4]=
% echo $str
bf複製程式碼
按位置替換字元。
% str=abcdefg
# 一對一地替換
% str[2]=1
% echo $str
a1cdefg
# 可以多對多(也包括一對多和多對一)地替換字元,兩邊的字元數量不需要一致。
# 把第二、三個字元替換成 2345
% str[2,3]=2345
% echo $str
a2345defg複製程式碼
判斷字串變數是否存在
如果用 [[ "$strxx" == "" ]]
,那無法區分變數是沒有定義還是內容為空,在某些情況是需要區分二者的。
% (($+strxx)) && echo good
% strxx=""
% (($+strxx)) && echo good
good複製程式碼
(($+var))
的用法也可以用來判斷其他型別的變數,如果變數存在則返回真(0),否則返回假(1)。
字串匹配判斷
判斷是否包含字串。
% str1=abcd
% str2=bc
% [[ $str1 == *$str2* ]] && echo good
good複製程式碼
正規表示式匹配。
% str=abc55def
# 少量字串的話,儘量不要用 grep
# 本文不講正規表示式格式相關內容
# 另外 zsh 有專門的正規表示式模組
% [[ $str =~ "c[0-9]{2}de" ]] && echo a
a複製程式碼
大小寫轉換
% str="ABCDE abcde"
# 轉成大寫,(U) 和 :u 兩種用法效果一樣
% echo ${(U)str} --- ${str:u}
ABCDE ABCDE --- ABCDE ABCDE
# 轉成小寫,(L) 和 :l 兩種用法效果一樣
% echo ${(L)str} --- ${str:l}
abcde abcde --- abcde abcde
# 轉成首字母大寫
% echo ${(C)str}
Abcde Abcde複製程式碼
目錄檔名擷取
% filepath=/a/b/c.x
# :h 是取目錄名,即最後一個 / 之前的部分,如果沒有 / 則為 .
% echo ${filepath:h}
/a/b
# :t 是取檔名,即最後一個 / 之後的部分,如果沒有 / 則為字串本身
% echo ${filepath:t}
c.x
# :e 是取副檔名,即檔名中最後一個點之後的部分,如果沒有點則為空
% echo ${filepath:e}
x
# :r 是去掉末尾副檔名的路徑
% echo ${filepath:r}
/a/b/c複製程式碼
字串分隔
# 使用空格作為分隔符,多個空格也只算一個分隔符
% str=`aa bb cc dd`
% echo ${str[(w)2]}
bb
% echo ${str[(w)3]}
cc
# 指定分隔符
% str=`aa--bb--cc`
# 如果分隔符是 : 就用別的字元作為左右界,比如 ws.:.
% echo ${str[(ws:--:)3]}
cc複製程式碼
多行字串
字串定義可以跨行。
% str="line1
> line2"
% echo $str
line1
line2複製程式碼
讀取檔案內容到字串
# 比用 str=$(cat filename) 效能好很多
str=$(<filename)
# 比用 cat filename 效能好很多,引號不能省略
echo "$(<filename)"
# 遍歷每行,引號不能省略
for i (${(f)"$(<filename)"}) {
echo $i
}複製程式碼
讀取檔案指定行。
檔案 test.txt 內容如下:
line 1. apple
line 2. orange複製程式碼
# 小檔案或者需要頻繁呼叫時,儘量不要用 sed
% echo ${"$(<test.txt)"[(f)2]}
line 2. orange
# 輸出包含 “ang” 的第一行
% echo ${"$(<test.txt)"[(fr)*ang*]}
line 2. orange
# 輸出包含 pp 的第一行,但從左截掉 “line” 4個字元。
echo ${"$(<test.txt)"[(fr)*pp*]#line}複製程式碼
讀取程式輸出到字串
讀程式輸出和讀檔案類似。
上邊字串相關的處理,直接把 $(<test.txt)
換成 $(命令)
即可。如果一定需要一個檔名,可以這樣。
# 返回 fd 路徑,優先使用,但某些場景會出錯
% wc -l <(ps)
4 /proc/self/fd/11
# 臨時檔案,會自動刪除,適合上邊用法出錯的情況
% wc -l =(ps)
3 /tmp/zshMWDpqD複製程式碼
參考
tim.vanwerkhoven.org/post/2012/1…
全系列文章地址:github.com/goreliu/zsh…
付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活定價,歡迎諮詢,微信 ly50247。