章 12. 編程

# 章 12. 編程

這裡我給出一些 Debian 系統中的資訊，幫助學習程式設計的人找出打包的原始碼。下面是值得關注的軟體包和與之對應的文件。

安裝 `manpages` 和 `manpages-dev` 包之後，可以通過運行“`man 名稱`”查看手冊頁中的參考資訊。安裝了 GNU 工具的相關文檔包之後，可以通過運行“`info 程序名稱`”查看參考文檔。某些 GFDL 協議的文檔與 DFSG 並不兼容，所以你可能需要在 `main` 倉庫中包含 `contrib` 和 `non-free` 才能下載並安裝它們。

請考慮使用版本控制系統工具。參見 節 10.5, “Git”。

| 警告
不要用“`test`”作爲可執行的測試文件的名字，因爲 shell 中內建有“`test`”指令。

| 注意
你可以把從源代碼編譯得到的程序直接放到“`/usr/local`”或“`/opt`”目錄，這樣可以避免與系統程序撞車。

| 提示
“歌曲：99瓶啤酒”的代碼示例可以給你提供實踐各種語言的好範本。

## 12.1. Shell 腳本

Shell 腳本 是指包含有下面格式的可執行的文本文件。

    #!/bin/sh
     ... command lines

第一行指明瞭讀取並執行這個文件的 shell 解釋器。

讀懂 shell 指令碼的最好 辦法是先理解類 Unix 系統是如何工作的。這裡有一些 shell 程式設計的提示。看看“Shell 錯誤”（https://www.greenend.org.uk/rjk/2001/04/shell.html），可以從錯誤中學習。

不像 shell 交互模式（參見節 1.5, “簡單 shell 指令” 和 節 1.6, “類 Unix 的文本處理”），shell 腳本會頻繁使用參數、條件和循環等。

### 12.1.1. POSIX shell 兼容性

系統中的許多指令碼都可以通過任意 POSIX shell（參見 表格 1.13, “shell 程式列表”）來執行。

- 預設的非互動 POSIX shell "`/usr/bin/sh`" 是一個指向到 `/usr/bin/dash` 的符號連結，並被許多系統程式使用。

- 預設的互動式 POSIX shell 是 `/usr/bin/bash`。

避免編寫具有 bashisms（bash 化）或者 zshisms（zsh 化）語法的 shell 指令碼，確保指令碼在所有 POSIX shell 之間具有可移植性。你可以使用 checkbashisms(1) 對其進行檢查。

表格 12.1. 典型 bashism 語法列表

好的：POSIX | 應該避免的：bashism
--- | ---
`if [ "$foo" = "$bar" ] ; then …` | `if [ "$foo" == "$bar" ] ; then …`
`diff -u file.c.orig file.c` | `diff -u file.c{.orig,}`
`mkdir /foobar /foobaz` | `mkdir /foo{bar,baz}`
`funcname() { … }` | `function funcname() { … }`
八進位制格式："`\377`" | 十六進位制格式："`\xff`"

使用 "`echo`" 指令的時候需要注意以下幾個方面，因為根據內建 shell 和外部指令的不同，它的實現也有差別。

- 避免使用除“`-n`”以外的任何指令列選項。

- 避免在字串中使用轉義序列，因為根據 shell 不同，計算後的結果也不一樣。

| 注意
儘管“`-n`”選項並不是 POSIX 語法，但它已被廣泛接受。

| 提示
如果你想要在輸出字串中嵌入轉義序列，用 "`printf`" 指令替代 "`echo`" 指令。

### 12.1.2. Shell 參數

特殊的 shell 參數經常在 shell 腳本里面被用到。

表格 12.2. shell 參數列表

shell 參數 | 值
--- | ---
`$0` | shell 或 shell 指令碼的名稱
`$1` | 第一個 shell 參數
`$9` | 第 9 個 shell 參數
`$#` | 位置參數數量
`"$*"` | `"$1 $2 $3 $4 … "`
`"$@"` | `"$1" "$2" "$3" "$4" …`
`$?` | 最近一次指令的退出狀態碼
`$$` | 這個 shell 指令碼的 PID
`$!` | 最近開始的後臺任務 PID

如下所示是需要記憶的基本的參數展開。

表格 12.3. shell 參數展開列表

參數表示式形式 | 如果 `var` 變數已設定那麼值為 | 如果 `var` 變數沒有被設定那麼值為
--- | --- | ---
`${var:-string}` | "`$var`" | "`string`"
`${var:+string}` | "`string`" | "`null`"
`${var:=string}` | "`$var`" | "`string`" (並執行 "`var=string`")
`${var:?string}` | "`$var`" | 在 stderr 中顯示 "`string`" (出錯退出)

以上這些操作中 "`:`" 實際上都是可選的。

- 有 "`:`" 等於測試的 var 值是存在且非空

- 沒有 "`:`" 等於測試的 var 值只是存在的，可以為空

表格 12.4. 重要的 shell 參數替換列表

參數替換形式 | 結果
--- | ---
`${var%suffix}` | 刪除位於 var 結尾的 suffix 最小匹配模式
`${var%%suffix}` | 刪除位於 var 結尾的 suffix 最大匹配模式
`${var#prefix}` | 刪除位於 var 開頭的 prefix 最小匹配模式
`${var##prefix}` | 刪除位於 var 開頭的 prefix 最大匹配模式

### 12.1.3. Shell 條件語句

每個指令都會回傳 退出狀態，這可以被條件語句使用。

- 成功：0 ("True")

- 失敗：非0 ("False")

| 注意
"0" 在 shell 條件語句中的意思是 "True"，然而 "0" 在 C 條件語句中的含義為 "False"。

| 注意
"`[`" 跟 `test` 指令是等價的，它評估到 "`]`" 之間的參數來作為一個條件表示式.

如下所示是需要記憶的基礎 條件語法。

- "`command && if_success_run_this_command_too || true`"

- "`command || if_not_success_run_this_command_too || true`"

- 如下所示是多行指令碼片段

    if [ conditional_expression ]; then
     if_success_run_this_command
    else
     if_not_success_run_this_command
    fi

這裡末尾的“`|| true`”是需要的，它可以保證這個 shell 指令碼在不小心使用了“`-e`”選項而被呼叫時不會在該行意外地退出。

表格 12.5. 在條件表示式中進行檔案比較

表示式 | 回傳邏輯真所需的條件
--- | ---
`-e file` | file 存在
`-d file` | file 存在並且是一個目錄
`-f file` | file 存在並且是一個普通檔案
`-w file` | file 存在並且可寫
`-x file` | file 存在並且可執行
`file1 -nt file2` | file1 是否比 file2 新
`file1 -ot file2` | file1 是否比 file2 舊
`file1 -ef file2` | file1 和 file2 位於相同的裝置上並且有相同的 inode 編號

表格 12.6. 在條件表示式中進行字串比較

表示式 | 回傳邏輯真所需的條件
--- | ---
`-z str` | str 的長度為零
`-n str` | str 的長度不為零
`str1 = str2` | str1 和 str2 相等
`str1 != str2` | str1 和 str2 不相等
`str1 < str2` | str1 排列在 str2 之前（取決於語言環境）
`str1 > str2` | str1 排列在 str2 之後（取決於語言環境）

算術整數的比較在條件表示式中為 "`-eq`"，"`-ne`"，"`-lt`"，"`-le`"，"`-gt`" 和 "`-ge`"。

### 12.1.4. shell 迴圈

這裡有幾種可用於 POSIX shell 的迴圈形式。

- "`for x in foo1 foo2 … ; do command ; done`"，該迴圈會將 "`foo1 foo2 …`" 賦予變數 "`x`" 並執行 "`command`"。

- "`while condition ; do command ; done`"，當 "`condition`" 為真時，會重複執行 "`command`"。

- "`until condition ; do command ; done`"，當 "`condition`" 為假時，會重複執行 "`command`"。

- "`break`" 可以用來退出迴圈。

- "`continue`" 可以用來重新開始下一次迴圈。

| 提示
C 語言中的數值迭代可以用 seq(1) 實現來生成 "`foo1 foo2 ...`"。

| 提示
參見 節 9.4.9, “使用檔案迴圈來重複一個指令”。

### 12.1.5. Shell 環境變數

普通的 shell 命令列提示下的一些常見的環境變數，可能在你的指令碼的執行環境中不存在。

- 對於 "`$USER`", 使用 "`$(id -un)`"

- 對於 "`$UID`", 使用 "`$(id -u)`"

- 對於 "`$HOME`"，使用"`$(getent passwd "$(id -u)"|cut -d ":" -f 6)`" (這個也在 節 4.5.2, “現代的集中式系統管理” 下工作)

### 12.1.6. shell 指令列的處理順序

shell 大致以下列的順序來處理一個指令碼。

- shell 讀取一行。

- 如果該行包含有`"…"` 或 `'…'`，shell 對該行各部分進行分組作為 一個標識（one token） （譯註：one token 是指 shell 識別的一個結構單元）.

- shell 通過下列方式將行中的其它部分分隔進 標識（tokens）。

  - 空白字元：`空格 tab 換行符`

  - 元字元：`< > | ; & ( )`

- shell 會檢查每一個不位於 `"…"` 或 `'...'` 的 token 中的 保留字 來調整它的行為。

  - 保留字：`if then elif else fi for in while unless do done case esac`

- shell 展開不位於 `"…"` 或 `'...'` 中的 別名。

- shell 展開不位於 `"…"` 或 `'...'` 中的 波浪線。

  - "`~`" → 當前使用者的家目錄

  - "`~user`" → `user` 的家目錄

- shell 將不位於 `'...'` 中的 變數 展開為它的值。

  - 變數："`$PARAMETER`" 或 "`${PARAMETER}`"

- shell 展開不位於 `'...'` 中的 指令替換。

  - "`$( command )`" → "`command`" 的輸出

  - "`` command ``" → "`command`" 的輸出

- shell 將不位於 `"…"` 或 `'...'` 中的 glob 路徑 展開為匹配的檔名。

  - `*` → 任何字元

  - `?` → 一個字元

  - `[…]` → 任何位於 "`…`" 中的字元

- shell 從下列幾方面查詢 指令 並執行。

  - 函式 定義

  - 內建指令

  - “`$PATH`” 中的可執行檔案

- shell 前往下一行，並按照這個順序從頭再次進行處理。

雙引號中的單引號是沒有效果的。

在 shell 中執行 “`set -x`” 或使用 “`-x`” 選項啟動 shell 可以讓 shell 顯示出所有執行的指令。這對除錯來說是非常方便的。

### 12.1.7. 用於 shell 指令碼的應用程式

為了使你的 shell 程式在 Debian 系統上儘可能地具有可移植性，你應該只使用 必要的 軟體包所提供的應用程式。

- "`aptitude search ~E`"，列出 必要的 軟體包。

- "`dpkg -L package_name |grep '/man/man.*/'`"，列出 `package_name` 軟體包所提供的 man 手冊。

表格 12.7. 包含用於 shell 指令碼的小型應用程式的軟體包

軟體包 | 流行度 | 大小 | 說明
--- | --- | --- | ---
`dash` | V:928, I:998 | 207 | 小和快的 POSIX 相容 shell，用於 `sh`
`coreutils` | V:910, I:1000 | 17994 | GNU 核心工具
`grep` | V:779, I:1000 | 1297 | GNU `grep`、`egrep` 和 `fgrep`
`sed` | V:762, I:1000 | 987 | GNU `sed`
`mawk` | V:485, I:998 | 295 | 小和快的 `awk`
`debianutils` | V:936, I:997 | 225 | 用於 Debian 的各種工具
`bsdutils` | V:445, I:1000 | 338 | 來自 4.4BSD-Lite 的基礎工具
`bsdextrautils` | V:754, I:863 | 382 | 來自 4.4BSD-Lite 的額外的工具
`moreutils` | V:18, I:39 | 232 | 額外的 Unix 工具

| 提示
儘管 `moreutils` 可能不存在 Debian 之外，但它提供了一些有趣的小程式。最值得注意的一個是 sponge(8)，當你想覆蓋原來的檔案時，它會非常好用。

參見 節 1.6, “類 Unix 的文本處理” 的例子。

## 12.2. 解釋性語言中的指令碼

表格 12.8. 直譯器相關軟體包列表

軟體包 | 流行度 | 大小 | 包
--- | --- | --- | ---
`dash` | V:928, I:998 | 207 | sh: 小和快的 POSIX 相容的 shell，用於 `sh`
`bash` | V:893, I:1000 | 7309 | sh: 由 `bash-doc` 包提供的“`info bash`”
`mawk` | V:485, I:998 | 295 | AWK: 小和快的 `awk`
`gawk` | V:250, I:301 | 3289 | AWK: 由 `gawk-doc` 包提供的“`info gawk`”
`perl` | V:673, I:992 | 836 | Perl: perl(1) 以及通過 `perl-doc` 和 `perl-doc-html` 提供的 html 文件
`libterm-readline-gnu-perl` | V:2, I:27 | 439 | GNU ReadLine/History 庫的 Perl 擴充套件： perlsh(1)
`libreply-perl` | V:0.01, I:0.10 | 171 | Perl 的 REPL : reply(1)
`libdevel-repl-perl` | V:0.03, I:0.57 | 237 | Perl 的 REPL : re.pl(1)
`python3` | V:717, I:974 | 80 | Python: python3(1) 以及通過 `python3-doc` 包提供的 html 文件
`tcl` | V:23, I:176 | 20 | Tcl: tcl(3) 以及通過 `tcl-doc` 包提供的更詳細的手冊頁文件
`tk` | V:17, I:169 | 20 | Tk:tk(3) 以及通過 `tk-doc` 包提供的更詳細的手冊頁文件
`ruby` | V:71, I:157 | 32 | Ruby: ruby(1), erb(1), irb(1), rdoc(1), ri(1)

當你希望在 Debian 上自動化執行一個任務，你應當首先使用解釋性語言指令碼。選擇解釋性語言的準則是：

- 使用 `dash`，如果任務是簡單的，使用 shell 程式聯合 CLI 命令列程式。

- 使用 `python3`，如果任務不是簡單的，你從零開始寫。

- 使用 `perl`、`tcl`、`ruby`……，如果在 Debian 上有用這些語言寫的現存程式碼，需要為完成任務進行調整。

如果最終程式碼太慢，為提升執行速度，你可以用編譯型語言重寫關鍵部分，從解釋性語言呼叫。

### 12.2.1. 除錯解釋性語言程式碼

大部分直譯器提供基本的語法檢查和程式碼跟蹤功能。

- “dash -n script.sh” - Shell 指令碼語法檢查

- “dash -x script.sh” - 跟蹤一個 Shell 指令碼

- “python -m py_compile script.py” - Python 指令碼語法檢查

- “python -mtrace --trace script.py” - 跟蹤一個 Python 指令碼

- “perl -I ../libpath -c script.pl” - Perl 指令碼語法檢查

- “perl -d:Trace script.pl” - 跟蹤一個 Perl 指令碼

為測試 `dash` 程式碼，嘗試下 節 9.1.4, “Readline 封裝”，它提供了和 `bash` 類似的互動式環境。

為了測試 `perl` 程式碼，嘗試下 Perl 的 REPL 環境，它為 Perl 提供了 Python 類似的 REPL (=READ + EVAL + PRINT + LOOP) 環境。

### 12.2.2. 使用 shell 指令碼的 GUI 程式

shell 指令碼能夠被改進用來製作一個吸引人的 GUI（圖形使用者介面）程式。技巧是用一個所謂的對話程式來代替使用 `echo` 和 `read` 命令的乏味互動。

表格 12.9. 對話（dialog ）程式列表

軟體包 | 流行度 | 大小 | 說明
--- | --- | --- | ---
`x11-utils` | V:222, I:551 | 651 | xmessage(1)：在一個視窗中顯示一條訊息或疑問（X）
`whiptail` | V:303, I:997 | 61 | 從 shell 指令碼中顯示使用者友好的對話方塊（newt）
`dialog` | V:8, I:77 | 520 | 從 shell 指令碼中顯示使用者友好的對話方塊（ncurses）
`zenity` | V:74, I:338 | 193 | 從 shell 指令碼中顯示圖形對話方塊（GTK）
`ssft` | V:0.01, I:0.16 | 80 | Shell 指令碼前端工具 (zenity, kdialog, and 帶有 gettext 的 dialog 封裝)
`gettext` | V:51, I:216 | 20468 | “`/usr/bin/gettext.sh`”：翻譯資訊

這裡是一個用來演示的 GUI 程式的例子，僅使用一個 shell 指令碼是多麼容易。

這個指令碼使用 `zenity` 來選擇一個檔案 (預設 `/etc/motd`) 並顯示它。

這個指令碼的 GUI 啟動器能夠按 節 9.4.10, “從 GUI 啟動一個程式” 建立。

    #!/bin/sh -e
    # Copyright (C) 2021 Osamu Aoki <osamu@debian.org>, Public Domain
    # vim:set sw=2 sts=2 et:
    DATA_FILE=$(zenity --file-selection --filename="/etc/motd" --title="Select a file to check") || \
      ( echo "E: File selection error" >&2 ; exit 1 )
    # Check size of archive
    if ( file -ib "$DATA_FILE" | grep -qe '^text/' ) ; then
      zenity --info --title="Check file: $DATA_FILE" --width 640  --height 400 \
        --text="$(head -n 20 "$DATA_FILE")"
    else
      zenity --info --title="Check file: $DATA_FILE" --width 640  --height 400 \
        --text="The data is MIME=$(file -ib "$DATA_FILE")"
    fi

這種使用 shell 指令碼的 GUI 程式方案只對簡單選擇的場景有用。如果你寫一個其它任何複雜的程式，請考慮在功能更強的平臺上寫。

### 12.2.3. 定製 GUI（圖形使用者介面）檔案管理器的行為

GUI（圖形使用者介面）檔案管理器在選定的檔案上，能夠用外加的擴充套件軟體包來擴充套件執行一些常見行為。通過增加特定的指令碼，它們也能夠用來定製執行非常特殊的行為。

- 對於 GNOME，參見 NautilusScriptsHowto。

- 對於 KDE，參見 Creating Dolphin Service Menus。

- 對於 Xfce，參見 Thunar - Custom Actions 和 https://help.ubuntu.com/community/ThunarCustomActions。

- 對於 LXDE，參見 Custom Actions。

### 12.2.4. Perl 短指令碼的瘋狂

為了處理資料，`sh` 需要生成子程序執行 `cut`、`grep`、 `sed` 等，是慢的。從另外一個方面，`perl` 有內部處理資料能力，是快的。所以 Debian 上的許多系統維護指令碼使用 `perl`。

讓我們考慮下面一行 AWK 指令碼片段和它在 Perl 中的等價物。

    awk '($2=="1957") { print $3 }' |

這等價於下列的任意一行。

    perl -ne '@f=split; if ($f[1] eq "1957") { print "$f[2]\n"}' |

    perl -ne 'if ((@f=split)[1] eq "1957") { print "$f[2]\n"}' |

    perl -ne '@f=split; print $f[2] if ( $f[1]==1957 )' |

    perl -lane 'print $F[2] if $F[1] eq "1957"' |

    perl -lane 'print$F[2]if$F[1]eq+1957' |

最後一個簡直就是個迷。它用上了下面列出的這些 Perl 的特性。

- 空格為可選項。

- 存在從數字到字串的自動轉換。

- 通過命令列選項： perlrun(1) 的 Perl 執行技巧

- Perl 特異變數：perlvar(1)

靈活性是 Perl 的強項。與此同時，這允許我們建立令人困惑和繁亂的程式碼。所以請小心。

## 12.3. 編譯型語言程式碼

表格 12.10. 編譯相關軟體包列表

軟體包 | 流行度 | 大小 | 說明
--- | --- | --- | ---
`gcc` | V:155, I:564 | 36 | GNU C 編譯器
`libc6-dev` | V:269, I:584 | 12861 | GNU C 庫：開發庫和標頭檔案
`g++` | V:57, I:531 | 13 | GNU C++ 編譯器
`libstdc++-16-dev` | V:0.7, I:4.3 | 24382 | GNU 標準 C++ 庫 版本 3（開發檔案）
`cpp` | V:332, I:719 | 18 | GNU C 預處理
`gettext` | V:51, I:216 | 20468 | GNU 國際化工具
`glade` | V:0.7, I:2.7 | 1613 | GTK 使用者介面構建器
`valac` | V:0.2, I:3.2 | 533 | 使用 GObject 系統類似 C# 的語言
`flex` | V:7, I:68 | 1247 | LEX 相容的 fast lexical analyzer generator
`bison` | V:7, I:73 | 3122 | YACC 相容的 解析器生成器
`susv2` | I:0.03 | 16 | 通過“單一UNIX規範（版本2）”獲得（英語文檔）
`susv3` | I:0.06 | 16 | 通過“單一UNIX規範（版本3）”獲得（英語文檔）
`susv4` | I:0.04 | 16 | 通過“單一UNIX規範（版本4）”獲取（英語文件）
`golang` | I:22 | 12 | Go 程式語言編譯器
`rustc` | V:5, I:20 | 13061 | Rust 系統程式語言
`gfortran` | V:4, I:50 | 15 | GNU Fortran 95 編譯器
`fpc` | I:2.3 | 101 | 自由 Pascal

這裡，包括了 節 12.3.3, “Flex — 一個更好的 Lex” 和 節 12.3.4, “Bison — 一個更好的 Yacc”，用來說明 類似編譯器的程式怎樣用C 語言來編寫，是通過編譯高階描述到 C 語言。

### 12.3.1. C

你可以通過下列方法設定適當的環境來編譯使用 C 程式語言編寫的程式。

    # apt-get install glibc-doc manpages-dev libc6-dev gcc build-essential

`libc6-dev` 軟體包，即 GNU C 庫，提供了 C 標準庫，它包含了 C 程式語言所使用的標頭檔案和庫例程。

參考資訊如下。

- “`info libc`”（C 庫函式參考）

- gcc(1) 和 “`info gcc`”

- each_C_library_function_name(3)

- Kernighan & Ritchie，“C 程式設計語言”，第二版（Prentice Hall）

### 12.3.2. 簡單的 C 程式（gcc）

一個簡單的例子 “`example.c`” 可以通過如下方式和 “`libm`” 庫一起編譯為可執行程式 “`run_example`”。

    $ cat > example.c << EOF
    #include <stdio.h>
    #include <math.h>
    #include <string.h>

    int main(int argc, char **argv, char **envp){
            double x;
            char y[11];
            x=sqrt(argc+7.5);
            strncpy(y, argv[0], 10); /* prevent buffer overflow */
            y[10] = '\0'; /* fill to make sure string ends with '\0' */
            printf("%5i, %5.3f, %10s, %10s\n", argc, x, y, argv[1]);
            return 0;
    }
    EOF
    $ gcc -Wall -g -o run_example example.c -lm
    $ ./run_example
            1, 2.915, ./run_exam,     (null)
    $ ./run_example 1234567890qwerty
            2, 3.082, ./run_exam, 1234567890qwerty

為了使用 sqrt(3)，必須使用 “`-lm`” 連結來自 `libc6` 軟體包的庫 “`/usr/lib/libm.so`”。實際的庫檔案位於 “`/lib/`”，檔名為 “`libm.so.6`”，它是指向 “`libm-2.7.so`” 的一個連結。

請看一下輸出文字的最後一段。即使指定了 “`%10s`”，它依舊超出了 10 個字元。

使用沒有邊界檢查的指標記憶體操作函式，比如 sprintf(3) 和 strcpy(3), 是不建議使用，是為防止快取溢位洩露而導致上面的溢位問題。請使用 snprintf(3) 和 strncpy(3) 來替代.

### 12.3.3. Flex — 一個更好的 Lex

Flex 是相容 Lex 的快速語法分析程式生成器。

可以使用 “`info flex`” 檢視 flex(1) 的教程。

很多簡單的例子能夠在 “`/usr/share/doc/flex/examples/`”下發現。[7]

### 12.3.4. Bison — 一個更好的 Yacc

在 Debian 裡，有幾個軟體包提供 Yacc相容的前瞻性的 LR 解析 或 LALR 解析的生成器。

表格 12.11. 相容 Yacc 的 LALR 解析器生成器列表

軟體包 | 流行度 | 大小 | 說明
--- | --- | --- | ---
`bison` | V:7, I:73 | 3122 | GNU LALR 解析器生成器
`byacc` | V:0.1, I:3.0 | 263 | 伯克利（Berkeley）LALR 解析器生成器
`btyacc` | V:0.01, I:0.06 | 247 | 基於 `byacc` 的回溯解析生成器

可以使用 “`info bison`” 檢視 bison(1) 的教學。

你需要提供你自己的的 "`main()`" 和 "`yyerror()`".通常，Flex 建立的 "`main()`" 呼叫 "`yyparse()`"，它又呼叫了 "`yylex()`".

這裡是一個建立簡單終端計算程式的例子。

讓我們建立 `example.y`:

    /* calculator source for bison */
    %{
    #include <stdio.h>
    extern int yylex(void);
    extern int yyerror(char *);
    %}

    /* declare tokens */
    %token NUMBER
    %token OP_ADD OP_SUB OP_MUL OP_RGT OP_LFT OP_EQU

    %%
    calc:
     | calc exp OP_EQU    { printf("Y: RESULT = %d\n", $2); }
     ;

    exp: factor
     | exp OP_ADD factor  { $$ = $1 + $3; }
     | exp OP_SUB factor  { $$ = $1 - $3; }
     ;

    factor: term
     | factor OP_MUL term { $$ = $1 * $3; }
     ;

    term: NUMBER
     | OP_LFT exp OP_RGT  { $$ = $2; }
      ;
    %%

    int main(int argc, char **argv)
    {
      yyparse();
    }

    int yyerror(char *s)
    {
      fprintf(stderr, "error: '%s'\n", s);
    }

讓我們建立 `example.l`:

    /* calculator source for flex */
    %{
    #include "example.tab.h"
    %}

    %%
    [0-9]+ { printf("L: NUMBER = %s\n", yytext); yylval = atoi(yytext); return NUMBER; }
    "+"    { printf("L: OP_ADD\n"); return OP_ADD; }
    "-"    { printf("L: OP_SUB\n"); return OP_SUB; }
    "*"    { printf("L: OP_MUL\n"); return OP_MUL; }
    "("    { printf("L: OP_LFT\n"); return OP_LFT; }
    ")"    { printf("L: OP_RGT\n"); return OP_RGT; }
    "="    { printf("L: OP_EQU\n"); return OP_EQU; }
    "exit" { printf("L: exit\n");   return YYEOF; } /* YYEOF = 0 */
    .      { /* ignore all other */ }
    %%

按下面的方法來從 shell 提示符執行來嘗試這個：

    $ bison -d example.y
    $ flex example.l
    $ gcc -lfl example.tab.c lex.yy.c -o example
    $ ./example
    1 + 2 * ( 3 + 1 ) =
    L: NUMBER = 1
    L: OP_ADD
    L: NUMBER = 2
    L: OP_MUL
    L: OP_LFT
    L: NUMBER = 3
    L: OP_ADD
    L: NUMBER = 1
    L: OP_RGT
    L: OP_EQU
    Y: RESULT = 9

    exit
    L: exit

## 12.4. 靜態程式碼分析工具

類似 lint 的工具能夠幫助進行自動化 靜態程式碼分析。

類似 Indent 的工具能夠幫助人進行程式碼檢查，通過一致性的重新格式化原始碼。

類似 Ctags 的工具能夠幫助人進行程式碼檢查，通過利用原始碼中發現的名字生成 索引（或標籤）檔案。

| 提示
配置你喜歡的編輯器(`emacs` 或 `vim`)使用非同步 lint 引擎外掛幫助你的程式碼寫作。這些外掛通過充分利用 Language Server Protocol 的優點，會變得非常強大。因它們在快速開發，使用它們上游的程式碼代替 Debian 軟體包，是一個好的選擇。

表格 12.12. 靜態程式碼分析工具的列表

軟體包 | 流行度 | 大小 | 說明
--- | --- | --- | ---
`vim-ale` | I:0.80 | 2833 | 用於 Vim 8 和 NeoVim 的非同步 Lint 引擎
`vim-syntastic` | I:2.0 | 1379 | vim 語法檢查利器
`elpa-flycheck` | V:0.1, I:1.7 | 1130 | Emacs 現代即時語法檢查
`elpa-relint` | I:0.06 | 150 | Emacs Lisp 正則錯誤發現器
`cppcheck-gui` | V:0.1, I:1.0 | 7697 | 靜態 C/C++ 程式碼分析工具(GUI)
`shellcheck` | V:3, I:16 | 22860 | shell 指令碼的 lint 工具
`pyflakes3` | V:1, I:12 | 20 | Python 3 程式被動檢查器
`pylint` | V:3, I:17 | 2093 | Python 程式碼靜態檢查器
`perl` | V:673, I:992 | 836 | 帶有內部靜態程式碼檢測的直譯器：`B::Lint`(3perl)
`rubocop` | V:0.1, I:1.1 | 4192 | Ruby 靜態程式碼分析器
`clang-tidy` | V:2, I:10 | 21 | 基於 clang 的 C++ 規則格式檢查工具
`splint` | V:0.06, I:0.92 | 2325 | 靜態檢查 C 程式 bug 的工具
`flawfinder` | V:0.07, I:0.49 | 187 | 檢查 C/C++ 原始碼和查詢安全漏洞的工具
`black` | V:2, I:13 | 10057 | 強硬的 Python 程式碼格式化器
`perltidy` | V:0.4, I:3.1 | 3086 | Perl 指令碼縮排和重新格式化
`indent` | V:0.8, I:5.1 | 438 | C 語言原始碼格式化程式
`astyle` | V:0.2, I:2.7 | 770 | C、 C++、 Objective-C、 C# 和 Java 的原始碼縮排器
`bcpp` | V:0.02, I:0.26 | 114 | 美化 C(++)
`xmlindent` | V:0.05, I:0.79 | 52 | XML 流 重新格式化
`global` | V:0.2, I:1.6 | 1923 | 原始碼檢索和瀏覽工具
`exuberant-ctags` | V:2, I:13 | 341 | 構建原始碼定義的標籤檔案索引
`universal-ctags` | V:2, I:10 | 4238 | 構建原始碼定義的標籤檔案索引

## 12.5. 除錯

除錯是程式中很重要的一部分。知道怎樣去除錯程式，能夠讓你成為一個好的 Debian 使用者, 能夠做出有意義的錯誤報告。

表格 12.13. 除錯軟體包列表

軟體包 | 流行度 | 大小 | 包
--- | --- | --- | ---
`gdb` | V:88, I:161 | 12453 | 由 `gdb-doc` 包提供的“`info gdb`”
`ddd` | V:0.7, I:5.2 | 4210 | 由 `ddd-doc` 包提供的“`info ddd`”

### 12.5.1. 基本的 gdb 使用指令

Debian 上原始的偵錯程式是 gdb(1), 它能讓你在程式執行的時候檢查程式。

讓我們通過如下所示的指令來安裝 `gdb` 及其相關程式。

    # apt-get install gdb gdb-doc build-essential devscripts

好的 `gdb` 教程能夠被發現：

- “`info gdb`”

- 在 `/usr/share/doc/gdb-doc/html/gdb/index.html` 的 “Debugging with GDB”

- “tutorial on the web”

這裡是一個簡單的列子，用 gdb(1) 在"`程式`"帶有 "`-g`" 選項編譯的時候來產生除錯資訊。

    $ gdb program
    (gdb) b 1                # set break point at line 1
    (gdb) run args           # run program with args
    (gdb) next               # next line
    ...
    (gdb) step               # step forward
    ...
    (gdb) p parm             # print parm
    ...
    (gdb) p parm=12          # set value to 12
    ...
    (gdb) quit

| 提示
許多 gdb(1) 指令都能被縮寫。Tab 擴展跟在 shell 一樣都能工作。

### 12.5.2. 除錯 Debian 軟體包

Debian 系統在預設情況下，所有安裝的二進位制程式會被 stripped，因此大部分除錯符號（debugging symbols）在通常的軟體包裡面會被移除。為了使用 gdb(1) 除錯 Debian 軟體包， `*-dbgsym` 軟體包需要被安裝。（例如，安裝 `coreutils-dbgsym`，用於除錯`coreutils`）原始碼軟體包和普通的二進位制軟體包一起自動生成 `*-dbgsym` 軟體包。那些除錯軟體包將被獨立放在 debian-debug 檔案庫。更多資訊請參閱 Debian Wiki 文件 。

如果一個需要被除錯的軟體包沒有提供其 `*-dbgsym` 軟體包，你需要按如下所示的從原始碼中重構並且安裝它。

    $ mkdir /path/new ; cd /path/new
    $ sudo apt-get update
    $ sudo apt-get dist-upgrade
    $ sudo apt-get install fakeroot devscripts build-essential
    $ apt-get source package_name
    $ cd package_name*
    $ sudo apt-get build-dep ./

按需修改 bug。

軟體包除錯版本跟它的官方 Debian 版本不衝突，例如當重新編譯已存在的軟體包版本產生的 "`+debug1`" 字尾，如下所示是編譯未發行的軟體包版本產生的 "`~pre1`" 字尾。

    $ dch -i

如下所示編譯並安裝帶有除錯符號的軟體包。

    $ export DEB_BUILD_OPTIONS="nostrip noopt"
    $ debuild
    $ cd ..
    $ sudo debi package_name*.changes

你需要檢查軟體包的構建指令碼並確保編譯二進位制的時候使用了 "`CFLAGS=-g -Wall`" 選項。

### 12.5.3. 獲得棧幀

當你碰到程式崩潰的時候，報告 bug 時附上棧幀資訊是個不錯的注意。

使用如下方案之一，可以通過 gdb(1) 取得棧幀資訊：

- 在 GDB 中崩潰的方案：

  - 從 GDB 執行程式。

  - 崩潰程式。

  - 在 GDB 提示符輸入 "`bt`"。

- 先奔潰的方案：

  - 更新“/etc/security/limits.conf”檔案，包括下面內容：

        * soft core unlimited

  - shell 提示符下輸入 "`ulimit -c unlimited`"。

  - 從這個 shell 提示符執行程式。

  - 崩潰的程式產生一個 core dump 檔案。

  - 載入 core dump 檔案到 GDB，用 "`gdb gdb ./program_binary core`" 。

  - 在 GDB 提示符輸入 "`bt`"。

對於無限迴圈或者鍵盤凍結的情況，你可以通過按 `Ctrl-\` 或 `Ctrl-C` 或者執行 “`kill -ABRT PID`” 強制奔潰程式。(參見 節 9.4.12, “殺死一個程序”)

| 提示

通常，你會看到堆疊頂部有一行或者多行有 "`malloc()`" 或 "`g_malloc()`".當這個出現的時候，你的堆疊不是非常有用的。找到一些有用資訊的一個簡單方法是設定環境變數 "`$MALLOC_CHECK_`" 的值為 2 (malloc(3)).你可以通過下面的方式在執行 `gdb` 時設定。

     $ MALLOC_CHECK_=2 gdb hello

### 12.5.4. 高階 gdb 指令

表格 12.14. 高階 gdb 指令列表

指令 | 指令用途的描述
--- | ---
`(gdb) thread apply all bt` | 得到多執行緒程式的所有執行緒棧幀
`(gdb) bt full` | 檢視函式呼叫棧中的參數資訊
`(gdb) thread apply all bt full` | 和前面的選項一起得到堆疊和參數
`(gdb) thread apply all bt full 10` | 得到前10個呼叫的棧幀和參數資訊，以此來去除不相關的輸出
`(gdb) set logging on` | 把 `gdb` 的日誌輸出到檔案 (預設的是 "`gdb.txt`")

### 12.5.5. 檢查庫依賴性

按如下所示使用 ldd(1) 來找出程式的庫依賴性。

    $ ldd /usr/bin/ls
            librt.so.1 => /lib/librt.so.1 (0x4001e000)
            libc.so.6 => /lib/libc.so.6 (0x40030000)
            libpthread.so.0 => /lib/libpthread.so.0 (0x40153000)
            /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

因為 ls(1) 執行在 `chroot`ed 環境，以上的庫在 `chroot`ed 環境也必須是可用的。

參見 節 9.4.6, “跟蹤程式活動”。

### 12.5.6. 動態呼叫跟蹤工具

在 Debian 中，有幾個動態呼叫跟蹤工具存在。參見 節 9.4, “監控、控制和啟動程式活動”。

### 12.5.7. 除錯與 X 相關的錯誤

如果一個 GNOME 程式 `preview1` 收到了一個 X 錯誤，您應當看見一條下面這樣的資訊。

    The program 'preview1' received an X Window System error.

如果就是這種情況，你可以嘗試在執行程式的時候加上 "`--sync`" 選項，並且在 "`gdk_x_error`" 函式處設定中斷來獲得棧幀資訊。

### 12.5.8. 記憶體洩漏檢測工具

Debian 上有一些可用的記憶體洩漏檢測工具。

表格 12.15. 記憶體洩漏檢測工具的列表

軟體包 | 流行度 | 大小 | 說明
--- | --- | --- | ---
`libc6-dev` | V:269, I:584 | 12861 | mtrace(1)：除錯 glibc 中的 malloc
`valgrind` | V:4, I:31 | 89318 | 記憶體偵錯程式和分析器
`electric-fence` | V:0.2, I:2.3 | 69 | `malloc`(e) 偵錯程式
`libdmalloc5` | V:0.01, I:0.61 | 373 | 記憶體分配庫除錯
`duma` | V:0.00, I:0.06 | 297 | 在 C 和 C++ 程式中檢測快取溢位和快取欠載（ buffer under-runs ）的庫
`leaktracer` | V:0.01, I:0.33 | 56 | C++ 程式記憶體洩露跟蹤器

### 12.5.9. 反彙編二進位制程式

你可以使用下面的方式通過 objdump(1) 反編譯二進位制程式碼。

    $  objdump -m i386 -b binary -D /usr/lib/grub/x86_64-pc/stage1

| 注意
gdb(1) 可以用來互動式反彙編程式碼。

## 12.6. 編譯工具

表格 12.16. 編譯工具軟體包列表

軟體包 | 流行度 | 大小 | 包
--- | --- | --- | ---
`make` | V:150, I:566 | 1762 | 通過 `make-doc` 包提供的“`info make`”
`autoconf` | V:29, I:200 | 2244 | 由 `autoconf-doc` 包提供的“`info autoconf`”
`automake` | V:28, I:199 | 1932 | 由 `automake1.10-doc` 包提供的“`info automake`”
`libtool` | V:24, I:183 | 1245 | 由 `libtool-doc` 包提供"`info libtool`"
`cmake` | V:21, I:123 | 59735 | cmake(1) 跨平臺、開源的編譯系統
`ninja-build` | V:9, I:55 | 456 | ninja(1) 接近 Make 精髓的小編譯系統
`meson` | V:6, I:29 | 4278 | meson(1) 在 `ninja` 之上的高生產力的構建系統
`xutils-dev` | V:0.9, I:7.0 | 1495 | imake(1)，xmkmf(1) 等。

### 12.6.1. make

Make 是一個維護程式組的工具。一旦執行 make(1)，`make` 會讀取規則檔案 `Makefile`，自從上次目標檔案被修改後，如果目標檔案依賴的相關檔案發生了改變，那麼就會更新目標檔案，或者目標檔案不存在，那麼這些檔案更新可能會同時發生。

規則檔案的語法如下所示。

    target: [ prerequisites ... ]
     [TAB]  command1
     [TAB]  -command2 # ignore errors
     [TAB]  @command3 # suppress echoing

這裡面的 "`[TAB]`" 是一個 TAB 程式碼。每一行在進行變數替換以後會被 shell 解釋。在行末使用 "`\`" 來繼續此指令碼。使用 "`$$`" 輸入 "`$`" 來獲得 shell 指令碼中的環境變數值。

目標跟相關檔案也可以通過隱式規則給出，例如，如下所示。

    %.o: %.c header.h

在這裡，目標包含了 "`%`" 字元 (只是它們中確切的某一個）。"`%`" 字元能夠匹配實際的目標檔案中任意一個非空的子串。相關檔案同樣使用 "`%`" 來表明它們是怎樣與目標檔案建立聯絡的。

表格 12.17. 自動變數的列表

自動變數 | 值
--- | ---
`$@` | 當前目標
`$<` | 首個相關檔案
`$?` | 所有較新的相關檔案
`$^` | 所有前製必要檔案
`$*` | 目標模式中，$* 指代匹配符 "`%`" 匹配的部分

表格 12.18. 變數擴展的列表

變數擴展 | 說明
--- | ---
`foo1 := bar` | 一次性擴展
`foo2 = bar` | 遞迴擴展
`foo3 += bar` | 增加

執行 "`make -p -f/dev/null`" 指令來檢視內部自動化的規則。

### 12.6.2. Autotools 工具

Autotools 是一套程式設計工具，旨在協助使原始碼包可移植到許多 類 Unix 系統。

- Autoconf 是一個從"`configure.ac`" 生成 shell 指令碼 "`configure`" 的工具。

  - "`configure`" 隨後用於從"`Makefile.in`"模板生成"`Makefile`"。

- Automake 是一個從"`Makefile.am`" 生成"`Makefile.in`" 的工具。

- Libtool 是一個 shell 腳本，用於解決從原始碼編譯動態共享庫時的軟體可移植性問題。

#### 12.6.2.1. 編譯並安裝程式

| 警告
當你安裝編譯好的程式的時候，注意不要覆蓋系統檔案。

Debian 不會在 "`/usr/local`" 或 "`/opt`" 目錄下建立檔案。如果你想要原始碼編譯程式，把它安裝到 "`/usr/local/`" 目錄下，因為這並不會影響到 Debian。

    $ cd src
    $ ./configure --prefix=/usr/local
    $ make # this compiles program
    $ sudo make install # this installs the files in the system

#### 12.6.2.2. 解除安裝程式

如果你有原始碼並且它使用 autoconf(1)/automake(1)，如果你能記得你是怎樣調配它的話，執行如下的指令來解除安裝程式。

    $ ./configure all-of-the-options-you-gave-it
    $ sudo make uninstall

或者，如果你十分確信安裝程序把檔案都放在了 "`/usr/local/`" 下並且這裡沒什麼重要的東西，你可以通過如下的指令來清除它所有的內容。

    # find /usr/local -type f -print0 | xargs -0 rm -f

如果你不確定檔案被安裝到了哪裡，你可以考慮使用 `checkinstall` 軟體包中的 checkinstall(8)，它將會提供一個清晰的解除安裝路徑。現在，它支援建立帶有 “`-D`” 選項的 Debian 軟體包。

### 12.6.3. Meson 軟體

軟體建置系統的進化史：

- Autotools 自 1990 年代以來，一直是可攜式建置基礎設施的事實標準，它建立在 Make 之上。作者認為這非常慢。

- CMake 最初於 2000 年發布，顯著提高了速度，但最初是建立在本質上較慢的 Make 之上。（現在 Ninja 可以作為其後端。）

- Ninja 首次發布於 2012 目的是取代 Make，以進一步提高構建速度，並且設計用於由更高級別的構建系統生成其輸入文件。

- Meson 首次發佈於 2013 的高階建置系統， 它使用 Ninja 作為後端。

參見："The Meson Build system" 與 " Ninja 建置系統".

## 12.7. Web

基本的動態互動網頁可由如下方法制作。

- 呈現給瀏覽器使用者的是 HTML 形式。

- 填充並點選表單條目將會從瀏覽器向 web 伺服器傳送帶有編碼參數的下列 URL 字串之一。

  - "`https://www.foo.dom/cgi-bin/program.pl?VAR1=VAL1&VAR2=VAL2&VAR3=VAL3`"

  - "`https://www.foo.dom/cgi-bin/program.py?VAR1=VAL1&VAR2=VAL2&VAR3=VAL3`"

  - "`https://www.foo.dom/program.php?VAR1=VAL1&VAR2=VAL2&VAR3=VAL3`"

- 在 URL 裡面 "`%nn`" 是使用一個 16 進位制字元的 `nn` 值代替。

- 環境變數設定為: "`QUERY_STRING="VAR1=VAL1 VAR2=VAL2 VAR3=VAL3"`".

- Web伺服器上的CGI程式 (任何一個 "`program.*`")在執行時，都會使用"`$QUERY_STRING`"環境變數.

- CGI 程式的 `stdout`傳送到瀏覽器，作為互動式的動態 web 頁面展示。

出於安全考慮，最好不要自己從頭編寫解析CGI參數的手藝. 在Perl和Python中有現有的模組可以使用. PHP 中包含這些功能. 當需要客戶端資料儲存時, 可使用HTTP cookies . 當需要處理客戶端資料時, 通常使用Javascript.

更多資訊，參見 通用閘道器介面, Apache 軟體基金會, 和 JavaScript.

直接在瀏覽器地址中輸入 https://www.google.com/search?hl=en&ie=UTF-8&q=CGI+tutorial 就可以在 Google 上搜索 “CGI tutorial”。這是在 Google 伺服器上檢視 CGI 指令碼執行的好方法。

## 12.8. 原始碼轉換

原始碼轉換程式。

表格 12.19. 原始碼轉換工具列表

軟體包 | 流行度 | 大小 | 關鍵詞 | 說明
--- | --- | --- | --- | ---
`perl` | V:673, I:992 | 836 | AWK→PERL | 把原始碼從 AWK 轉換為 PERL: a2p(1)
`f2c` | V:0.1, I:2.0 | 443 | FORTRAN→C | 把原始碼從 FORTRAN 77 轉換成 C/C++: f2c(1)
`intel2gas` | V:0.02, I:0.19 | 178 | intel→gas | 從 NASM (Intel 格式)轉換成 GNU 彙編程式(GAS)

## 12.9. 製作 Debian 套件

如果你想製作一個 Debian 套件，閱讀下面內容。

- 章 2, Debian 軟體包管理 理解基本的套件管理系統

- 節 2.7.13, “移植一個軟體包到 stable 系統” 理解基本的移植過程

- 節 9.11.4, “Chroot 系統” 理解基本的 chroot 技術

- debuild(1) 和 sbuild(1)

- 節 12.5.2, “除錯 Debian 軟體包” 編譯和除錯

- Debian 維護者指引 (`debmake-doc` 包)

- Debian 開發者參考手冊 (`developers-reference` 包)

- Debian 策略手冊 (`debian-policy` 包)

`debmake`, `dh-make`, `dh-make-perl` 等軟體包，對軟體包打包過程，也有幫助。

[7] 在當前系統下，為了讓它們工作，需要做一些 調整。

章 11. 資料轉換 |  | 附錄 A. 附錄