章 12. 編程

內容目錄

12.1. Shell 腳本
12.1.1. POSIX shell 兼容性
12.1.2. Shell 參數
12.1.3. Shell 條件語句
12.1.4. shell 迴圈
12.1.5. Shell 環境變數
12.1.6. shell 指令列的處理順序
12.1.7. 用於 shell 指令碼的應用程式
12.2. 解釋性語言中的指令碼
12.2.1. 除錯解釋性語言程式碼
12.2.2. 使用 shell 指令碼的 GUI 程式
12.2.3. 定製 GUI(圖形使用者介面)檔案管理器的行為
12.2.4. Perl 短指令碼的瘋狂
12.3. 編譯型語言程式碼
12.3.1. C
12.3.2. 簡單的 C 程式(gcc)
12.3.3. Flex — 一個更好的 Lex
12.3.4. Bison — 一個更好的 Yacc
12.4. 靜態程式碼分析工具
12.5. 除錯
12.5.1. 基本的 gdb 使用指令
12.5.2. 除錯 Debian 軟體包
12.5.3. 獲得棧幀
12.5.4. 高階 gdb 指令
12.5.5. 檢查庫依賴性
12.5.6. 動態呼叫跟蹤工具
12.5.7. 除錯與 X 相關的錯誤
12.5.8. 記憶體洩漏檢測工具
12.5.9. 反彙編二進位制程式
12.6. 編譯工具
12.6.1. make
12.6.2. Autotools 工具
12.6.2.1. 編譯並安裝程式
12.6.2.2. 解除安裝程式
12.6.3. Meson 軟體
12.7. Web
12.8. 原始碼轉換
12.9. 製作 Debian 套件

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

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

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

[警告] 警告

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

[注意] 注意

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

[提示] 提示

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

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 腳本會頻繁使用參數、條件和循環等。

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

  • 成功: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”選項而被呼叫時不會在該行意外地退出。



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

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 顯示出所有執行的指令。這對除錯來說是非常方便的。


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

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

  • 使用 python3,如果任務不是簡單的,你從零開始寫。

  • 使用 perltclruby……,如果在 Debian 上有用這些語言寫的現存程式碼,需要為完成任務進行調整。

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

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


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

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

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

#!/bin/sh -e
# Copyright (C) 2021 Osamu Aoki <[email protected]>, 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.3.3, “Flex — 一個更好的 Lex”節 12.3.4, “Bison — 一個更好的 Yacc”,用來說明 類似編譯器的程式怎樣用C 語言來編寫,是透過編譯高階描述到 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)

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

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

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

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


可以使用 “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

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

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

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

[提示] 提示

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


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


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 一樣都能工作。

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" 選項。

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

使用如下方案之一,可以透過 gdb(1) 取得棧幀資訊:

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

[提示] 提示

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

 $ MALLOC_CHECK_=2 gdb hello

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

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



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

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

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

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

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

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

軟體建置系統的進化史:

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

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

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

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

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

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

  • 呈現給瀏覽器使用者的是 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 指令碼執行的好方法。

原始碼轉換程式。


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

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



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