Awk裏的域賦值操作和部分源碼解析($1=$1,$0=$0,FS,OFS)

 

前言:

cu上出了個shell題:

http://bbs.chinaunix.net/thread-2319120-1-1.html

第八題:GNU awk的$1=$1到底有什麼作用?$0=$0呢?

 

這題問得相當的細緻。可能很多人已經常用這二個賦值語句,卻半知半解。以下分二部分對這個題目進行分析

建議沒心情,沒耐心,沒興趣的人,只需要瞭解下第一部分,看第二部分就表看了,很羅嗦的。

 

第一部分:能過man上邊的解析,回籤這二個賦值語句的功能

 

第二部分:awk部分源碼解析(結點樹簡介,及域模塊)

          通過分析awk 域模塊源碼 ,瞭解awk的內部處理機制

 

參考程序及源碼版本:gawk-3.1.5

 

=========================第一部分================================================

 

第一部分:GNU awk的$1=$1到底有什麼作用?$0=$0呢?

 

 

首先了解一下一些知識,先翻一下man awk裏的這段話:

 assigning to a    non-existent field (e.g., $(NF+2) = 5) increases the value of NF,

對不存在的域賦值,會增加NF

creates any intervening fields with the  null  string  as  their  value, 

中間域默認爲NULL字符串

and  causes the value of $0 to be recomputed, with the fields being separated by the value of OFS.

$0會被根據OFS值重新計算,

References to negative numbered fields cause a fatal error. 

引用負的域索引是無效的,並且會導致錯誤

Decrementing NF causes  the  values of  fields past the new value to be lost,

減少NF值時,索引大於NF的域將會丟失

and the value of $0 to be recomputed, with the fields being separated  by the value of OFS.

同時$0也會被根據OFS值重新計算,

Assigning a value to an existing field causes the whole record to be rebuilt when $0 is referenced.

對當前存在的域值進行賦值,會使記錄在$0被引用時重構

Similarly,assigning a value to $0 causes the record to be resplit, creating new values for the fields.

$0賦值,也會使記錄重新重分割,對域重新賦值

 

 

這裏涉及到二種記錄操作

1:記錄分割,記錄按FS值分割成域,賦給每個域值

2:記錄重構,根據域值和OFS重構域。重構之後: $0=$1OFS$2OFS……

 

另外補充一點額外的知識:

$0$1,$2….是分開存放的,$0保存的是記錄初始的整串值(包括剛讀進來時候的分隔符),而$1,2...保存的是分割後的域值,

awk內部函數機制基本上保證了二者的同步,一般不會出現什麼錯誤。

 

儘管如此,分開存儲的記錄和域還是不可必免會存在一些應用問題,下邊舉二個例子說明:

 

例子一:當我們想通過更改OFS,變更域之間的輸出分隔符時,此時$0由於保存的是原來的串,輸出並不是按$1OFS$2進行輸出:

 

此時的辦法採用可以列舉的辦法:

 

但是當域值很多時(或不固定時),再用列舉的話可能要借用NF,寫一個循環

 

例子二:當我們想通過更改FS,變更域之間的分隔符時,此時$0由於保存的是原來的串,$1並沒有重新分隔

 

 

 

 

 

 

 

$1=$1的解析:

命令分解:

1:賦值操作的順序是從右到左,先通過右操作數引用了$1,取得當前$1的值,

2:再通過賦值操作將值賦給左邊的$1

 

步驟解析:

步驟1沒什麼特別的,若當前NF=0,這裏的$1只會得到空值(NULL string)

步驟2分二種情況

1:若當前NF=0,即上邊引文中提到的 assigning to a    non-existent field,

        此時,NF01

 2:若NF>1,則NF不變

接下來是關健做用:由於步驟2觸發了域賦值操作,產生了連帶的動作:在下一次引用$0時,會對$0進行重新構。(根據OFS

 

歸納作用:

下次引用$0時,對$0進行按引用時的OFS進行重構($1OFS$2OFS……

 

應用:

例子一:變更輸出分隔符,可以這樣:

 

 

 

再來是$0=$0的解析

命令分解:

1:賦值操作的順序是從右到左,先通過右操作數引用了$0,取得當前$0的值

2:再通過賦值操作將值賦給左邊的$0

 

步驟分析:

步驟1:這裏引用了$0進行取值,由於上邊提到的設定,要注意是否有重構問題

步驟2:這裏是賦值引用,對$0的賦值引用,會涉及到根據當前的FS對域的重分割

 

歸納作用:根據右操作數$0取出的值(可能是重構的),賦給$0,並根據FS重新分隔

 

應用:

例子二:變更輸入分隔符,對當前記錄進行重解析:

 

 

 

綜合應用$1=$1,$0=$0

這裏涉及到了FSOFS同時變更時,要注意分析,區別應用:

例子三:替換原來的分隔符(假如爲A)爲新的串(B),最後按分隔符(C)輸出第三域

 

這裏如果少了$1=$1的話,則$0在引用時無法根據新的OFS值重構,沒有做替換,仍按原先的串按新FS解析,得到原先的第三域值

 

 

這裏如果少了$0=$0的話,$0以原先的值"1CA2CA3CA"在讀入時按"A"分隔$3不變

 

這個結果與下邊等同:

 

 

 

=====================================================================================================

二:awk部分源碼解析(結點樹簡介,及域模塊)

 

man還是有些不夠清晰的,或多或少會留下點疑問,應該會有人考慮到$0在分割之後存放成各個域,既然可以通過各個域拼接起來,爲什麼還要保存一份原先的串的樣本?

比如怎麼證明$0是獨立於$1,$2,$3的存儲存在?這樣做又出於什麼考慮?

再比如爲什麼在$1=$1後不是立即重構$0,而是在引用的時候才重構。

再比如對$0進行賦值後,是直接觸發分割的,還是在引用域前才進行分隔。

 

沒什麼什麼比查源碼更能回答這些問題的了

 

通過查看awk的源碼中的awk.h頭文件可以瞭解到awk的源碼的核心數據結構:結點(typedef struct  NODE)和結點樹,awk代碼裏的各種元素都是以樹和結點這種結構存在的,變量如FS/$0/$0是結點,操作符如“=賦值符等”也是結點,內置函數(builtin)也是結點,哈希數組也是結點。

awk是以節點(node 結構)樹的形式保存各種變量和操作的,比如各變量,各{}操作塊都是樹的節點。awk通過調用awkgram.cyylex+yyparse二個函數,解析awk程序,並形成各種樹,比較典型的,如主體程序有三顆數:

 

 

程序塊在解析形成樹之後,由函數 執行Int interpret(register NODE *volatile tree)

begin模塊跟end模塊都執行一次

 

而中間模塊:expression_value 是在do_input裏,一次讀一一條記錄執行一次的

 

 

然後回到正題:域模塊源碼:field.c,通過分析模塊的數據結構和函數來了解模塊設計,包括分隔符設計,記錄分割,記錄設置,域設置等

 

首先是數據結構:

 

fields_arr數組保存了所有域節點,$1,2...分別對應一樣的下標fields_arr[1,2…]

C的數組索引是從0開始的,這裏也不例外,

fields_arr[0]是用於保存完整記錄的節點,保存記錄值,即$0

 

 

 

然後是函數,我們關注函數的功能和調用。

 

NF相關函數:

 

NF(域數值)賦值函數: 

      直接更改NF值,域數組隨NF長短做伸縮

       最後一句話:field0_valid = FALSE;涉及到記錄重構,見rebuild_record()

 

記錄層面的函數:賦值,重構,域分割等

 

記錄重置函數: 

       重置記錄,清除fields_arr,NF= -1,視情況保存當前FS

       當對$0賦值時即是調用此函數

 

記錄賦值函數: 

          此函數調用:reset_record,重置數組,將buf數組裏的內容保存到fields_arr[0] 裏邊去

          注意:此時並未進行分割。註釋裏是這麼寫的:

 * setup $0, but defer parsing rest of line until reference is made to $(>0)

 * or to NF.  At that point, parse only as much as necessary.

雖然保存的記錄值,但沒有做域解析,直到域或NF引用才做解析

 

記錄重構函數: 

        這個函數沒有參數,這個函數的功能是把當前各域fields_arr[1,2….]數組拼接OFS,形成新的字符串更新到fields_arr[0]

        並且這個函數只在get_field函數裏,只有在field0_valid標誌爲fault時前提下,做$0引用時,才調用

 

記錄分割函數: 

        根據設定的規則,解析域,包括定長解析,正則解析,字符串解析等。

       (這麼多的規則,是爲了滿足功能和效率需求。)

 

 

域層面的函數: 

 

域賦值函數: 

         通過對域節點操作fields_arr[num],簡單的賦域值,這函數由get_field調用

 

 

最關健的函數:

域引用函數: 

         requested是fields_arr的下標數值,下標爲零是記錄,$0,下標大於零是域

         這邊稱爲引用函數,因爲域引用,有可能是出現在賦值運算符的左邊(LHS)或右邊(LHS

          因爲在詞法解析過程中,$n是出現在賦值語句"="號左邊還是右邊都是一樣的標記,並沒有什麼不同,

          到語法解析的時候才能明白並且由assign指定,assign爲空則是取值引用,非空則是賦值引用

 

          這個函數分爲以下四種情況:

           1$0取值引用,requested=0,assign = NULL

若 field0_valid = FALSE則調用 rebuild_record(重構fields_arr[0])

返回fields_arr[0]

           2$n(n>=1)取值引用,requested!=0,assign = NULL

若未進行域解析,則調用域解析函數,

返回fields_arr[n]

           3$0賦值引用,requested=0,assign != NULL

若 field0_valid = FALSE,同樣調用 rebuild_record重構記錄,

記錄賦值是在 set_record裏完成的,返回fields_arr[0]

           4$n(n>=1)賦值引用:requested!=0,assign = NULL

設置 field0_valid = FALSE;

域賦值在set_field裏完成的,返回fields_arr[n]

 

 

 

 

 

結論:

1:記錄值($0)與各域值($1,2,….)是分離存放不同node結點裏,前者存放在fields_arr[0],後者存放在fields_arr[1,2...]

2:域賦值引用會引起記錄重構和分割(不是同步的)

3:域取值引用會根據情況判斷是否進行記錄重構,或重新分割。

 

 

再看awk的一些更新記錄關於get_field函數的說明:

 

 

其實這部分機制也就是awk程序當前版本的一種設定。在版本變更中,有的做爲bug,有的做爲設定進行調整。

沒必要再深究下去了,知道基本的作用就行了。

 

 

 

 

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