第 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:906, I:998 | 207 | 小和快的 POSIX 兼容 shell，用于 `sh`
`coreutils` | V:892, I:999 | 18457 | GNU 核心工具
`grep` | V:765, I:999 | 1297 | GNU `grep`、`egrep` 和 `fgrep`
`sed` | V:804, I:999 | 987 | GNU `sed`
`mawk` | V:466, I:997 | 296 | 小和快的 `awk`
`debianutils` | V:915, I:996 | 225 | 用于 Debian 的各种工具
`bsdutils` | V:439, I:999 | 335 | 来自 4.4BSD-Lite 的基础工具
`bsdextrautils` | V:728, I:849 | 361 | 来自 4.4BSD-Lite 的额外的工具
`moreutils` | V:16, I:37 | 231 | 额外的 Unix 工具

| 提示
尽管 `moreutils` 可能不存在 Debian 之外，但它提供了一些有趣的小程序。最值得注意的一个是 `sponge`(8)，当你想覆盖原来的文件时，它会非常好用。

参见 第 1.6 节 “类 Unix 的文本处理” 的例子。

## 12.2. 解释性语言中的脚本

表 12.8. 解释器相关软件包列表

软件包 | 流行度 | 大小 | 文档
--- | --- | --- | ---
`dash` | V:906, I:998 | 207 | sh: 小和快的 POSIX 兼容的 shell，用于 `sh`
`bash` | V:866, I:999 | 7277 | sh: 由 `bash-doc` 包提供的“`info bash`”
`mawk` | V:466, I:997 | 296 | AWK: 小和快的 `awk`
`gawk` | V:251, I:310 | 3289 | AWK: 由 `gawk-doc` 包提供的“`info gawk`”
`perl` | V:672, I:991 | 841 | Perl: `perl`(1) 以及通过 `perl-doc` 和 `perl-doc-html` 提供的 html 文档
`libterm-readline-gnu-perl` | V:2, I:28 | 439 | GNU ReadLine/History 库的 Perl 扩展： `perlsh`(1)
`libreply-perl` | V:0, I:0 | 171 | Perl 的 REPL : `reply`(1)
`libdevel-repl-perl` | V:0, I:0 | 237 | Perl 的 REPL : `re.pl`(1)
`python3` | V:713, I:970 | 82 | Python: `python3`(1) 以及通过 `python3-doc` 包提供的 html 文档
`tcl` | V:24, I:184 | 20 | Tcl: `tcl`(3) 以及通过 `tcl-doc` 包提供的更详细的手册页文档
`tk` | V:18, I:178 | 20 | Tk:`tk`(3) 以及通过 `tk-doc` 包提供的更详细的手册页文档
`ruby` | V:69, I:166 | 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:227, I:568 | 651 | `xmessage`(1)：在一个窗口中显示一条消息或疑问（X）
`whiptail` | V:298, I:996 | 61 | 从 shell 脚本中显示用户友好的对话框（newt）
`dialog` | V:9, I:82 | 520 | 从 shell 脚本中显示用户友好的对话框（ncurses）
`zenity` | V:67, I:357 | 194 | 从 shell 脚本中显示图形对话框（GTK）
`ssft` | V:0, I:0 | 75 | Shell 脚本前端工具 (zenity, kdialog, and 带有 gettext 的 dialog 封装)
`gettext` | V:53, I:229 | 7165 | “`/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:151, I:561 | 36 | GNU C 编译器
`libc6-dev` | V:268, I:579 | 12694 | GNU C 库：开发库和头文件
`g++` | V:56, I:524 | 13 | GNU C++ 编译器
`libstdc++-14-dev` | V:31, I:227 | 24527 | GNU 标准 C++ 库 版本 3（开发文件）
`cpp` | V:335, I:725 | 18 | GNU C 预处理
`gettext` | V:53, I:229 | 7165 | GNU 国际化工具
`glade` | V:0, I:3 | 1613 | GTK 用户界面构建器
`valac` | V:0, I:3 | 532 | 使用 GObject 系统类似 C# 的语言
`flex` | V:6, I:69 | 1247 | LEX 兼容的 fast lexical analyzer generator
`bison` | V:6, I:73 | 3122 | YACC 兼容的 解析器生成器
`susv2` | I:0 | 16 | 通过“单一UNIX规范（版本2）”获取（英语文档）
`susv3` | I:0 | 16 | 通过“单一UNIX规范（版本3）”获取（英语文档）
`susv4` | I:0 | 16 | 通过“单一UNIX规范（版本4）”获取（英语文档）
`golang` | I:21 | 12 | Go 编程语言编译器
`rustc` | V:5, I:18 | 13748 | Rust 系统编程语言
`gfortran` | V:5, I:52 | 15 | GNU Fortran 95 编译器
`fpc` | I:2 | 104 | 自由 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:6, I:73 | 3122 | GNU LALR 解析器生成器
`byacc` | V:0, I:3 | 263 | 伯克利（Berkeley）LALR 解析器生成器
`btyacc` | V:0, I:0 | 251 | 基于 `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 | 2833 | 用于 Vim 8 和 NeoVim 的异步 Lint 引擎
`vim-syntastic` | I:2 | 1379 | vim 语法检查利器
`elpa-flycheck` | V:0, I:1 | 815 | Emacs 现代实时语法检查
`elpa-relint` | I:0 | 150 | Emacs Lisp 正则错误发现器
`cppcheck-gui` | V:0, I:1 | 7682 | 静态 C/C++ 代码分析工具(GUI)
`shellcheck` | V:2, I:15 | 22859 | shell 脚本的 lint 工具
`pyflakes3` | V:2, I:15 | 20 | Python 3 程序被动检查器
`pylint` | V:4, I:20 | 2089 | Python 代码静态检查器
`perl` | V:672, I:991 | 841 | 带有内部静态代码检测的解释器：`B::Lint`(3perl)
`rubocop` | V:0, I:0 | 3247 | Ruby 静态代码分析器
`clang-tidy` | V:1, I:11 | 22 | 基于 clang 的 C++ 规则格式检查工具
`splint` | V:0, I:1 | 2328 | 静态检查 C 程序 bug 的工具
`flawfinder` | V:0, I:0 | 205 | 检查 C/C++ 源代码和查找安全漏洞的工具
`black` | V:4, I:16 | 10138 | 强硬的 Python 代码格式化器
`perltidy` | V:0, I:3 | 3086 | Perl 脚本缩进和重新格式化
`indent` | V:0, I:5 | 438 | C 语言源代码格式化程序
`astyle` | V:0, I:2 | 769 | C、 C++、 Objective-C、 C# 和 Java 的源代码缩进器
`bcpp` | V:0, I:0 | 114 | 美化 C(++)
`xmlindent` | V:0, I:0 | 52 | XML 流 重新格式化
`global` | V:0, I:1 | 1923 | 源代码检索和浏览工具
`exuberant-ctags` | V:1, I:14 | 341 | 构建源代码定义的标签文件索引
`universal-ctags` | V:1, I:12 | 4238 | 构建源代码定义的标签文件索引

## 12.5. 调试

调试是程序中很重要的一部分。知道怎样去调试程序，能够让你成为一个好的 Debian 使用者, 能够做出有意义的错误报告。

表 12.13. 调试软件包列表

软件包 | 流行度 | 大小 | 文档
--- | --- | --- | ---
`gdb` | V:79, I:156 | 12478 | 由 `gdb-doc` 包提供的“`info gdb`”
`ddd` | V:0, I:5 | 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:268, I:579 | 12694 | `mtrace`(1)：调试 glibc 中的 malloc
`valgrind` | V:6, I:34 | 87847 | 内存调试器和分析器
`electric-fence` | V:0, I:2 | 69 | `malloc`(e) 调试器
`libdmalloc5` | V:0, I:0 | 380 | 内存分配库调试
`duma` | V:0, I:0 | 297 | 在 C 和 C++ 程序中检测缓存溢出和缓存欠载（ buffer under-runs ）的库
`leaktracer` | I:0 | 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:145, I:562 | 1762 | 通过 `make-doc` 包提供“`info make`”
`autoconf` | V:29, I:205 | 2197 | 由 `autoconf-doc` 包提供“`info autoconf`”
`automake` | V:28, I:204 | 1933 | 由 `automake1.10-doc` 包提供“`info automake`”
`libtool` | V:23, I:187 | 1245 | 由 `libtool-doc` 包提供"`info libtool`"
`cmake` | V:18, I:118 | 44267 | `cmake`(1) 跨平台、开源的编译系统
`ninja-build` | V:7, I:51 | 456 | `ninja`(1) 接近 Make 精髓的小编译系统
`meson` | V:6, I:28 | 4186 | `meson`(1) 在 `ninja` 之上的高生产力的构建系统
`xutils-dev` | V:0, I:7 | 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 位于 Make 之上，从 90 年代开始，便是可移植构建架构的事实标准。它是相当慢的。

- CMake 在 2000 年初始发布，显著地改善了速度，但是它源于建立在本质上慢的 Make 之上。 (目前 Ninja 能够作为它的后端。)

- Ninja 在 2012 年初始发布，是为了取代 Make，进一步改善构建速度，在设计上，它的输入文件由上层的构建系统来生成。

- Meson 在 2013 年初始发布，是新的和流行的，并且是快速的和上层的构建系统，它使用 Ninja 作为它的后端。

参见在 "The Meson Build system" 和 "The Ninja build system"里发现的文档。

## 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 参数的手艺. 在 Per l和 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:672, I:991 | 841 | AWK→PERL | 把源代码从 AWK 转换为 PERL: `a2p`(1)
`f2c` | V:0, I:1 | 443 | FORTRAN→C | 把源代码从 FORTRAN 77 转换成 C/C++: `f2c`(1)
`intel2gas` | V:0, I:0 | 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. 附录