,避開那些坑)
天天在終端里跟文本文件打交道的人一定繞不開這個場景配置文件里幾十個IP要換日志文件里某段格式要改或者一組文件里同一個關(guān)鍵詞要批量替換。手寫編輯器逐個文件去打那效率太低人也遭罪。這時候sed替換命令就是最順手的那把刀。我用了十幾年Linux接觸sed到現(xiàn)在最常用的就是它的替換功能。這工具說白了就是專職處理文本的自動化流水線你給它輸入文本流它按照你設(shè)好的規(guī)則逐行處理再把結(jié)果輸出。不管是單文件修改、多文件批量處理還是嵌在shell腳本里做數(shù)據(jù)處理它都能頂上去。這篇就圍繞sed替換命令把用法、原理、坑點一次理清楚適合剛開始接觸命令行、被文本處理折磨過的開發(fā)者也適合想提升日常效率的運維和數(shù)據(jù)分析朋友。1. 先搞清楚sed到底在干什么很多人用sed時容易懵就是因為直接背參數(shù)、抄命令卻不知道它背后是怎么工作的。結(jié)果換個場景就不會用了或者遇到意外行為完全摸不著頭腦。1.1 不是文本替換器是流式工廠流水線我第一次接觸sed時以為它就是一個按下按鈕就全局替換文本的工具。實際上它是一臺流式處理流水線。sed這個名字的含義就是流編輯器Stream Editor和vim這種交互式編輯器完全不同。它的工作方式可以這樣理解有一條傳送帶文本文件的每一行都是一個被傳送的物品。sed程序站在傳送帶旁邊把物品一件一件拿下來檢查這一行內(nèi)容是否符合你設(shè)定的條件。如果符合就按照指定操作處理一下比如替換內(nèi)容然后放到輸出傳送帶上如果不符合就直接原樣放行。整條流水線從頭走到尾sed一次處理一行處理完立刻輸出內(nèi)存里永遠只保留當前正在處理的行。這個機制解釋了很多現(xiàn)象。比如為什么sed處理大文件很快因為它不需要把整個文件都裝進內(nèi)存哪怕是幾個GB的日志文件也能平穩(wěn)處理。為什么sed默認只替換每行的第一個匹配結(jié)果而不是全部因為這是設(shè)計者認為最常用的需求想要全換加個g標志就行。這些行為不是bug是流式處理設(shè)計下的必然特征。我打個比方一臺流水線機器上有個檢測探頭檢查每個經(jīng)過的零件上有沒有劃痕有就翻新一下然后放行沒有就直接放走。這個探頭不可能回頭看已經(jīng)走過的零件也不關(guān)心下一個零件長什么樣它只對當前手上的這個零件負責。sed就是這個探頭。1.2 什么時候應(yīng)該用sed什么時候不該用用熟了之后你會發(fā)現(xiàn)sed不是銀彈。知道它的邊界能讓工作更高效。適合用sed的場景有這些批量修改多個文件的同一處內(nèi)容比如把項目里所有配置文件里的IP地址換成新的。在管道命令鏈里充當文本處理中間環(huán)節(jié)比如從某個命令的輸出中提取、清洗需要的信息。對日志文件做快速格式化或脫敏比如把時間戳去掉、把敏感ID打碼。需要寫進shell腳本里做自動化文本處理sed命令輕量、隨處可用不像Perl或Python還需要依賴環(huán)境。不太適合用sed的情況需要跨多行進行復(fù)雜邏輯判斷的文本處理。sed雖然支持范圍尋址比如從第10行到第20行之間操作但對于如果這一行是A那接下來三行忽略直到遇到B才恢復(fù)這種有狀態(tài)邏輯寫起來很痛苦遠不如awk甚至Python清晰。對CSV或JSON這類結(jié)構(gòu)化數(shù)據(jù)進行解析和提取。CSV要處理引號轉(zhuǎn)義、字段邊界JSON要處理嵌套層級這些用專門的工具比如jq、Python的csv模塊會穩(wěn)妥得多。涉及浮點運算、復(fù)雜的數(shù)值變換。sed只認文本不做數(shù)學計算這種場景交給awk更合理。給新手一個判斷標準如果你發(fā)現(xiàn)自己為了一個任務(wù)在sed上憋了超過十分鐘或者寫出來的sed命令長度快要超過半行屏幕了這時候就該停下來想想awk是不是更合適或者直接用Python寫個一次性腳本。2. 替換命令的基礎(chǔ)操作與細節(jié)真正上手首先要把s命令的底子打牢。這個命令是sed里應(yīng)用最頻繁的值得把每個細節(jié)都搞清楚。2.1 最經(jīng)典的s命令從每行第一個到全部替換s命令的完整結(jié)構(gòu)是sed s/舊內(nèi)容/新內(nèi)容/標志。默認情況下sed s/舊/新/只會替換每一行中第一個匹配到的內(nèi)容。我舉一個非常容易踩坑的小例子。有一個文本apple banana apple cherry apple pie執(zhí)行sed s/apple/orange/輸出結(jié)果是orange banana apple cherry orange pie注意第一行第二個apple沒有變。原因我在前面說過sed逐行處理默認處理完第一個匹配就移動到下一行。如果想每一行的所有匹配都換掉加上g標志就行sed s/apple/orange/g輸出orange banana orange cherry orange pie這里給一個參數(shù)速查表標志作用使用場景g全局替換一行內(nèi)所有匹配都換最常用幾乎默認必加數(shù)字如2只替換每行第指定個匹配只需要改第二個出現(xiàn)的舊詞時p打印處理后的行一般配-n用只想查看被替換的行i忽略大小寫替換時不區(qū)分大小寫w 文件把替換結(jié)果寫入指定文件需要單獨保存匹配行組合使用也常見比如sed -n s/error/警告/p這個命令只打印發(fā)生替換的那幾行在日志文件中快速定位包含關(guān)鍵字的記錄非常好用。2.2 定界符的秘密不止斜杠s命令默認用/做定界符但如果你要替換的內(nèi)容本身包含/比如路徑就會寫得很痛苦。像這樣sed s/\/usr\/local\/bin/\/opt\/bin/g這一堆斜杠轉(zhuǎn)義可讀性極差稍不注意就多寫或者少寫一個反斜杠。其實sed的定界符可以是任意字符。替換路徑時換成#或者|就清爽多了sed s#/usr/local/bin#/opt/bin#g也可以在替換URL時用比如sed shttp://old.example.comp https://new.example.compg這算是一個隱藏技巧很多用了多年命令行的人都未必注意到。但是要提醒一點選了哪個字符做定界符內(nèi)容里如果再出現(xiàn)這個字符就必須轉(zhuǎn)義。所以選定界符時先看看你要處理的文本里哪個字符出現(xiàn)得最少。處理路徑用#處理帶#號的文本就用/處理網(wǎng)址用根據(jù)內(nèi)容靈活選擇。2.3 原地修改-i參數(shù)順手但危險sed默認把處理結(jié)果輸出到標準輸出原文件不動。如果想要直接修改原文件加-i選項。第一次看到這個功能我腦海里彈出一個詞真方便。但有一次操作失誤體驗了它的真危險。事情是這樣的當時有一個項目里面幾十個yaml配置文件都要改某個縮進我寫了一條命令把^兩個空格縮進換成^四個空格縮進。結(jié)果因為正則沒寫對把所有的行首空格都刪掉了。文件全亂了好在當時有用版本控制一條回滾命令才救了回來。從那以后我養(yǎng)成了三個習慣分享給你第一使用-i之前先不帶-i跑一遍把輸出檢查一遍再動手。 第二重要文件先備份。sed -i.bak s/舊/新/ 文件這條命令會生成一個后綴為.bak的備份文件修改原文件。所有配置類文件、代碼文件都應(yīng)該這樣操作。 第三盡量不要在不確定的場景直接對多個文件執(zhí)行-i。先拿單個文件驗證再推廣到全部文件。另外還有個跨平臺的區(qū)別。Linux上的GNU sed-i后面可以直接跟替換表達式。但macOS上的BSD sed-i需要額外的備份后綴參數(shù)如果沒有提供后綴要這樣寫sed -i s/舊/新/ 文件注意-i和之間有一個空格。3. 正則表達式與sed替換的深度配合sed的真正威力藏在和正則表達式的配合里。掌握了正則替換命令才能說是真正入門。3.1 基礎(chǔ)正則和擴展正則一個轉(zhuǎn)義符的差距sed默認使用基礎(chǔ)正則表達式BRE在這種模式下一些在別的正則引擎里可以直接用的元字符比如、?、{、}、(、)都必須加上反斜杠才能發(fā)揮特殊作用。舉例來說想把連續(xù)出現(xiàn)兩次以上的字母a替換成A在基礎(chǔ)正則下要寫sed s/a\/A/g注意那個\。如果不加反斜杠a在基礎(chǔ)正則里就是普通字符a后面跟一個加號。這確實容易忘。好在GNU sed提供了-E選項有些版本也支持-r不過-E已經(jīng)成為POSIX標準啟用擴展正則ERE后寫正則的方式和大多數(shù)編程語言一致sed -E s/a/A/g我個人強烈建議只要正則稍微復(fù)雜一點就加上-E。不是顯得專業(yè)而是\、\?這種轉(zhuǎn)義寫法太反直覺容易出錯。特別是管道上的命令錯一個字符就會導(dǎo)致替換結(jié)果完全不對。延展一下在擴展正則模式下分組符號直接()次數(shù)量詞直接{m,n}就不再需要轉(zhuǎn)義。但注意這時候如果想要匹配字面意義上的()反而要普通字符本身就是說要匹配括號就要寫成\(。3.2 分組捕獲與反向引用把替換玩出花樣這是sed替換最精彩的部分。正則里的括號不僅能分組還能把匹配到的內(nèi)容記住然后在替換部分引用。舉一個最常見的例子交換兩列內(nèi)容。有一行文本是姓氏,名字zhang,san li,si wang,wu執(zhí)行sed -E s/(^[^,]),([^,]$)/\2,\1/這條命令中第一個(^[^,])捕獲姓氏第二個([^,]$)捕獲名字替換部分\2,\1就把兩者順序調(diào)換了。輸出san,zhang si,li wu,wang分組捕獲的價值在于你不需要知道具體內(nèi)容是什么只要知道它的格式就能提取、重組、重排。這個能力在處理批量數(shù)據(jù)時節(jié)省時間的效果非常顯著。另一個實用的引用是符號。在替換部分代表整個匹配到的內(nèi)容。用它來給匹配內(nèi)容加前綴或后綴比分組要方便得多。比如想給文中所有出現(xiàn)的版本號加上引號sed -E s/v[0-9]\.[0-9]//g如果文本是version v2.3 released執(zhí)行后就是version v2.3 released。把整段匹配內(nèi)容直接原樣放到了引號中間不用寫分組。和\1各有適用場景需要部分引用時用分組反向引用需要整段引用時用。有一個小組節(jié)需要特別注意如果你的替換文本里需要出現(xiàn)字面意義上的字符那就要寫成\進行轉(zhuǎn)義否則它會被sed展開成整個匹配內(nèi)容結(jié)果會完全出乎你的意料。3.3 實戰(zhàn)nginx訪問日志中的信息提取這里我用一個實際場景演示正則與sed結(jié)合的完整操作。假設(shè)現(xiàn)在要分析一份nginx訪問日志每一行格式大概長這樣127.0.0.1 - - [12/Oct/2023:08:10:22 0000] GET /api/users HTTP/1.1 200 1024需求是把所有請求中/api/后面的路徑部分提取出來去掉時間戳。直接看這個需求用單條sed命令就能完成sed -E s/^([0-9.]).*GET ([^ ]).*/\1 \2/這條命令分組的思路是第一組([0-9.])抓取IP地址第二組([^ ])抓取請求路徑由于[^ ]可以匹配到空格前的所有字符正好截取完整的URI。替換時把IP和路徑用空格連接輸出。實際場景中如果你還需要把狀態(tài)碼提取出來可以擴展成三組sed -E s/^([0-9.]).*GET ([^ ]).* [0-9] ([0-9])$/\1 \2 \3/執(zhí)行結(jié)果就是IP 路徑 狀態(tài)碼的緊湊格式。這種操作在分析日志時非常順手比打開Excel慢慢篩選要快得多。4. 實戰(zhàn)案例從配置批量修改到日志清洗紙上談兵沒有說服力。這里走一遍幾個真實場景從單文件到多文件從簡單替換到組合處理。4.1 批量替換配置文件里的IP與端口假設(shè)某個測試環(huán)境的服務(wù)配置分散在多個目錄的config文件中舊的數(shù)據(jù)庫IP是192.168.1.100端口是3306現(xiàn)在要換成新環(huán)境的192.168.1.200和3307。先在單個文件上驗證命令是否正確sed s/192\.168\.1\.100/192.168.1.200/g; s/:3306/:3307/g 文件名.conf注意這里有兩處細節(jié)第一正則中的.要寫成\.。因為在正則表達式里.是任意字符的通配符如果寫192.168.1.100而不轉(zhuǎn)義它會匹配192x168y1z100這種意外內(nèi)容。雖然數(shù)字環(huán)境里這種意外出現(xiàn)的概率低但養(yǎng)成正確轉(zhuǎn)義習慣很重要。第二端口替換時我在3306前面加了:這是刻意為之。避免把數(shù)據(jù)庫配置中可能出現(xiàn)的其他數(shù)字串里包含的3306也替換掉。加限定字符縮小匹配范圍是降低替換風險的有效手段。驗證輸出沒問題后執(zhí)行原地修改并備份sed -i.bak s/192\.168\.1\.100/192.168.1.200/g; s/:3306/:3307/g 文件名.conf注意分號的使用sed支持在同一條命令里用分號分隔多個操作表達式這比寫多條sed管道效率高可讀性也好。如果要對整個目錄下所有conf文件做同樣操作find . -name *.conf -exec sed -i.bak s/192\.168\.1\.100/192.168.1.200/g; s/:3306/:3307/g {} \;這里用find找到所有符合條件的文件再通過-exec對每個文件執(zhí)行一次sed。好處是規(guī)則清晰、可控性強比直接sed接多個文件名更安全而且find可以附帶更多過濾條件比如按時間只處理最近修改過的文件。4.2 日志清洗去掉噪音、統(tǒng)一格式日志文件往往格式混亂混合了時間戳、來源模塊、各種括號信息想快速提取關(guān)鍵內(nèi)容就得靠sed連番清洗。一個常見場景某服務(wù)日志每一行都帶方括號包裹的時間戳和線程名類似[2023-10-12 08:10:22] [main] INFO 用戶登錄成功 usernamejohndoe [2023-10-12 08:11:45] [worker-1] ERROR 連接超時 ip10.0.0.5現(xiàn)在只想保留日志級別、事件描述、關(guān)鍵參數(shù)去掉時間戳和線程名。一條命令搞定sed -E s/^\[[^]]\] \[[^]]\] ([A-Z]) (.*)$/\1 \2/執(zhí)行后INFO 用戶登錄成功 usernamejohndoe ERROR 連接超時 ip10.0.0.5這個模式中\(zhòng)[[^]]\]專門匹配方括號開頭 若干非右方括號字符 右方括號用它來跳過時間戳這段噪音。如果你需要進一步把冒號前后的參數(shù)拆成鍵值對格式可以疊加后續(xù)sed處理。注意-E在這里的必要性如果不用擴展正則[^]]這種寫法中的必須轉(zhuǎn)義命令變成sed s/^\[[^]]\\] \[[^]]\\] ...寫起來和心理負擔都會大很多。4.3 與管道和其他命令的組合用法sed最常見的位置其實是在命令管道中間。比如從一個命令的輸出中提取信息查看系統(tǒng)某進程及其啟動命令過濾出需要的字段ps aux | sed -n s/.*python3 \(.*\.py\).*/\1/p | sort -u這條命令從ps aux輸出里抓取所有python3進程啟動時攜帶的腳本文件名去重排序后展示。-n加上末尾的p標志讓sed只輸出匹配的行不打印其他進程信息。再比如配合grep先篩選再替換grep ERROR app.log | sed -E s/.*ip([0-9.]).*/\1/ | sort | uniq -c這段組合的含義是先從日志中篩出所有ERROR行然后從每行提取ip后面的IP地址最后統(tǒng)計每個IP出現(xiàn)的次數(shù)。一條管道下來就把哪些IP引發(fā)了最多錯誤給統(tǒng)計出來了沒有多余文件也沒有中間狀態(tài)。sed在這里扮演的就是提取器的角色。5. 常見問題與排查技巧實錄最后這部分是我的踩坑合集。有些問題遇到過兩三次才徹底想明白寫出來幫各位減少試錯時間。5.1 替換沒生效的五個原因原因一忘了加g標志。這是最常見的。以為替換了所有的結(jié)果每一行只換了第一個。判斷方法很簡單把輸出全部打印出來看一遍如果每行第二個舊值還在那就是這個原因。原因二正則匹配不到。尤其是想匹配行尾內(nèi)容時容易忘記$錨點。比如你想刪掉每行末尾的日期寫成sed s/2023-10-12//如果日期不在行首這個命令可能碰巧替換了別處的文本而不是行尾的。正確寫法應(yīng)該是sed s/2023-10-12$//。原因三特殊字符沒轉(zhuǎn)義。替換文本或者模式中的.、、/、\這些字符該轉(zhuǎn)義的沒轉(zhuǎn)義。排查方法推薦一個二分定位法先用最簡單的字符串比如只替換一個單詞做測試確認基礎(chǔ)命令沒有問題再逐步加入正則和特殊字符很快能找到哪一步出了問題。原因四引號混用導(dǎo)致變量沒展開。在shell腳本里sed s/舊/$變量/和sed s/舊/$變量/的結(jié)果可以是完全不同的。單引號內(nèi)的內(nèi)容原樣傳給sed$變量不會被shell解析雙引號內(nèi)的$變量會被shell展開成變量的值。想用外部變量做替換內(nèi)容必須用雙引號包住整個表達式并且內(nèi)部如果有其他字符需要轉(zhuǎn)義時要注意處理。做一個對比就更清楚了NAMEjohndoe sed s/用戶/$NAME/ log.txt # 結(jié)果輸出里的用戶被替換成了字面量 $NAME sed s/用戶/$NAME/ log.txt # 結(jié)果輸出里的用戶被替換成了 johndoe原因五編碼或不可見字符問題。文本里有\(zhòng)rWindows換行符殘留或者\t你看上去是空格或者行尾但正則匹配不上。排查方法用cat -A 文件查看隱藏字符或者用sed -n l 文件讓sed把不可見字符以轉(zhuǎn)義形式顯示出來。5.2 macOS與Linux的差異同一套sed命令在兩套系統(tǒng)上行為可能不同。最典型的差異是-i參數(shù)Linux上-i直接接命令macOS上-i必須接備份后綴參數(shù)哪怕為空即sed -i ...。另一個差異是部分正則元字符的支持度BSD sed對\t這類轉(zhuǎn)義的支持不如GNU sed穩(wěn)定。如果你寫的腳本需要在兩個平臺上跑兩條建議一是用-E而不是-r開啟擴展正則二是先小范圍驗證再全量執(zhí)行。5.3 我的幾個使用習慣文章最后分享幾個實踐經(jīng)驗基本上是花了不少冤枉時間才養(yǎng)成的習慣。第一重要操作必做備份。要么配合版本控制要么用-i.bak要么先cp 文件 文件.bak。成本很低但能救命。第二能一步到位就不要繞彎。一條sed命令能解決的事不要拆成五條管道。不是追求炫技而是減少中間環(huán)節(jié)出錯的概率也讓之后的維護者很可能就是兩個月后的自己更容易理解。第三善用s///g;t這種循環(huán)替換技巧。sed默認替換不重疊匹配如果文本里有aaaa你想把aa替換成a一次sed s/aa/a/g的結(jié)果是aa而不是a。想要徹底替換到無重疊為止可以寫成sed :a; s/aa/a/g; ta這是利用t標簽實現(xiàn)循環(huán)跳轉(zhuǎn)。這種技巧平時用得不多但真遇到時極其有用。第四也是最重要的一條習慣先打印后修改。任何一條sed命令都先不加-i跑一次看著輸出確認是預(yù)期效果再加-i真正執(zhí)行。這個習慣幫我避過了無數(shù)次災(zāi)難性的批量替換。sed命令的學習曲線不算陡峭基礎(chǔ)的替換功能半天就能上手但想要用得行云流水還是得在實戰(zhàn)中不斷積累。碰到文本處理的活兒多想一想這條能不能用sed來切一刀用著用著就順手了。畢竟對天天泡在終端里的人少浪費幾分鐘就能多喝一口咖啡。