sed 小魔法 獲取文件名 去掉後綴 將短橫線替換爲空格 精準捕捉 陷阱 附錄 挑戰

在寫 Markdown 預覽腳本時 [1],遇到了一個小問題,如何將一段文本里的短橫線替換成空格。例如,將「寫一個-Markdown-預覽腳本」轉化爲「寫一個 Markdown 預覽腳本」。

文本里之所以會出現短橫線,是因爲我用這樣的文本當文件名,用短橫線代替空格。之所以如此,是因爲在命令行 [2] 裏,文件名如果有空格,會給很多讀取文件的程序帶來麻煩。那麼,爲什麼要將文件名裏用來代替的短橫線再變成空格呢?因爲我的 Markdown 預覽腳本希望用文件名作爲文章的標題名。

我要用 sed 解決這個問題,因爲這是學習或複習 sed 的一個好機會。

獲取文件名

圖難於其易,爲大於其細。一步一步來,先寫一個簡單的 Bash 腳本 [3],名曰 bird:

#!/bin/bash
echo $1

將其放入系統 PATH 變量裏設定的某個目錄內,然後給它可執行權限:

$ chmod +x bird

這個腳本能幹什麼呢?如果我給它一份文件,它能說出文件的名字。例如:

$ bird 寫一份-Markdown-預覽腳本.md

這個腳本的輸出:

寫一份-Markdown-預覽腳本.md

去掉後綴

bird 輸出的文件名裏,後綴 .md,我希望去掉。這樣的事,在 Bash 腳本里,可以有……不知道多少種做法。例如,其中一種是

#!/bin/bash
echo ${1%.md}

若文件名的後綴形式不確定,則更爲通用的去除方法是

#!/bin/bash
echo ${1%.*}

上述方法是使用了 Bash 內置的文本處理功能。sed 也能勝任這一工作……不僅是也能,應該是擅長,但是需要用管道與 echo 命令進行銜接:

#!/bin/bash
echo $1 | sed -e "s/\..*$//g"

執行

$ bird 寫一個-Markdown-預覽腳本.md

輸出

寫一個-Markdown-預覽腳本

發生了什麼?echo $1 的輸出結果通過管道 | 傳給了 sed。sed 本質上是一個文本編輯器,它按照用戶提供的命令對文本進行處理,它拿到 echo $1 的輸出後,根據正則表達式 \..+$ 從其中找出自己感興趣的內容,然後將其替換爲空文本。

s/\..+$//g 就是我給 sed 下的命令。s 的意思是 substitution,替換。\..+$ 是正則表達式,它可以從 sed 要處理的文本中抓住可與之匹配的內容。g 的意思是,global,全局。還有一個空字符,從 sed 命令裏看不到它……否則它就不是空字符了。如果將命令修改爲

s/\..+$/空字符/g

就可以看到它了,但是上述的 bird 的輸出結果會變成

寫一個-Markdown-預覽腳本空字符

sed 的文本替換命令裏的 / 符號是間隔符,用於隔離替換命令的各個元素。該命令的一般形式是

s/目標文本/替換文本/g

目標文本可以是普通的文本,也可以是正則表達式。

正則表達式 \..+$ 是什麼意思呢?首先,\. 是沒有特殊意義的 .\ 是 sed 的轉義符。\. 可以匹配文本中的 . 符號。第 2 個和第 3 個 .,是任意一個字符的意思。+ 是重複的意思,即它的前一個字符重複 0 次或多次。$ 是文本末尾的意思。綜合起來,\...*$ 的意思是,文本末尾以 . 爲開頭的任何片段。我要去掉的文件名的後綴,正是這樣的片段。

如果開啓 sed 對擴展的正則表達式的支持,上述正則表達式中的 ..* 可替換爲 .++ 的意思是,它前面的字符重複 1 次或多次。如果使用擴展的正則表達式,bird 腳本需改爲

#!/bin/bash
echo $1 | sed -E -e "s/\..+$//g"

將短橫線替換爲空格

將上一節最終的 bird 腳本改爲

#!/bin/bash
echo $1 | sed -E -e "s/\..+$//g; s/-/ /g"

即可將文件名中的所有短橫替換爲空格。sed 支持多條文本編輯命令,只需用 ; 將其隔開。

試驗一下:

$ bird 寫一份-Markdown-預覽腳本.md

結果爲

寫一個 Markdown 預覽腳本

看上去,我一開始提出的問題已經得到了很好的解決。

真的是這樣嗎?

當然不是。有時候,文件名裏有一些詞彙含有短橫,它們的作用是連字符號,不應該替換爲空格。例如,gtk-doc-教程.md,其中 gtk-doc 的短橫就是連字符。這個文件名會被 bird 腳本處理成

gtk doc 教程

這樣就不正確了。我想得到的是

gtk-doc 教程

精準捕捉

我認真想了一下,我要替換成空格的短橫線,通常位於漢字和英文字符之間。這種模式,用正則表達式可表示爲 [^a-zA-Z]-[a-zA-Z],其中 [a-zA-Z] 表示任意一個西文字符,[^a-zA-Z][a-zA-Z] 的反義。雖然 [^a-zA-Z] 波及的字符不僅僅是漢字,但是我也不懂漢字和西文字符之外的文字了。

基於上述正則表達式,我很快將 bird 改寫爲

#!/bin/bash
echo $1 | sed -E -e "s/\..+$//g;
                     s/[^a-zA-Z]-[a-zA-Z]/ /g;
                     s/[a-zA-Z]-[^a-zA-Z]/ /g"

然後執行

$ bird 寫一個-Markdown-預覽腳本.md

結果得到

寫一 arkdow 覽腳本

成功了一小半。個-Mn-預 被替換成了空格,因爲上述的正則表達式給抓住的是它們,所以被替換成空格的也是它們。

陷阱

有沒有辦法從正則表達式匹配到的文本里抓取一個片段,從而可以在替換的文本將它們保留下來?

有。正則表達式裏有一個概念……我也不知道這個概念到底叫什麼,我覺得它像個陷阱,可以讓正則表達式匹配到的文本的一部分掉進去,然後等着我把它們撈上來。

下面,我先在 bird 腳本里設一個陷阱,看看效果如何。將 bird 腳本改爲

#!/bin/bash
echo $1 | sed -E -e "s/\..+$//g;
                     s/([^a-zA-Z])-[a-zA-Z]/\1 /g;
                     s/[a-zA-Z]-[^a-zA-Z]/ /g"

執行

$ bird 寫一個-Markdown-預覽腳本.md

結果爲

寫一個 arkdow 覽腳本

上一節最後所寫的 bird 腳本在執行上述命令時,丟失的「」,現在被陷阱 ([^a-zA-Z]) 捕捉到了,然後,我在替換文本里使用 \1 又把它撈了出來。

同理,繼續設陷阱,然後打撈:

#!/bin/bash
echo $1 | sed -E -e "s/\..+$//g;
                     s/([^a-zA-Z])-([a-zA-Z])/\1 \2/g;
                     s/([a-zA-Z])-([^a-zA-Z])/\1 \2/g"

下面,測試這個最新版本的 bird,

$ bird 這是-gtk-doc-教程.md

結果爲

這是 gtk-doc 教程

現在,這個 bird 腳本是不是已經很好地解決了本文開始時提出的問題呢?

是的。但是,我並不能確定以後會不會遇到更加難對付的短橫。

附錄

如果不開啓 sed 對擴展的正則表達式的支持,只使用 sed 的基本正則表達式,bird 的寫法會有一些變化:

#!/bin/bash
echo $1 | sed -e "s/\...*$//g;
                  s/\([^a-zA-Z]\)-\([a-zA-Z]\)/\1 \2/g;
                  s/\([a-zA-Z]\)-\([^a-zA-Z]\)/\1 \2/g"

挑戰

bird 腳本里的正則表達式還能寫得更簡單一些,試試看吧。


  1. 詳見拙文「寫一個 Markdown 預覽腳本」。

  2. 確切地說,是 Shell,我用的是 Bash Shell。

  3. 我曾經寫過一篇很簡單的 Bash 入門教程,見:https://liyanrui.github.io/posts/bash.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章