前言:
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,
此時,NF由0變1,
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
這裏涉及到了FS跟OFS同時變更時,要注意分析,區別應用:
例子三:替換原來的分隔符(假如爲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.c的yylex+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,有的做爲設定進行調整。
沒必要再深究下去了,知道基本的作用就行了。