在寫 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 覽腳本
成功了一小半。個-M
和 n-預
被替換成了空格,因爲上述的正則表達式給抓住的是它們,所以被替換成空格的也是它們。
陷阱
有沒有辦法從正則表達式匹配到的文本里抓取一個片段,從而可以在替換的文本將它們保留下來?
有。正則表達式裏有一個概念……我也不知道這個概念到底叫什麼,我覺得它像個陷阱,可以讓正則表達式匹配到的文本的一部分掉進去,然後等着我把它們撈上來。
下面,我先在 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 腳本里的正則表達式還能寫得更簡單一些,試試看吧。
-
詳見拙文「寫一個 Markdown 預覽腳本」。 ↩
-
確切地說,是 Shell,我用的是 Bash Shell。 ↩
-
我曾經寫過一篇很簡單的 Bash 入門教程,見:https://liyanrui.github.io/posts/bash.html ↩