# Jserv老師-課程的coding style ### clang-format 工具和一致的程式撰寫風格 使用一致的 [programming style](https://en.wikipedia.org/wiki/Programming_style) 很重要,我們可透過 [clang-format](https://clang.llvm.org/docs/ClangFormat.html) 這個工具來調整作業程式要求的風格,使用方式如下: ```shell $ clang-format -i *.[ch] ``` 課程要求的 C 程式撰寫風格簡述: * 使用 ==4 個空白字元==來進行縮排,不用 Tab; - 為何不比照 Linux 核心都用 tab 呢? - 首先是為了 code review 的便利,4 個空白字元可避免程式碼過寬,從而易於課堂討論,且和共筆系統 (這裡指 [HackMD](https://hackmd.io/)) 預設的縮排方式相符。 - 再者是考慮到不同的編輯器 (editor) 對於 tab 行為有落差,而且 [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) 也建議使用空白字元而非 tab,這樣的風格被許多開放原始碼專案所採納; * 將單一行程式碼寬度控制在 80 個字元內 - 任何一行超過 80 列寬度的敘述都該拆分成多個行 * switch 敘述的縮排方式是讓 case 與 switch 對齊,範例: ```cpp switch (c) { case 'h': usage(argv[0]); break; ``` * 延續 [K&R C](https://en.wikipedia.org/wiki/The_C_Programming_Language) 風格去處理大括號 (`{ }` 英語: curly brackets, 又稱花括號) 與空格 - Kernighan 和 Ritchie 在撰寫《[The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language)》一書所用的程式範例,是把左大括號 `{` 放在行末,把右大括號 `}` 放在行首,如: ```cpp if (x == true) { do_y(); } else { do_z(); } do { /* body of do-loop */ } while (condition); ``` - 然而有個特殊的例子,就是函式:函式的左大括號應該放在行首,如: ```cpp int function(int x) { body of function } ``` * 在不降低可讀性的前提下,儘可能減少空行的數量。無用的換行一旦減少,那就有更多的空行來寫註解。 * Linux 核心風格對於空格,體現於一些關鍵字的運用,即在關鍵字之後增添一個空格。值得關注的例外是一些長得像函式的關鍵字,如: `sizeof`, `typeof`, `alignof`, `__attribute__` (註: 這是 [gcc 延展的功能](https://gcc.gnu.org/onlinedocs/gcc/Attribute-Syntax.html)),在 Linux 核心中,這些關鍵字的使用都會帶上一對括號,儘管在 C 語言的使用上並不需帶上括號 * `sizeof` 不是函式,而是 operator! * 在這些關鍵字之後新增一個空格: `if`, `switch`, `case`, `for`, `do`, `while` - 但不要新增在 `sizeof`, `typeof`, `alignof`, `__attribute__` 之後 - 期望見到的用法是 ```cpp s = sizeof(struct file); ``` * 不要在括號周圍多此一舉的新增空格 - 下面這個例子糟透了 ```cpp s = sizeof( struct file ); ``` * 在宣告指標或者返回值為指標的函式時,指標 (即 `*` 符號) 的位置應該緊靠著變數名稱或函式名稱,而非型別名稱,例如: ```cpp char *linux_banner; unsigned long long memparse(char *ptr, char **retptr); char *match_strdup(substring_t *s); ``` * 在二元操作符和三元操作符周圍新增一個空格 - 例如: `= + - < > * / % | & ^ <= >= == != ? :` - 但不要在一元操作符之後新增空格: `&*+-~!` `sizeof` `typeof` `alignof` `__attribute__` `defined` - 不要在後綴的 `++` `--` 運算子之前新增空格 (即 `i++`) - 不要在開頭的 `++` `--` 之後新增空格 (即 `++i`) - 不要在結構體成員運算子 (即 `.` 和 `->`) 周圍新增空格 * 不要在行末新增多餘的空格 - 某些編輯器的「智慧」縮排會幫你在行首新增一些空格,好讓你在下一行可以立即撰寫程式碼。但某些編輯器不會幫你把多餘的空格給刪掉,儘管你已經寫完了一行程式碼。比如你只想留一行空行,但是編輯器卻「好心」地幫你填上了一些空格。這樣一來,你就在行末添加多餘的空格。 * 變數和函式命名力求簡潔且精準 - C 是種==簡潔粗曠==的語言,因此命名也該簡潔 - C 程式設計師不會像 Pascal 程式設計師那樣使用 `ThisVariableIsATemporaryCounter` 這種「可愛」的名字,相反地,一個 C 程式設計師會把這種變數命名為 `tmp`,如此簡潔易寫。 - 全域變數 (只有當你真正需要的時候才用它) 和全域函式 (也就是沒有用 `static` 宣告的函式) 需要使用描述性的名稱。若你有個計算活躍使用者數量的函式,你應該用 count_active_users() 一類的名稱,避免用 `cntusr()` 這樣不易望文生義的名稱。 - 起一個包含函式型別的名字([匈牙利命名法](https://en.wikipedia.org/wiki/Hungarian_notation))是摧殘大腦的行為,編譯器知道函式的型別並且會檢查型別,這樣的名字不會起到任何幫助,它僅僅會迷惑程式設計師。 - 區域變數名應該簡短,若你需要寫一個迴圈,定義一個計數器,在不產生歧義的情況下,你大可命名為 `i`。反過來說,命名為 `loop_counter` 是生產力很低的行為。同樣地,`tmp` 可以是任何型別的區域變數。 :::info :-1: 你可以想像 Apple 和 Google 的工程師隨便安置程式碼,然後不用管合作的議題嗎? :balloon: 「[Linux 核心設計](http://wiki.csie.ncku.edu.tw/linux/schedule)」課程希望引導學員最終能夠欣然面對 Linux 核心或者有一定規模的軟體專案,不再只是「[舉燭](http://dict.revised.moe.edu.tw/cgi-bin/cbdic/gsweb.cgi?ccd=UOmTQ4&o=e0&sec1=1&op=sid=%22Z00000158282%22.&v=-2)」,而是真正和世界各地的高手協作,上述看似繁雜的程式開發風格就會是相當基本且該貫徹的基本素養。 :+1: 或許你會反問:「只是一個作業,有必要這樣自虐嗎?」不妨這樣想:即便一個人寫作業,其實是三人的參與 —— 過去的你、現在的你,以及未來的你 ::: ### [Git Hooks](https://www.atlassian.com/git/tutorials/git-hooks) 進行自動程式碼排版檢查 首次執行 `make` 後,Git pre-commit / pre-push hook 將自動安裝到現行的工作區 (workspace),之後每次執行 `git commit` 時,Git hook 會檢查 C/C++ 原始程式碼的風格是否一致,並透過 [Cppcheck](http://cppcheck.sourceforge.net/) 進行靜態程式碼檢查。 :::warning :warning: 任何人都可以寫出機器看得懂的程式碼 (在 Windows 檔案總管裡面,隨便選一個 EXE 檔,按右鍵複製,隨後再貼上即可),但我們之所以到資訊工程系接受訓練,為了寫出人看得懂、可持續維護和改進的程式 ::: 下圖展示 Git pre-commit hook 偵測到開發者的修改並未遵守一致的 coding style,主動回報並提醒開發者: ![](https://i.imgur.com/g7DQtYF.png) 紅色標注的二行程式碼 (即 `int member1;` 和 `int member2;`) 不符合指定的 4 個空白縮排方式,在 git pre-commit 階段就成功阻擋這樣風格不一致的程式碼變更。 ### 撰寫 Git Commit Message 和自動檢查機制 ![](https://i.imgur.com/stK5oBN.png) Git commit message 是什麼呢?在取得 [lab0-c](https://github.com/sysprog21/lab0-c) 程式碼後,執行 `$ git log` 的輸出就是了。你或許會納悶,commit message 又不是程式碼,充其量只能算是「程式開發的軌跡」,為何要特別探討? :::info :notebook: 可安裝 `tig` 套件,更便利地瀏覽 git repository 資訊。 安裝方式: `$ sudo apt install tig` 參考執行畫面如下: ![](https://i.imgur.com/pMRybxY.png) ::: Peter Hutterer 在 [On commit messages](https://who-t.blogspot.com/2009/12/on-commit-messages.html) 說得很精闢: > 「重新了解一段程式碼更動的脈絡很浪費腦力。雖然這件事情沒辦法完全避免,但是我們可以盡量[降低](https://www.osnews.com/story/19266/wtfsm/)這件事情的複雜度。Commit messages 正可以做到這點,而**我們可以從 commit message 看出一個開發者是否為一位好的合作對象**。」 一個專案是否能長期且成功地運作 (撇除其他影響的因素),取決於它的可維護性,而在這件事上沒有其他工具比專案本身的開發軌跡更為強大。因此花時間學習如何撰寫與維護專案的開發紀錄,是件值得投資的事。一開始你可能會覺得麻煩,但它很快就會變成一種習慣,甚至能成為你之所以感到自豪及具備生產力的因素。 Chris Beams 在 [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) 一文 (繁體中文翻譯: [如何寫一個 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/)) 提出,好的 Git commit message 應符合七條規則: 1. 用一行空白行分隔標題與內容 2. 限制標題最多只有 50 字元 3. 標題開頭要大寫 4. 標題不以句點結尾 5. 以祈使句撰寫標題 6. 內文每行最多 72 字 7. 用內文解釋 what 以及 why vs. how [lab0-c](https://github.com/sysprog21/lab0-c) 內建 Git pre-commit hook 就包含依循上述七條規則的自動檢查,可阻擋不符合規則的 git commit message,此外還透過 [GNU Aspell](http://aspell.net/) 對 commit message 的標題進行拼字檢查。 :::info :warning: 既然我們著眼於 Linux 核心,當然要用英語撰寫 commit message,英語不該只是大學畢業門檻,更該要活用,「[Linux 核心設計](http://wiki.csie.ncku.edu.tw/linux/schedule)」課程還兼英語寫作的功效,真划算! ::: 以下實際操作 Git。 事先編輯檔案 `queue.h` 後,執行 `$ git commit` 會發現: ```shell $ git commit On branch master Your branch is up-to-date with 'origin/master'. Changes not staged for commit: modified: queue.h no changes added to commit ``` 原來要指定變更的檔案,用命令 `$ git add`: ```shell $ git add queue.h ``` :::success :notebook: 可改用 `$ git commit -a` 這道命令,讓 Git 自動追蹤變更的檔案並提交 ::: 重新 `git commit`,這時可發現安裝的 Git hooks 發揮作用: ```shell= $ git commit -m "add fields to point to the tail" add fields to point to the tail [line 1] - Capitalize the subject line Proceed with commit? [e/n/?] e add fields to point to the tail [line 1] - Capitalize the subject line Proceed with commit? [e/n/?] y How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/ e - edit commit message n - abort commit ? - print help Proceed with commit? [e/n/?] ? How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/ e - edit commit message n - abort commit ? - print help Proceed with commit? [e/n/?] e [master 23f7113] Add fields to point to the tail ``` Git pre-commit hook 提示我們: * 第 4 行輸入 `e`,可編輯 git commit message; * 第 7 行輸入 `y`,因為 y 不是有效選項, 所以出現 help; * 第 17 行輸入 `e`, 再次編輯訊息,因訊息有限制標題開頭要大寫; 注意:請避免用 `$ git commit -m`,而是透過編輯器調整 git commit message。許多網路教學為了行文便利,用 `$ git commit -m` 示範,但這樣很容易讓人留下語焉不詳的訊息,未能提升為 [好的 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/)。因此,從今以後,不要用 `git commit -m`, 改用 `git commit -a` (或其他參數) 並詳細查驗變更的檔案。 在 Ubuntu Linux 預設組態中,執行 `$ git commit -a` 會呼叫 [GNU nano](https://www.nano-editor.org/) 來編輯訊息,可透過變更 `EDITOR` 環境變數,讓 git 找到你偏好的編輯器,例如指定 vim 就可以這樣做: ```shell export EDITOR=vim ``` 你也可把上述變更加到 bash 設定檔案中,例如: ```shell $ echo "export EDITOR=vim" >> ~/.bashrc ``` 一番折騰後,終於順利提交 (commit) 我們的變更: ``` shell $ git commit -a On branch master Your branch is ahead of 'origin/master' by 1 commit. (use "git push" to publish your local commits) nothing to commit, working tree clean ``` 需要注意的是,目前提交的變更**僅存在於這台電腦中**,還未發佈到 GitHub 上,於是我們需要執行 `git push` > 下方的 `jserv` 是示範帳號,請換為你自己的 GitHub 帳號名稱 ```shell $ git push Username for 'https://github.com': jserv Password for 'https://jserv@github.com': Hint: You might want to know why Git is always asking for my password. https://help.github.com/en/github/using-git/why-is-git-always-asking-for-my-password Running pre push to master check... Trying to build tests project... Pre-push check passed! Counting objects: 4, done. Delta compression using up to 64 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (4/4), 709 bytes | 709.00 KiB/s, done. Total 4 (delta 3), reused 0 (delta 0) remote: Resolving deltas: 100% (3/3), completed with 3 local objects. To https://github.com/sysprog21/lab0-c d2d7711..b42797a master -> master ``` 不難發現上面訊息提到 [Why is Git always asking for my password?](https://help.github.com/en/github/using-git/why-is-git-always-asking-for-my-password) 這個超連結,如果你厭倦每次 `$ git push` 都需要輸入帳號及密碼,可參考超連結內容去變更設定。 又因 pre-push hook 使然,每次發布變更到 GitHub 服務時,只要是 `master` branch (主要分支) 就會觸發編譯要求,只有通過編譯的程式碼才能發布,但 `master` 以外的 branch 不受此限,換言之,我們鼓勵學員運用 `git branch` 進行各項實驗,請參照線上中文教材 ==《[為你自己學 Git](https://gitbook.tw/)》==。 ### 牛刀小試 若你很有耐心地讀到這裡,恭喜你,終於可著手修改 [lab0-c](https://github.com/sysprog21/lab0-c) 程式碼。 這裡我們嘗試實作函式 `q_size`,首先找出 [`queue.c`](https://github.com/sysprog21/lab0-c/blob/master/queue.c) 檔案中對應的註解: > Return number of elements in queue. > Return 0 if q is NULL or empty > Remember: It should operate in **$O(1)$** time `q_size(queue_t *)` 由於要在 $O(1)$ 的常數時間內執行完成,所以也不可能每次都走訪整個鏈結串列,以取得佇列的容積。又在 [`queue.h`](https://github.com/sysprog21/lab0-c/blob/master/queue.h) 的註解,我們得知: > You will need to add more fields to this structure to efficiently implement `q_size` and `q_insert_tail` 因此,我們嘗試新增 **int size** 這個新成員到 `struct queue_t`: ```cpp typedef struct { list_ele_t *head; /* Linked list of elements */ int size; } queue_t; ``` 接著修改 `q_size` 的傳回值,改成傳回 `q->size`。一切就緒後,提交修改: ```cpp $ git commit -m "Change q_size return value to q->size" --- modified queue.c +++ expected coding style @@ -70,7 +70,7 @@ bool q_insert_tail(queue_t *q, char *s) { /* You need to write the complete code for this function */ /* Remember: It should operate in O(1) time */ - list_ele_t *newt; // newt means new tail + list_ele_t *newt; // newt means new tail newt = malloc(sizeof(list_ele_t)); q->tail = newt; return true; [!] queue.c does not follow the consistent coding style. Make sure you indent as the following: clang-format -i queue.c ``` Git pre-commit hook 偵測到程式縮排不符合前述的風格,因而擋住我們的提交,我們要執行 `$ clang-format -i queue.c` 去調整程式縮排,才得以繼續提交。