# 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,主動回報並提醒開發者:

紅色標注的二行程式碼 (即 `int member1;` 和 `int member2;`) 不符合指定的 4 個空白縮排方式,在 git pre-commit 階段就成功阻擋這樣風格不一致的程式碼變更。
### 撰寫 Git Commit Message 和自動檢查機制

Git commit message 是什麼呢?在取得 [lab0-c](https://github.com/sysprog21/lab0-c) 程式碼後,執行 `$ git log` 的輸出就是了。你或許會納悶,commit message 又不是程式碼,充其量只能算是「程式開發的軌跡」,為何要特別探討?
:::info
:notebook: 可安裝 `tig` 套件,更便利地瀏覽 git repository 資訊。
安裝方式: `$ sudo apt install tig`
參考執行畫面如下:

:::
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` 去調整程式縮排,才得以繼續提交。