ctags使用詳解

2puT發表於2016-09-18

一.       ctags是幹什麼的

ctags的功能:掃描指定的原始檔,找出其中所包含的語法元素,並將找到的相關內容記錄下來。

我用的是Exuberant Ctags,在Windows上使用,就一個可執行檔案,非常綠色,可在sourceforge下載。

二.       ctags可以識別哪些語言,是如何識別的

ctags識別很多語言,可以用如下命令來檢視:

ctags --list-languages

還可以識別自定義語言,具體沒研究過。

   ctags是可以根據檔案的副檔名以及檔名的形式來確定該檔案中是何種語言,從而使用正確的分析器。可以使用如下命令來檢視預設哪些副檔名對應哪些語言:

ctags --list-maps

還可以指定ctags用特定語言的分析器來分析某種副檔名的檔案或者名字元合特定模式的檔案。例如如下命令告知ctags,以inl為副檔名的檔案是c++檔案。

ctags --langmap=c++:+.inl –R

   並不十分清楚ctags使用何種技術來解析內容,估計包括正規表示式、詞法分析、語法分析等等。但ctags不是編譯器也不是前處理器,它的解析能力是有限的。例如它雖然可以識別巨集定義,但對於使用了巨集的語句的識別還是有缺陷的,在一些稍微正規點的程式碼(例如ACE的庫或VC的標頭檔案等)中的某些常規的巨集使用方式會導致ctags無法識別,或者識別錯誤,從而使得ctags沒有記錄user想記錄的內容,或者記錄下的資訊不準確。另一方面ctags也有聰明的一面,例如在cpp檔案中掃描到static的全域性變數時,ctags會記錄這個變數,而且還會標明說這個變數是侷限於本檔案的,同樣的定義,如果放在h檔案中,ctags則不會標明說這個變數是侷限於本檔案的,因為ctags認為h檔案是標頭檔案的一種,會被其他檔案include,所以在其他檔案中可能會用到該h檔案裡定義的這個全域性變數。

三.       ctags可以識別和記錄哪些語法元素

可以用如下命令檢視ctags可以識別的語法元素:

ctags --list-kinds

或者單獨檢視可以識別的c++的語法元素

ctags --list-kinds=c++

   ctags識別很多元素,但未必全都記錄,例如“函式宣告”這一語法元素預設是不記錄的,可以控制ctags記錄的語法元素的種類。如下命令要求ctags記錄c++檔案中的函式宣告和各種外部和前向宣告:

ctags -R --c++-kinds=+px

四.       ctags是怎麼記錄的

不管一次掃描多少檔案,一條ctags命令把記錄的內容都記到一個檔案裡去,預設是當前目錄的tags檔案,當然這是可以更改的。

每個語法元素對應檔案裡的一行,學名叫tag entry

1)            開頭是tag的名字,其實也就是語法元素的名字,例如記錄的是函式的話則tag名就是函式名,記錄的是類的話,tag名就是類名。

2)            接下來是一個tab

3)            接下來是語法元素所在的檔名。

4)            又是一個tab

5)            一條“命令”。這個要解釋一下意義:ctags所記錄的內容的一個功能就是要幫助像vi這樣的編輯器快速定位到語法元素所在的檔案中去。前面已經記錄了語法元素所在的檔案,這條命令的功能就是一旦在vi中開啟語法元素所在的檔案,並且執行了該“命令”後,vi的游標就能定位到語法元素在檔案中的具體位置。所以該“命令”的內容一般分兩種,一種是一個正規表示式的搜尋命令,一種是第幾行的指向命令。預設讓ctags在記錄時自行選擇命令的種類,選擇的依據不詳,可以通過命令列引數來強制ctags使用某種命令,這裡就不多談了。

6)            對於本tag entry(簡稱tag)所對應的語法元素的描述,例如語法元素的型別等。具體內容和語法元素的種類密切相關。顯示哪些描述,顯示的格式等都是可以在命令列指定的。例如如下命令要求描述資訊中要包含:a表示如果語法元素的類的成員的話,要標明其access(即是public的還是private的);i表示如果有繼承,標明父類;K表示顯示語法元素的型別的全稱;S表示如果是函式,標明函式的signaturez表示在顯示語法元素的型別是使用kind:type的格式。

ctags -R --fields=+aiKSz

   ctags除了記錄上述的各種內容之外,還可以在tags檔案中記錄本次掃描的各個檔案,一個檔名對應一個tag entry。預設是不記錄的,要強制記錄要是使用如下命令:

ctags –R --extra=+f

   還可以強制要求ctags做這樣一件事情——如果某個語法元素是類的一個成員,當然ctags預設會給其記錄一個tag entry(說白了就是在tags檔案裡寫一行),可以要求ctags對同一個語法元素再記一行。舉一個例子來說明:假設語法元素是一個成員函式,ctags預設記錄的tag entry中的tag的名字就是該函式的名字(不包括類名作為字首),而我們強制要求ctags多記的那個tag entrytag的名字是包含了類明作為字首的函式的全路徑名。這樣做有什麼好處見下文分析。強制ctags給類的成員函式多記一行的命令為:

ctags -R --extra=+q

五.       vi大概是怎樣使用ctags生成的tags檔案的

估計vi是這樣使用tags檔案的:我們使用vi來定位某個tag時,vi根據我們輸入的tag的名字在tags檔案中一行行查詢,判斷每一行tag entrytag名字(即每行的開頭)是否和使用者給出的相同,如果相同就認為找到一條記錄,最後vi顯示所有找到的記錄,或者根據這些記錄直接跳轉到對應檔案的特定位置。

考慮到ctags記錄的內容和方式,出現同名的tag entry是很常見的現象,例如函式宣告和函式定義的tag名字是一樣的,過載函式的tag名字是一樣的等等。vi只是使用tag名字來搜尋,還沒智慧到可以根據函式的signature來選擇相應的tag entryvi只能簡單的顯示tag entry的內容給user,讓user自行選擇。

ctags在記錄成員函式時預設是把函式的名字(僅僅是函式的名字,不帶任何類名和namespace作為字首)作為tag的名字的,這樣就導致很多不同類但同名的函式所對應的tag entry的名字都是一樣的,這樣uservi中使用函式名來定位時就會出現暴多選擇,挑選起來十分麻煩。user可能會想在vi中用函式的全路徑名來進行定位,但這樣做會失敗,因為tags檔案中沒有對應名字的tag entry。要滿足使用者的這種心思,就要求ctags在記錄時針對類的成員多記錄一條tag entry,該tag entry和已有的tag entry的內容都相同,除了tag的名字不同,該tag entry的名字是類的成員的全路徑名(包括了名稱空間和類名)。這就解釋了ctags--extra=+q這樣一條命令列選項(見四)。

六.       我的一條ctags命令

ctags-R --languages=c++ --langmap=c++:+.inl -h +.inl --c++-kinds=+px--fields=+aiKSz --extra=+q --exclude=lex.yy.cc --exclude=copy_lex.yy.cc

命令太長了,折成兩行了,可以考慮把命令的各個引數寫到檔案裡去了(具體做法就不談了)。

1.

-R

表示掃描當前目錄及所有子目錄(遞迴向下)中的原始檔。並不是所有檔案ctags都會掃描,如果使用者沒有特別指明,則ctags根據檔案的副檔名來決定是否要掃描該檔案——如果ctags可以根據檔案的副檔名可以判斷出該檔案所使用的語言,則ctags會掃描該檔案。

2.

--languages=c++

只掃描檔案內容判定為c++的檔案——即ctags觀察副檔名,如果副檔名對應c++,則掃描該檔案。反之如果某個檔案叫aaa.pypython檔案),則該檔案不會被掃描。

3.

--langmap=c++:+.inl

告知ctags,以inl為副檔名的檔案是c++語言寫的,在加之上述2中的選項,即要求ctagsc++語法掃描以inl為副檔名的檔案。

4.

-h +.inl

告知ctags,把以inl為副檔名的檔案看作是標頭檔案的一種(inl檔案中放的是inline函式的定義,本來就是為了被include的)。這樣ctags在掃描inl檔案時,就算裡面有static的全域性變數,ctags在記錄時也不會標明說該變數是侷限於本檔案的(見第一節描述)。

5.

--c++-kinds=+px

記錄型別為函式宣告和前向宣告的語法元素(見第三節)。

6.

--fields=+aiKSz

控制記錄的內容(見第四節)。

7.

--extra=+q

ctags額外記錄一些東西(見第四、五節)。

8.

--exclude=lex.yy.cc --exclude=copy_lex.yy.cc

告知ctags不要掃描名字是這樣的檔案。還可以控制ctags不要掃描指定目錄,這裡就不細說了。

9.

-f tagfile:指定生成的標籤檔名,預設是tags. tagfile指定為 - 的話,輸出到標準輸出。


七.       本文內容來源

   Exuberant Ctags附帶的幫助文件(ctags.html)。


========================================補充===========================================

"ctags"是一個獨立的程式,絕大多數Unix系統上都會預裝這個程式。

    1、使用tags
   
    tag是什麼?一個位置。它記錄了關於一個識別符號在哪裡被定義的資訊,比如C或C++程式中的一個函式定義。這種tag聚集在一起被放入一個tags檔案。這個檔案可以讓Vim能夠從任何位置起跳達到tag所指示的位置-識別符號被定義的位置。

    下面的命令可以為當前目錄下的所有C程式檔案生成對應的tags檔案:
        (shell command) ctags *.c


    現在你在Vim中要跳到一個函式的定義(如startlist)就可以用下面的命令:
       (ex command) :tag startlist
這個命令會帶你到函式"startlist"的定義處,哪怕它是在另一個檔案中。

    CTRL+] 命令會取當前游標下的word作為tag的名字並直接跳轉。這使得在大量C程式中進行探索更容易一些。假設你正看函式"write block",發現它呼叫了一個叫"write line"的函式,這個函式是幹什麼的呢?你可以把游標置於"write_line"上,按下CTRL+] 即可。如果"write_line"函式又呼叫了 "write_ char".你當然又要知道這個函式又是什麼功能。同時,置游標於"write_char"上按下CTRL+]。現在你位於函式"write_char"的定義處。

    ":tags"命令會列出現在你就已經到過哪些tag了:
       (ex command):tags
       #      TO          tag       FROM line          in file/text
       1       1       write_line        8             write_block.c
       2       1       write_char        7             write_line.c

    現在往回走。CTRL+T命令會跳到你前一次的tag處。在上例中它會帶你到呼叫了"write_char"的"write_line"函式的地方。CTRL+T可以帶一個命令記數, 以此作為往回跳的次數, 你已經向前跳過了,現在正在往回跳,我們再往前跳一次。下面的命令可以直接跳轉到當前tag序列的最後:
       (ex command) :tag
你也可以給它一個前輟, 讓它向前跳指定的步長. 比如":3tag"。CTRL+T也可以帶一個前輟。這些命令可以讓你向下深入一個函式呼叫樹(使用CTRL+]), 也可以回溯跳轉(使用CTRL+T). 還可以隨時用":tags"看你當前的跳轉歷史記錄。

    2、分隔視窗

    ":tag"命令會在當前視窗中載入包含了目標函式定義的檔案。但假設你不僅要檢視新的函式定義,還要同時保留當前的上下文呢?你可以在":tag"後使用一個分隔視窗命令":split"。Vim還有一個一舉兩得的命令:
      (ex command) :stag tagname
要分隔當前視窗並跳轉到游標下的tag:
      (normal mode command) CTRL+W+]
如果同時還指定了一個命令記數, 它會被當作新開視窗的行高.

    3、多個tags檔案

    如果你的原始檔位於多個目錄下,你可以為每個目錄都建一個tags檔案。Vim會在使用某個目錄下的tags檔案進行跳轉時只在那個目錄下跳轉。

    要使用更多tags檔案,可以通過改變'tags'選項的設定來引入更多的tags檔案。如:
       (ex command) :set tags=./tags, ./../tags, ./*/tags
這樣的設定使Vim可以使用當前目錄下的tags檔案,上一級目錄下的tags檔案,以及當前目錄下所有層級的子目錄下的tags檔案。這樣可能會引入很多的tags檔案,但還有可能不敷其用。比如說你正在編輯"~/proj/src"下的一個檔案,但又想使用"~/proj/sub/tags"作為 tags檔案。對這種Vim情況提供了一種深度搜尋目錄的形式。如下:(ex command) :set tags=~/proj/**/tags

    4、單個tags檔案

    Vim在搜尋眾多的tags檔案時,你可能會聽到你的硬碟在咔嗒咔嗒拼命地叫。顯然這會降低速度。如果這樣還不如花點時間生成一個大一點的tags檔案。這需要一個功能豐富的ctags程式,比如上面提到的那個。它有一個引數可以搜尋整個目錄樹:
       (shell command)cd ~/proj
        ctags -R
    用一個功能更強的ctags的好處是它能處理多種型別的檔案。不光是C和C++源程式,也能對付Eiffel或者是Vim指令碼。你可以參考ctags程式的檔案調整自己的需要。現在你只要告訴Vim你那一個tags檔案在哪就行了:
        (ex command) :set tags=~/proj/tags

    5、同名tag

    當一個函式被多次過載(或者幾個類裡都定義了一些同名的函式),":tag"命令會跳轉到第一個符合條件的。如果當前檔案中就有一個匹配的,那又會優先使用它。當然還得有辦法跳轉到其它符合條件的tag去:
      (ex command) :tnext
重複使用這個命令可以發現其餘的同名tag。如果實在太多,還可以用下面的命令從中直接選取一個:
      (ex command) :tselect tagname
Vim會提供給你一個選擇列表,例如:(Display)

#     pri     kind     tag               file
1      F        f      mch_init     os_amiga.c
                        mch_init()
2      F        f      mch_init     os_mac.c
                        mch_init()
3      F        f      mch_init     os_msdos.c
                        mch_init(void)
4      F        f      mch_init     os_riscos.c
                        mch_init()
Enter nr of choice (<CR> to abort):

   現在你只需鍵入相應的數字(位於第一欄的)。 其它欄中的資訊是為了幫你作出決策的。在多個匹配的tag之間移動,可以使用下面這些命令:
       (ex command):tfirst             go to first match
                   :[count]tprevious   go to [count] previous match
                   :[count]tnext       go to [count] next match
                   :tlast              go to last match
如果沒有指定[count],預設是1。

    6、tag的名字

    命令補齊真是避免鍵入一個長tag名的好辦法。只要輸入開頭的幾個字元然後按下製表符:
       (ex command) :tag write_<Tab>
Vim 會為你補全第一個符合的tag名。如果還不合你意,接著按製表符直到找到你要的。有時候你只記得一個tag名的片段,或者有幾個tag開頭相同。這裡你可以用一個模式匹配來告訴Vim你要找的tag。

    假設你想跳轉到一個包含"block"的tag。首先鍵入命令:(ex command) :tag /block。現在使用命令補齊:按<Tab>。Vim會找到所有包含"block"的tag並先提供給你第一個符合的。"/"告訴Vim下面的名字不是一五一十的tag名,而是一個搜尋模式。通常的搜尋技巧都可以用在這裡。比如你有一個tag以"write "開始:(ex command) :tselect / ^write_,"^"表示這個tag以"write_"開始。不然在半中間出現write的tag也會被搜尋到。同樣"$"可以用於告訴Vim要查詢的tag如何結束。

    7、tags的瀏覽器

    CTRL+]可以直接跳轉到以當前游標下的word為tag名的地方去,所以可以在一個tag列表中使用它。下面是一個例子。首先建立一個識別符號的列表(這需要一個好的ctags):
      (shell command) ctags --c-types=f -f functions *.c

    現在直接啟動Vim, 以一個垂直分隔視窗的編輯命令開啟生成的檔案:
       (shell command) vim:vsplit functions
    這個視窗中包含所有函式名的列表。可能會有很多內容,但是你可以暫時忽略它。用一個":setlocal ts=99"命令清理一下顯示。在該視窗中,定義這樣的一個對映:
        (ex command):nnoremap <buffer> <CR> 0ye<C-W>w:tag <C-R>"<CR>
    現在把游標移到你想要檢視其定義的函式名上,按下Enter鍵,Vim就會在另一個視窗中開啟相應的檔案並定位到到該函式的定義上。

    8、其它相關主題

    設定'ignorecase'也可以讓tag名的處理忽略掉大小寫。'tagsearch'選項告訴Vim當前參考的tags檔案是否是排序過的。預設情況假設該檔案是排序過的,這會使tag的搜尋快一些,但如果tag檔案實際上沒有排序就會在搜尋時漏掉一些tag。

    'taglength'告訴Vim一個tag名字中有效部分的字元個數。例:
#include <stdio.h>
int very_long_variable_1;
int very_long_variable_2;
int very_long_variable_3;
int very_long_variable_4;
int main()
{
    very_long_variable_4 = very_long_variable_1 *
    very_long_variable_2;
}

    對於上面這段程式碼, 4個變數長度都為20, 如果將'taglength'設為10, 則:
       (ex command):tag very_long_variable_4
會匹配到4個tag,而不是1個,游標停留在very_long_variable_1所在行上,因為被搜尋的tag部分只有前面的10個字元: "very_long_",相應的顯示是(是gvim中文版的真正顯示,不是翻譯的):
      (Display)找到tag: 1/4 或更多