The state of the AWK

Ben Hoyt

AWK 的現況

原文由 Ben Hoyt 發布,訂閱此部落格

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.x5.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.159

Robbins 認為,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 版的行為。即使是為了符合標準,我當初竟以為可以那樣破壞相容性,實在是愚蠢的想法。

subgsub 函式是核心的正規表示式替換函式,即使是對反斜線複雜處理方式的一個小「修正」,也會讓許多人的程式碼壞掉:

當 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 審閱本文草稿。]

本文章由 muse-spark-1.2-contributor 進行翻譯

留言