AWK 的現況
AWK 是一套歷史超過 40 年的文字處理語言。它擁有 POSIX 標準、數個符合標準的實作,到了 2020 年依然令人意外地保有高度的關聯性——無論是簡單的文字處理任務,還是「大數據」的整理工作皆然。最近 發布的 GNU Awk 5.1,正好提供了一個機會來回顧 AWK 的整體現況,看看 GNU Awk 近來做了些什麼,以及 AWK 如今在哪些地方被使用。
這個語言於 1977 年在貝爾實驗室誕生。其名稱取自三位最初作者姓名的首字母:Alfred Aho、Peter Weinberger 與 Brian Kernighan。AWK 本質上就是一個 Unix 工具,設計理念是把一件事做好:篩選與轉換文字行。它常用來解析日誌檔中的欄位、轉換其他工具的輸出,以及統計單字與欄位的出現次數。Aho 曾簡潔地總結 AWK 的功能:
AWK 一次讀取一行輸入。程式中的每個模式都會掃描該行,每當有模式吻合,就會執行對應的動作。
AWK 程式常常是直接在命令列執行的一行指令。舉例來說,若要從某個假想的網頁伺服器日誌計算 GET 請求的平均回應時間,你可能會輸入:
$ awk '/GET/ { total += $6; n++ } END { print total/n }' server.log
0.0186667意思是:對於所有符合正規表示式 /GET/ 的行,將回應時間(第六個欄位,即 $6)累加並計算行數;最後印出回應時間的算術平均數。
多種 AWK 版本
目前常用的 AWK 主要有三個版本,而且全都(至少在絕大多數使用情境下,足夠貼近地)符合 POSIX 標準。第一個是經典的 awk,也就是 Aho、Weinberger 與 Kernighan 在其著作The AWK Programming Language中所描述的 AWK 版本。它有時被稱為「new AWK」(nawk)或「one true AWK」,現在託管在 GitHub 上。這是在許多 BSD 系統上預先安裝的版本,包含 macOS(不過 macOS 內建的版本已經過時,值得升級)。
第二個是GNU Awk(gawk),是目前功能最豐富、也最積極維護的版本。Gawk 通常預裝在 Linux 系統上,而且經常就是預設的 awk。在 macOS 上可以透過 Homebrew輕鬆安裝,也有提供Windows 執行檔。Arnold Robbins 自 1994 年起就是 gawk 的主要維護者,至今仍持續引領這個語言的發展(他也為經典的 awk 版本貢獻了許多修正)。Gawk 擁有許多功能是 awk 或 POSIX 標準所沒有的,包含新函式、網路功能、C 擴充 API、剖析器與除錯器,以及最近新增的命名空間。
第三個常見的版本是mawk,由 Michael Brennan 撰寫。它是 Ubuntu 與 Debian Linux 上預設的 awk,至今仍是速度最快的 AWK 版本,具備 bytecode 編譯器與更節省記憶體的數值表示方式。(Gawk 自 4.0 起也有了 bytecode 編譯器,因此現在在速度上已相當接近 mawk。)
如果你只是想用 AWK 來寫一行指令或做基本的文字處理,上述任何一種版本都很適合。如果你考慮用它來撰寫較大的腳本或程式,Gawk 的功能會是更明智的選擇。
此外還有其他幾種成熟度與維護程度不一的 AWK 實作,特別是針對嵌入式 Linux 環境、經過體積最佳化的BusyBox 版本、可在執行時期存取 Java 語言功能的Java 重寫版,以及我自己用 Go 撰寫、符合 POSIX 標準的GoAWK。三個主要的 AWK 與 BusyBox 版本都是以 C 語言寫成。
Gawk 自 4.0 以來的變化
距離 LWN 報導 gawk 4.0 發布已經將近 10 年。或許會讓人想說「自 2011 年以來改變了很多」,但事實是在 AWK 的世界裡,變化相對緩慢。本文將在此介紹自 4.0 以來值得注意的功能,若想了解更多細節,可以閱讀完整的 4.x 與 5.x 更新日誌。Gawk 5.1.0 就在一個多月前的 4 月 14 日發布。
最主要的面向使用者的新功能,是在 5.0 中引入的命名空間。多數現代語言都有某種命名空間的概念,讓大型專案與函式庫在發布時更不容易發生名稱衝突。Gawk 5.0 以向下相容的方式加入命名空間,讓開發者可以建立函式庫,例如這個簡單的數學函式庫:
# area.awk
@namespace "area"
BEGIN {
pi = 3.14159 # namespaced "constant"
}
function circle(radius) {
return pi*radius*radius
}若要參照函式庫中的變數或函式,請使用 namespace::name 語法,類似於 C++:
$ gawk -f area.awk -e 'BEGIN { print area::pi, area::circle(10) }'
3.14159 314.159Robbins 認為,AWK 缺乏命名空間是它未能成為大規模程式語言的關鍵原因之一,而 gawk 5.0 的這項功能或許能改善這個問題。Robbins 認為阻礙 AWK 發展的另一個主要問題是缺乏良好的 C 擴充介面。Gawk 的動態擴充介面在 4.1 中被徹底翻新;現在它擁有一套明確的 API,能將現有的 C 與 C++ 函式庫包裝起來,讓 AWK 輕鬆呼叫。
以下程式碼片段取自使用手冊中範例的 C 語言包裝程式,它會以檔名以及來自 stat() 系統呼叫的數值來填入一個 AWK 陣列(以字串為鍵的雜湊表):
/* empty out the array */
clear_array(array);
/* fill in the array */
array_set(array, "name", make_const_string(name, strlen(name), &tmp));
array_set_numeric(array, "dev", sbuf->st_dev);
array_set_numeric(array, "ino", sbuf->st_ino);
array_set_numeric(array, "mode", sbuf->st_mode);另一項在 4.2 版本中(並延續至 5.0)的變更是徹底改寫的原始碼美化排版器。Gawk 的美化排版器讓它能作為一套標準化的 AWK 程式碼格式化工具,類似 Go 的go fmt工具與 Python 的Black格式化工具。舉例來說,若要美化排版上面提到的 area.awk 檔案:
$ gawk --pretty-print -f area.awk
會得到以下輸出:
@namespace "area"
BEGIN {
pi = 3.14159 # namespaced "constant"
}
function circle(radius)
{
return (pi * radius * radius)
}你可能會質疑這個工具的選擇:為什麼「BEGIN {」在大括號前沒有換行,而 function 卻有?(事實上是 AWK 語法不允許那樣做。)為什麼函式前要空兩行,又為什麼要在 return 運算式外加上括號?但至少它是一致的,或許有助於避免程式碼風格的爭論。
Gawk 允許有限度的執行時期型別檢查,並在 4.2 中新增 typeof() 函式進一步擴充了這項能力。typeof() 會依輸入型別回傳像是「string」、「number」或「array」的字串常數。這些函式對於會遞迴走訪巢狀陣列中每個項目的程式碼特別重要(這是 POSIX AWK 做不到的事)。
在 4.2 中,gawk 也開始支援將正規表示式常數作為第一級資料型別,語法為 @/foo/。過去你無法將正規表示式常數存入變數;typeof(@/foo/) 會回傳字串「regexp」。在效能方面,gawk 4.2 在 Linux 系統上透過在可用時使用fwrite_unlocked()帶來了顯著的提升。由於 gawk 是單執行緒的,它可以使用非鎖定的 stdio 函式,讓原始輸出速度提升 7 至 18%——例如在大型檔案上執行 gawk '{ print }'。
GNU Awk 使用手冊向來是一份詳盡的參考文件,但在 4.1 以及後續的 5.x 版本中又經過大幅更新,加入了新的範例、摘要章節與習題,並進行了大規模的文字編修。
最後(也是最微不足道的),我在 4.0 中發現一個有趣的細微變動,那就是在 sub() 與 gsub() 中對反斜線處理方式的還原。Robbins 寫道:
在 sub() 與 gsub() 中對反斜線的預設處理方式已還原為 3.1 版的行為。即使是為了符合標準,我當初竟以為可以那樣破壞相容性,實在是愚蠢的想法。
sub 與 gsub 函式是核心的正規表示式替換函式,即使是對反斜線複雜處理方式的一個小「修正」,也會讓許多人的程式碼壞掉:
當 4.0.0 版發布時,gawk 維護者將 POSIX 規則設為預設值,破壞了超過十年的向下相容性。不用說,這是個糟糕的主意,而自 4.0.1 版起,gawk 恢復了原本的歷史行為,只有在指定 --posix 時才會遵循 POSIX 規則。
Robbins 在最初的改動中或許有小小的判斷失誤,但顯然他非常重視向下相容性。尤其對於像 gawk 這樣廣受歡迎的工具來說,有時候與其改變長久以來的運作方式,不如持續違反規格還比較好。
AWK 至今仍有其重要性嗎?
問 AWK 是否仍具重要性,有點像問空氣是否仍重要:你或許看不見它,但它無所不在。許多 Linux 管理者與 DevOps 工程師會用它來轉換資料或透過日誌檔診斷問題。幾乎所有以 Unix 為基礎的機器上都安裝了某個版本的 AWK。除了臨時性的使用之外,許多大型開源專案也在其建置或文件工具中的某處使用了 AWK。僅舉幾個例子:Linux 核心在 x86 工具中用它來檢查與重新格式化 objdump 檔案,Neovim 用它來產生文件,而 FFmpeg 則將它用於建置與測試。
AWK 的建置腳本出乎意料地難以被淘汰,即使人們想淘汰它:2018 年 LWN 報導 GCC 的貢獻者想在產生選項解析程式碼的腳本中以 Python 取代 AWK。當時這項提案獲得了一些支持,但顯然沒有人自願實際進行移植,AWK 腳本至今依然存在。
Robbins 在他2018 年的論文中主張將 AWK(特別是 gawk)作為一種「系統程式語言」來使用,在此脈絡下是指用來撰寫較大型工具與程式的語言。他闡述了自己認為 AWK 未能普及的原因,但 Kernighan 對於「缺乏擴充機制是 AWK 未被廣泛用於大型程式的主因」這個說法「並非百分之百信服」。他認為原因可能是缺乏對系統呼叫等功能的內建支援。不過,這些都沒有阻止一些人打造出較大型的工具:Robbins 自己的文學程式設計工具TexiWeb Jr.(1300 行 AWK)、Werner Stoop 從原始碼中 Markdown 註解產生文件的d.awk工具(800 行),以及Translate Shell,一套達 6000 行、為雲端翻譯 API 提供相當強大命令列介面的 AWK 工具。
近幾年有數位開發者撰文描述在他們的「大數據」工具組中使用 AWK,將其視為比 Spark 與 Hadoop 這類笨重的分散式運算系統更簡單(有時甚至更快)的工具。Nick Strayer 撰文描述如何使用 AWK 與 R 在多核心上解析 25 TB 的資料。其他大數據的例子還包括 Adam Drake 那篇標題誘人的文章「Command-line Tools can be 235x Faster than your Hadoop Cluster」,以及 Brendan O'Connor 的「Don’t MAWK AWK – the fastest and most elegant big data munging language!」
無論是臨時性的文字處理、建置工具、「系統程式設計」,還是大數據處理——更別提文字模式的第一人稱射擊遊戲——AWK 在 2020 年似乎依然活得好好的。
[感謝 Arnold Robbins 審閱本文草稿。]
隨機一篇部落格
留言
登入後參與討論