借鉴 C 语言的历史，学习如何用 Python 编写有用的 CLI 程序。本文的目标很简单：帮助新的 Python 开发者了解一些关于命令行接口（CLI）的历史和术语，并探讨如何在 Python 中编写这些有用的程序。最初……首先，从 Unix 的角度谈谈命令行界面设计。Unix 是一种计算机操作系统，也是 Linux 和 macOS（以及许多其他操作系统）的祖先。在图形用户界面之前，用户通过命令行提示符与计算机进行交互（想想如今的 Bash 环境）。在 Unix 下开发这些程序的主要语言是 C)，它的功能非常强大。因此，我们至少应该了解 C 程序的基础知识。假设你没有读过上面那个链接的内容，C 程序的基本架构是一个叫做 main 的函数，它的签名是这样的。 int main(int argc, char **argv) { ... }对于 Python 程序员来说，这应该不会显得太奇怪。C 函数首先有一个返回类型、一个函数名，然后是括号内的类型化参数。最后，函数的主体位于大括号之间。函数名 main 是运行时链接器（构造和运行程序的程序）如何决定从哪里开始执行你的程序。如果你写了一个 C 程序，而它没有包含一个名为 main 的函数，它将什么也做不了。伤心。函数参数变量 argc 和 argv 共同描述了程序被调用时用户在命令行输入的字符串列表。在典型的 Unix 命名传统中，argc 的意思是“ 参数计数 argument count ”，argv 的意思是“ 参数向量 argument vector ”。向量听起来比列表更酷，而 argl 听起来就像一个要勒死的求救声。我们是 Unix 系统的程序员，我们不求救。我们让其他人哭着求救。再进一步$ ./myprog foo bar -x baz如果 myprog 是用 C 语言实现的，则 argc 的值是 5，而 argv 是一个有五个条目的字符指针数组。（不要担心，如果这听起来过于技术，那换句话说，这是一个由五个字符串组成的列表。）向量中的第一个条目 argv[0] 是程序的名称。argv 的其余部分包含参数。 argv[0] == "./myprog" argv[1] == "foo" argv[2] == "bar" argv[3] == "-x" argv[4] == "baz" /* 注：不是有效的 C 代码 */在 C 语言中，你有很多方法来处理 argv 中的字符串。你可以手动地循环处理数组 argv，并根据程序的需要解释每个字符串。这相对来说比较简单，但会导致程序的接口大相径庭，因为不同的程序员对什么是“好”有不同的想法。include <stdio.h> /* 一个打印 argv 内容的简单 C 程序。 */ int main(int argc, char **argv) { int i; for(i=0; i<argc; i++) printf("%s\n", argv[i]); }早期对命令行标准化的尝试命令行武器库中的下一个武器是一个叫做 getopt 的 C 标准库函数。这个函数允许程序员解析开关，即前面带破折号的参数（比如 -x），并且可以选择将后续参数与它们的开关配对。想想 /bin/ls -alSh 这样的命令调用，getopt 就是最初用来解析该参数串的函数。使用 getopt 使命令行的解析变得相当简单，并改善了用户体验（UX）。include <stdio.h> #include <getopt.h> #define OPTSTR "b:f:" extern char *optarg; int main(int argc, char **argv) { int opt; char *bar = NULL; char *foo = NULL; while((opt=getopt(argc, argv, OPTSTR)) != EOF) switch(opt) { case 'b': bar = optarg; break; case 'f': foo = optarg; break; case 'h': default': fprintf(stderr, "Huh? try again."); exit(-1); /* NOTREACHED */ } printf("%s\n", foo ? foo : "Empty foo"); printf("%s\n", bar ? bar : "Empty bar"); }就个人而言，我希望 Python 有开关，但这永远、永远不会发生。GNU 时代GNU 项目出现了，并为他们实现的传统 Unix 命令行工具引入了更长的格式参数，比如--file-format foo。当然，我们这些 Unix 程序员很讨厌这样，因为打字太麻烦了，但是就像我们这些旧时代的恐龙一样，我们输了，因为用户喜欢更长的选项。我从来没有写过任何使用 GNU 风格选项解析的代码，所以这里没有代码示例。GNU 风格的参数也接受像 -f foo 这样的短名，也必须支持。所有这些选择都给程序员带来了更多的工作量，因为他们只想知道用户要求的是什么，然后继续进行下去。但用户得到了更一致的用户体验：长格式选项、短格式选项和自动生成的帮助，使用户不必再试图阅读臭名昭著的难以解析的手册页面（参见 ps 这个特别糟糕的例子）。但我们正在讨论 Python？你现在已经接触了足够多（太多？）的命令行的历史，对如何用我们最喜欢的语言来编写 CLI 有了一些背景知识。Python 在命令行解析方面给出了类似的几个选择：自己解析， 自给自足 batteries-included 的方式，以及大量的第三方方式。你选择哪一种取决于你的特定情况和需求。首先，自己解析你可以从 sys 模块中获取程序的参数。import sys if __name__ == '__main__': for value in sys.argv: print(value)自给自足在 Python 标准库中已经有几个参数解析模块的实现：getopt、optparse，以及最近的 argparse。argparse 允许程序员为用户提供一致的、有帮助的用户体验，但就像它的 GNU 前辈一样，它需要程序员做大量的工作和“模板代码”才能使它“奏效”。from argparse import ArgumentParser if __name__ == "__main__": argparser = ArgumentParser(description='My Cool Program') argparser.add_argument("--foo", "-f", help="A user supplied foo") argparser.add_argument("--bar", "-b", help="A user supplied bar") results = argparser.parse_args() print(results.foo, results.bar)好处是当用户调用 --help 时，有自动生成的帮助。但是 自给自足 batteries included 的优势呢？有时，你的项目情况决定了你对第三方库的访问是有限的，或者说是没有，你不得不用 Python 标准库来“凑合”。CLI 的现代方法然后是 Click。Click 框架使用装饰器的方式来构建命令行解析。突然间，写一个丰富的命令行界面变得有趣而简单。在装饰器的酷炫和未来感的使用下，很多复杂的东西都消失了，用户惊叹于自动支持关键字补完以及上下文帮助。所有这些都比以前的解决方案写的代码更少。任何时候，只要你能写更少的代码，还能把事情做好，就是一种胜利。而我们都想要胜利。import click @click.command() @click.option("-f", "--foo", default="foo", help="User supplied foo.") @click.option("-b", "--bar", default="bar", help="User supplied bar.") def echo(foo, bar): """My Cool Program It does stuff. Here is the documentation for it. """ print(foo, bar) if __name__ == "__main__": echo()你可以在 @click.option 装饰器中看到一些与 argparse 相同的模板代码。但是创建和管理参数分析器的“工作”已经被抽象化了。现在，命令行参数被解析，而值被赋给函数参数，从而函数 echo 被魔法般地调用。在 Click 接口中添加参数就像在堆栈中添加另一个装饰符并将新的参数添加到函数定义中一样简单。但是，等等，还有更多!Typer 建立在 Click 之上，是一个更新的 CLI 框架，它结合了 Click 的功能和现代 Python 类型提示。使用 Click 的缺点之一是必须在函数中添加一堆装饰符。CLI 参数必须在两个地方指定：装饰符和函数参数列表。Typer 免去你造轮子 去写 CLI 规范，让代码更容易阅读和维护。import typer cli = typer.Typer() @cli.command() def echo(foo: str = "foo", bar: str = "bar"): """My Cool Program It does stuff. Here is the documentation for it. """ print(foo, bar) if __name__ == "__main__": cli()是时候开始写一些代码了哪种方法是正确的？这取决于你的用例。你是在写一个只有你才会使用的快速而粗略的脚本吗？直接使用 sys.argv 然后继续编码。你需要更强大的命令行解析吗？也许 argparse 就够了。你是否有很多子命令和复杂的选项，你的团队是否会每天使用它？现在你一定要考虑一下 Click 或 Typer。作为一个程序员的乐趣之一就是魔改出替代实现，看看哪一个最适合你。最后，在 Python 中有很多用于解析命令行参数的第三方软件包。我只介绍了我喜欢或使用过的那些。你喜欢和/或使用不同的包是完全可以的，也是我们所期望的。我的建议是先从这些包开始，然后看看你最终的结果。去写一些很酷的东西吧。via: https://opensource.com/article/20/6/c-python-cli作者：Erik O'Shaughnessy 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
使用 Pelican 创建博客，这是一个基于 Python 的平台，与 GitHub 配合的不错。GitHub 是一个非常流行的用于源代码控制的 Web 服务，它使用 Git 同步本地文件和 GitHub 服务器上保留的副本，这样你就可以轻松地共享和备份你的工作。除了为代码仓库提供用户界面之外，GitHub 还运允许用户直接从仓库发布网页。GitHub 推荐的网站生成软件包是 Jekll，是使用 Ruby 编写的。因为我是 Python 的忠实粉丝，所以我更喜欢 Pelican，这是一个基于 Python 的博客平台，可与 GitHub 很好地协同工作。Pelican 和 Jekll 都可以将 Markdown 或 reStructuredText 中编写的内容转换为 HTML 以生成静态网站，并且两个生成器都支持定制的主题。在本文中，我将介绍如何安装 Pelican、设置 GitHub 仓库、运行快速入门帮助、编写一些 Markdown 文件以及发布第一篇博客。我假设你有一个 GitHub 账户，熟悉基础的 Git 命令，并且想使用 Pelican 发布博客。安装 Pelican 并创建仓库首先，你必须在本地计算机上安装 Pelican 和 ghp-import。使用 Python 软件包安装工具 pip（你有，对吧？），这非常容易：$ pip install pelican ghp-import Markdown然后，打开浏览器并在 GitHub 上为你新鲜出炉的博客创建一个新仓库，命名如下（在此处以及整个教程中，用 GitHub 用户名替换 username）：https://GitHub.com/username/username.github.io让它保持为空，稍后我们用引人注目的博客内容来填充它。使用命令行（确保正确），将这个空 Git 仓库克隆到本地计算机：$ git clone <https://GitHub.com/username/username.github.io> blog $ cd blog奇怪的把戏…在 GitHub 上发布 Web 内容有一个不太引入注意的技巧，对于托管在名为 username.github.io 的仓库的用户页面，其内容由 master 分支提供服务。我强烈建议所有的 Pelican 配置文件和原始的 Markdown 文件都不要保留在 master 中，master 中只保留 Web 内容。因此，我将 Pelican 配置和原始内容保留在一个我喜欢称为 content 的单独分支中。（你可以随意创建一个分支，但以下内容沿用 content。）我喜欢这种结构，因为我可以放弃掉 master 中的所有文件，然后用 content 分支重新填充它。$ git checkout -b content Switched to a new branch 'content'配置 Pelican现在该进行内容配置了。Pelican 提供了一个很棒的初始化工具 pelican-quickstart，它会询问你有关博客的一系列问题。$ pelican-quickstart Welcome to pelican-quickstart v3.7.1. This script will help you create a new Pelican-based website. Please answer the following questions so this script can generate the files needed by Pelican. > Where do you want to create your new web site? [.] > What will be the title of this web site? Super blog > Who will be the author of this web site? username > What will be the default language of this web site? [en] > Do you want to specify a URL prefix? e.g., http://example.com (Y/n) n > Do you want to enable article pagination? (Y/n) > How many articles per page do you want? [10] > What is your time zone? [Europe/Paris] US/Central > Do you want to generate a Fabfile/Makefile to automate generation and publishing? (Y/n) y > Do you want an auto-reload & simpleHTTP script to assist with theme and site development? (Y/n) y > Do you want to upload your website using FTP? (y/N) n > Do you want to upload your website using SSH? (y/N) n > Do you want to upload your website using Dropbox? (y/N) n > Do you want to upload your website using S3? (y/N) n > Do you want to upload your website using Rackspace Cloud Files? (y/N) n > Do you want to upload your website using GitHub Pages? (y/N) y > Is this your personal page (username.github.io)? (y/N) y Done. Your new project is available at /Users/username/blog你可以对每个问题都采用默认值，但除了以下这些问题：网站标题，应该唯一且特殊网站作者，可以是个人用户名或你的全名时区，可能你不在巴黎上传到 GitHub 页面，我们选择 y回答完所有问题后，Pelican 会在当前目录中留下以下内容：$ ls Makefile content/ develop_server.sh* fabfile.py output/ pelicanconf.py publishconf.py你可以查看 Pelican 文档来了解如何使用这些文件，但现在我们要做的是完成手头的工作。说实话，我也没有阅读文档。继续将所有 Pelican 生成的文件添加到本地 Git 仓库的 content 分支，提交更改，然后将本地更改推送到 Github 上托管的远程仓库：$ git add . $ git commit -m 'initial pelican commit to content' $ git push origin content这件事情并不是特别令人兴奋，但是如果我们需要撤销这些文件之一的修改时，这将非常方便。终于终于，现在你得到一个博客了！你所有的博客文章、照片、图像、PDF 等都将位于 content 目录中，它最初是空的。要开始创建第一篇博客和关于页面，输入：$ cd content $ mkdir pages images $ cp /Users/username/SecretStash/HotPhotoOfMe.jpg images $ touch first-post.md $ touch pages/about.md接下来，在你喜欢的文本编辑器中打开 first-post.md，并添加以下内容：title: First Post on My Sweet New Blog date: <today's date> author: Your Name Here # I am On My Way To Internet Fame and Fortune! This is my first post on my new blog. While not super informative it should convey my sense of excitement and eagerness to engage with you, the reader!前三行是 Pelican 用于组织内容的元数据。有很多不同的元数据可供你选择。再说一次，文档是你了解更多选项的最佳选择。现在，打开空白文件 pages/about.md 并添加以下文本：title: About date: <today's date> ![So Schmexy][my_sweet_photo] Hi, I am <username> and I wrote this epic collection of Interweb wisdom. In days of yore, much of this would have been deemed sorcery and I would probably have been burned at the stake. ? 现在，content 目录中将包含三个新的 Web 内容，在 content 分支中还有很多内容。发布不要急，马上要见到成果了！剩下要做的就是：运行 Pelican 以在 output 中生成静态 HTML 文件：$ pelican content -o output -s publishconf.py使用 ghp-import 将 output 目录的内容添加到 master 分支中：$ ghp-import -m "Generate Pelican site" --no-jekyll -b master output将本地 master 分支推送到远程仓库：$ git push origin master提交新内容并将其推送到 content 分支$ git add content $ git commit -m 'added a first post, a photo and an about page' $ git push origin contentOMG，我成功了现在最激动的时候到了，当你想要看到你发布给大家的博客内容时，打开浏览器输入：https://username.github.io恭喜你可以在 GitHub 上发布自己的博客了！当你想添加更多页面或文章时，都可以按照上面的步骤来。希望你可以愉快地发布博客。via: https://opensource.com/article/19/5/run-your-blog-github-pages-python作者：Erik O'Shaughnessy 选题：lujun9972 译者：MjSeven 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
我将在本系列的第二篇中深入研究由多个文件组成的 C 程序的结构。在第一篇中，我设计了一个名为喵呜喵呜的多文件 C 程序，该程序实现了一个玩具编解码器。我也提到了程序设计中的 Unix 哲学，即在一开始创建多个空文件，并建立一个好的结构。最后，我创建了一个 Makefile 文件夹并阐述了它的作用。在本文中将另一个方向展开：现在我将介绍简单但具有指导性的喵呜喵呜编解码器的实现。当读过我的《如何写一个好的 C 语言 main 函数》后，你会觉得喵呜喵呜编解码器的 main.c 文件的结构很熟悉，其主体结构如下：/* main.c - 喵呜喵呜流式编解码器 */ /* 00 系统包含文件 */ /* 01 项目包含文件 */ /* 02 外部声明 */ /* 03 定义 */ /* 04 类型定义 */ /* 05 全局变量声明（不要用）*/ /* 06 附加的函数原型 */ int main(int argc, char *argv[]) { /* 07 变量声明 */ /* 08 检查 argv[0] 以查看该程序是被如何调用的 */ /* 09 处理来自用户的命令行选项 */ /* 10 做点有用的事情 */ } /* 11 其它辅助函数 */包含项目头文件位于第二部分中的 /* 01 项目包含文件 */ 的源代码如下：/* main.c - 喵呜喵呜流式编解码器 */ ... /* 01 项目包含文件 */ #include "main.h" #include "mmecode.h" #include "mmdecode.h"#include 是 C 语言的预处理命令，它会将该文件名的文件内容拷贝到当前文件中。如果程序员在头文件名称周围使用双引号（""），编译器将会在当前目录寻找该文件。如果文件被尖括号包围（<>），编译器将在一组预定义的目录中查找该文件。main.h 文件中包含了 main.c 文件中用到的定义和类型定义。我喜欢尽可能多将声明放在头文件里，以便我在我的程序的其他位置使用这些定义。头文件 mmencode.h 和 mmdecode.h 几乎相同，因此我以 mmencode.h 为例来分析。/* mmencode.h - 喵呜喵呜流编解码器 */ #ifndef _MMENCODE_H #define _MMENCODE_H #include <stdio.h> int mm_encode(FILE *src, FILE *dst); #endif /* _MMENCODE_H */#ifdef、#define、#endif 指令统称为 “防护” 指令。其可以防止 C 编译器在一个文件中多次包含同一文件。如果编译器在一个文件中发现多个定义/原型/声明，它将会产生警告。因此这些防护措施是必要的。在这些防护内部，只有两个东西：#include 指令和函数原型声明。我在这里包含了 stdio.h 头文件，以便于能在函数原型中使用 FILE 定义。函数原型也可以被包含在其他 C 文件中，以便于在文件的命名空间中创建它。你可以将每个文件视为一个独立的命名空间，其中的变量和函数不能被另一个文件中的函数或者变量使用。编写头文件很复杂，并且在大型项目中很难管理它。不要忘记使用防护。喵呜喵呜编码的最终实现该程序的功能是按照字节进行 MeowMeow 字符串的编解码，事实上这是该项目中最简单的部分。截止目前我所做的工作便是支持允许在适当的位置调用此函数：解析命令行，确定要使用的操作，并打开将要操作的文件。下面的循环是编码的过程：/* mmencode.c - 喵呜喵呜流式编解码器 */ ... while (!feof(src)) { if (!fgets(buf, sizeof(buf), src)) break; for(i=0; i<strlen(buf); i++) { lo = (buf[i] & 0x000f); hi = (buf[i] & 0x00f0) >> 4; fputs(tbl[hi], dst); fputs(tbl[lo], dst); } }简单的说，当文件中还有数据块时（ feof(3) ），该循环读取（feof(3) ）文件中的一个数据块。然后将读入的内容的每个字节分成两个 hi 和 lo 的 半字节 nibble 。半字节是半个字节，即 4 个位。这里的奥妙之处在于可以用 4 个位来编码 16 个值。我将 hi 和 lo 用作 16 个字符串查找表 tbl 的索引，表中包含了用半字节编码的 MeowMeow 字符串。这些字符串使用 fputs(3) 函数写入目标 FILE 流，然后我们继续处理缓存区的下一个字节。该表使用 table.h 中的宏定义进行初始化，在没有特殊原因（比如：要展示包含了另一个项目的本地头文件）时，我喜欢使用宏来进行初始化。我将在未来的文章中进一步探讨原因。喵呜喵呜解码的实现我承认在开始工作前花了一些时间。解码的循环与编码类似：读取 MeowMeow 字符串到缓冲区，将编码从字符串转换为字节 /* mmdecode.c - 喵呜喵呜流式编解码器 */ ... int mm_decode(FILE *src, FILE *dst) { if (!src || !dst) { errno = EINVAL; return -1; } return stupid_decode(src, dst); }这不符合你的期望吗？在这里，我通过外部公开的 mm_decode() 函数公开了 stupid_decode() 函数细节。我上面所说的“外部”是指在这个文件之外。因为 stupid_decode() 函数不在该头文件中，因此无法在其他文件中调用它。当我们想发布一个可靠的公共接口时，有时候会这样做，但是我们还没有完全使用函数解决问题。在本例中，我编写了一个 I/O 密集型函数，该函数每次从源中读取 8 个字节，然后解码获得 1 个字节写入目标流中。较好的实现是一次处理多于 8 个字节的缓冲区。更好的实现还可以通过缓冲区输出字节，进而减少目标流中单字节的写入次数。/* mmdecode.c - 喵呜喵呜流式编解码器 */ ... int stupid_decode(FILE *src, FILE *dst) { char buf[9]; decoded_byte_t byte; int i; while (!feof(src)) { if (!fgets(buf, sizeof(buf), src)) break; byte.field.f0 = isupper(buf[0]); byte.field.f1 = isupper(buf[1]); byte.field.f2 = isupper(buf[2]); byte.field.f3 = isupper(buf[3]); byte.field.f4 = isupper(buf[4]); byte.field.f5 = isupper(buf[5]); byte.field.f6 = isupper(buf[6]); byte.field.f7 = isupper(buf[7]); fputc(byte.value, dst); } return 0; }我并没有使用编码器中使用的位移方法，而是创建了一个名为 decoded_byte_t 的自定义数据结构。/* mmdecode.c - 喵呜喵呜流式编解码器 */ ... typedef struct { unsigned char f7:1; unsigned char f6:1; unsigned char f5:1; unsigned char f4:1; unsigned char f3:1; unsigned char f2:1; unsigned char f1:1; unsigned char f0:1; } fields_t; typedef union { fields_t field; unsigned char value; } decoded_byte_t;初次看到代码时可能会感到有点儿复杂，但不要放弃。decoded_byte_t 被定义为 fields_t 和 unsigned char 的 联合。可以将联合中的命名成员看作同一内存区域的别名。在这种情况下，value 和 field 指向相同的 8 位内存区域。将 field.f0 设置为 1 也将会设置 value 中的最低有效位。虽然 unsigned char 并不神秘，但是对 fields_t 的类型定义（typedef）也许看起来有些陌生。现代 C 编译器允许程序员在结构体中指定单个位字段的值。字段所在的类型是一个无符号整数类型，并在成员标识符后紧跟一个冒号和一个整数，该整数指定了位字段的长度。这种数据结构使得按字段名称访问字节中的每个位变得简单，并可以通过联合中的 value 字段访问组合后的值。我们依赖编译器生成正确的移位指令来访问字段，这可以在调试时为你节省不少时间。最后，因为 stupid_decode() 函数一次仅从源 FILE 流中读取 8 个字节，所以它效率并不高。通常我们尝试最小化读写次数，以提高性能和降低调用系统调用的开销。请记住：少量的读取/写入大的块比大量的读取/写入小的块好得多。总结用 C 语言编写一个多文件程序需要程序员要比只是是一个 main.c 做更多的规划。但是当你添加功能或者重构时，只需要多花费一点儿努力便可以节省大量时间以及避免让你头痛的问题。回顾一下，我更喜欢这样做：多个文件，每个文件仅有简单功能；通过头文件公开那些文件中的小部分功能；把数字常量和字符串常量保存在头文件中；使用 Makefile 而不是 Bash 脚本来自动化处理事务；使用 main() 函数来处理命令行参数解析并作为程序主要功能的框架。我知道我只是蜻蜓点水般介绍了这个简单的程序，并且我很高兴知道哪些事情对你有所帮助，以及哪些主题需要详细的解释。请在评论中分享你的想法，让我知道。via: https://opensource.com/article/19/7/structure-multi-file-c-part-2作者：Erik O'Shaughnessy 选题：lujun9972 译者：萌新阿岩 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
准备好你喜欢的饮料、编辑器和编译器，放一些音乐，然后开始构建一个由多个文件组成的 C 语言程序。大家常说计算机编程的艺术部分是处理复杂性，部分是命名某些事物。此外，我认为“有时需要添加绘图”是在很大程度上是正确的。在这篇文章里，我会编写一个小型 C 程序，命名一些东西，同时处理一些复杂性。该程序的结构大致基于我在 《如何写一个好的 C 语言 main 函数》 文中讨论的。但是，这次做一些不同的事。准备好你喜欢的饮料、编辑器和编译器，放一些音乐，让我们一起编写一个有趣的 C 语言程序。优秀 Unix 程序哲学首先，你要知道这个 C 程序是一个 Unix 命令行工具。这意味着它运行在（或者可被移植到）那些提供 Unix C 运行环境的操作系统中。当贝尔实验室发明 Unix 后，它从一开始便充满了设计哲学。用我自己的话来说就是：程序只做一件事，并做好它，并且对文件进行一些操作。虽然“只做一件事，并做好它”是有意义的，但是“对文件进行一些操作”的部分似乎有点儿不合适。事实证明，Unix 中抽象的 “文件” 非常强大。一个 Unix 文件是以文件结束符（EOF）标志为结尾的字节流。仅此而已。文件中任何其它结构均由应用程序所施加而非操作系统。操作系统提供了系统调用，使得程序能够对文件执行一套标准的操作：打开、读取、写入、寻址和关闭（还有其他，但说起来那就复杂了）。对于文件的标准化访问使得不同的程序共用相同的抽象，而且可以一同工作，即使它们是不同的人用不同语言编写的程序。具有共享的文件接口使得构建可组合的的程序成为可能。一个程序的输出可以作为另一个程序的输入。Unix 家族的操作系统默认在执行程序时提供了三个文件：标准输入（stdin）、标准输出（stdout）和标准错误（stderr）。其中两个文件是只写的：stdout 和 stderr。而 stdin 是只读的。当我们在常见的 Shell 比如 Bash 中使用文件重定向时，可以看到其效果。$ ls | grep foo | sed -e 's/bar/baz/g' > ack这条指令可以被简要地描述为：ls 的结果被写入标准输出，它重定向到 grep 的标准输入，grep 的标准输出重定向到 sed 的标准输入，sed 的标准输出重定向到当前目录下文件名为 ack 的文件中。我们希望我们的程序在这个灵活又出色的生态系统中运作良好，因此让我们编写一个可以读写文件的程序。喵呜喵呜：流编码器/解码器概念当我还是一个露着豁牙的孩子懵懵懂懂地学习计算机科学时，学过很多编码方案。它们中的有些用于压缩文件，有些用于打包文件，另一些毫无用处因此显得十分愚蠢。列举最后这种情况的一个例子：哞哞编码方案。为了让我们的程序有个用途，我为它更新了一个 21 世纪 的概念，并且实现了一个名为“喵呜喵呜” 的编码方案的概念（毕竟网上大家都喜欢猫）。这里的基本的思路是获取文件并且使用文本 “meow” 对每个半字节（半个字节）进行编码。小写字母代表 0，大写字母代表 1。因为它会将 4 个比特替换为 32 个比特，因此会扩大文件的大小。没错，这毫无意义。但是想象一下人们看到经过这样编码后的惊讶表情。$ cat /home/your_sibling/.super_secret_journal_of_my_innermost_thoughts MeOWmeOWmeowMEoW...这非常棒。最终的实现完整的源代码可以在 GitHub 上面找到，但是我会写下我在编写程序时的思考。目的是说明如何组织构建多文件 C 语言程序。既然已经确定了要编写一个编码和解码“喵呜喵呜”格式的文件的程序时，我在 Shell 中执行了以下的命令 ：$ mkdir meowmeow $ cd meowmeow $ git init $ touch Makefile # 编译程序的方法 $ touch main.c # 处理命令行选项 $ touch main.h # “全局”常量和定义 $ touch mmencode.c # 实现对喵呜喵呜文件的编码 $ touch mmencode.h # 描述编码 API $ touch mmdecode.c # 实现对喵呜喵呜文件的解码 $ touch mmdecode.h # 描述解码 API $ touch table.h # 定义编码查找表 $ touch .gitignore # 这个文件中的文件名会被 git 忽略 $ git add . $ git commit -m "initial commit of empty files"简单的说，我创建了一个目录，里面全是空文件，并且提交到 git。即使这些文件中没有内容，你依旧可以从它的文件名推断每个文件的用途。为了避免万一你无法理解，我在每条 touch 命令后面进行了简单描述。通常，程序从一个简单 main.c 文件开始，只有两三个解决问题的函数。然后程序员轻率地向自己的朋友或者老板展示了该程序，然后为了支持所有新的“功能”和“需求”，文件中的函数数量就迅速爆开了。“程序俱乐部”的第一条规则便是不要谈论“程序俱乐部”，第二条规则是尽量减少单个文件中的函数。老实说，C 编译器并不关心程序中的所有函数是否都在一个文件中。但是我们并不是为计算机或编译器写程序，我们是为其他人（有时也包括我们）去写程序的。我知道这可能有些奇怪，但这就是事实。程序体现了计算机解决问题所采用的一组算法，当问题的参数发生了意料之外的变化时，保证人们可以理解它们是非常重要的。当在人们修改程序时，发现一个文件中有 2049 函数时他们会诅咒你的。因此，优秀的程序员会将函数分隔开，将相似的函数分组到不同的文件中。这里我用了三个文件 main.c、mmencode.c 和 mmdecode.c。对于这样小的程序，也许看起来有些过头了。但是小的程序很难保证一直小下去，因此哥忒拓展做好计划是一个“好主意”。但是那些 .h 文件呢？我会在后面解释一般的术语，简单地说，它们被称为头文件，同时它们可以包含 C 语言类型定义和 C 预处理指令。头文件中不应该包含任何函数。你可以认为头文件是提供了应用程序接口（API）的定义的一种 .c 文件，可以供其它 .c 文件使用。但是 Makefile 是什么呢？我知道下一个轰动一时的应用都是你们这些好孩子们用 “终极代码粉碎者 3000” 集成开发环境来编写的，而构建项目是用 Ctrl-Meta-Shift-Alt-Super-B 等一系列复杂的按键混搭出来的。但是如今（也就是今天），使用 Makefile 文件可以在构建 C 程序时帮助做很多有用的工作。Makefile 是一个包含如何处理文件的方式的文本文件，程序员可以使用其自动地从源代码构建二进制程序（以及其它东西！）以下面这个小东西为例：00 # Makefile 01 TARGET= my_sweet_program 02 $(TARGET): main.c 03 cc -o my_sweet_program main.c# 符号后面的文本是注释，例如 00 行。01 行是一个变量赋值，将 TARGET 变量赋值为字符串 my_sweet_program。按照惯例，也是我的习惯，所有 Makefile 变量均使用大写字母并用下划线分隔单词。02 行包含该 步骤 recipe 要创建的文件名和其依赖的文件。在本例中，构建 目标 target 是 my_sweet_program，其依赖是 main.c。最后的 03 行使用了一个制表符号（tab）而不是四个空格。这是将要执行创建目标的命令。在本例中，我们使用 C 编译器 C compiler 前端 cc 以编译链接为 my_sweet_program。使用 Makefile 是非常简单的。$ make cc -o my_sweet_program main.c $ ls Makefile main.c my_sweet_program构建我们喵呜喵呜编码器/解码器的 Makefile 比上面的例子要复杂，但其基本结构是相同的。我将在另一篇文章中将其分解为 Barney 风格。形式伴随着功能我的想法是程序从一个文件中读取、转换它，并将转换后的结果存储到另一个文件中。以下是我想象使用程序命令行交互时的情况：$ meow < clear.txt > clear.meow $ unmeow < clear.meow > meow.tx $ diff clear.txt meow.tx $我们需要编写代码以进行命令行解析和处理输入/输出流。我们需要一个函数对流进行编码并将结果写到另一个流中。最后，我们需要一个函数对流进行解码并将结果写到另一个流中。等一下，我们在讨论如何写一个程序，但是在上面的例子中，我调用了两个指令：meow 和 unmeow？我知道你可能会认为这会导致越变越复杂。次要内容：argv[0] 和 ln 指令回想一下，C 语言 main 函数的结构如下：int main(int argc, char *argv[])其中 argc 是命令行参数的数量，argv 是字符指针（字符串）的列表。argv[0] 是包含正在执行的程序的文件路径。在 Unix 系统中许多互补功能的程序（比如：压缩和解压缩）看起来像两个命令，但事实上，它们是在文件系统中拥有两个名称的一个程序。这个技巧是通过使用 ln 命令创建文件系统链接来实现两个名称的。在我笔记本电脑中 /usr/bin 的一个例子如下：$ ls -li /usr/bin/git* 3376 -rwxr-xr-x. 113 root root 1.5M Aug 30 2018 /usr/bin/git 3376 -rwxr-xr-x. 113 root root 1.5M Aug 30 2018 /usr/bin/git-receive-pack ...这里 git 和 git-receive-pack 是同一个文件但是拥有不同的名字。我们说它们是相同的文件因为它们具有相同的 inode 值（第一列）。inode 是 Unix 文件系统的一个特点，对它的介绍超越了本文的内容范畴。优秀或懒惰的程序可以通过 Unix 文件系统的这个特点达到写更少的代码但是交付双倍的程序。首先，我们编写一个基于其 argv[0] 的值而作出相应改变的程序，然后我们确保为导致该行为的名称创建链接。在我们的 Makefile 中，unmeow 链接通过以下的方式来创建：# Makefile ... $(DECODER): $(ENCODER) $(LN) -f $< $@ ...我倾向于在 Makefile 中将所有内容参数化，很少使用 “裸” 字符串。我将所有的定义都放置在 Makefile 文件顶部，以便可以简单地找到并改变它们。当你尝试将程序移植到新的平台上时，需要将 cc 改变为某个 cc 时，这会很方便。除了两个内置变量 $@ 和 $< 之外，该 步骤 recipe 看起来相对简单。第一个便是该步骤的目标的快捷方式，在本例中是 $(DECODER)（我能记得这个是因为 @ 符号看起来像是一个目标）。第二个，$< 是规则依赖项，在本例中，它解析为 $(ENCODER)。事情肯定会变得复杂，但它还在管理之中。via: https://opensource.com/article/19/7/structure-multi-file-c-part-1作者：Erik O'Shaughnessy 选题：lujun9972 译者：萌新阿岩 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
无论你身处编程旅程中的何处，这 12 篇编程文章都有你需要学习一些东西。“学习计算机编程的最佳时间是 20 年前，其次是现在。” — 计算机科学的谚语（也许是）无论你是新程序员、经验丰富的 Web 开发人员、后端大师、头发花白的系统程序员，还是其他神秘物种的计算机极客？无论你身处广阔的软件开发人员生态系统中的何处，在我们领域中唯一不变的就是需要了解新技术及其应用方法。这是过去一年来阅读量最大的 Opensource.com 文章的集合，我很高兴在这里与你分享它们。好好学习无论你的技能或兴趣是什么，学习如何编写计算机程序都可以将特定领域的知识变成一种超能力。不幸的是，至少从现在开始，还不可能通过坐在计算机前面接受电磁辐射就能学会如何编程。在这之前，我推荐这些出色的文章来帮助激发你的编程技能。它们解决了一些重要的问题，例如考虑学习哪种语言以及不同的入门方式。你应该学习哪种编程语言？（译文）学习 Python 的 12 个方式（译文）你可以使用树莓派学习的三种流行编程语言（译文）利用 Python 引导孩子的计算机思维（译文）天天向上经验丰富的程序员知道，与学习编程相比，唯一更难的就是使你的技能和知识保持最新。但是我们是自学成才的一族，总是希望提高我们的专业知识和理解力。即使我们不期望使用新技术，我们也知道总会有人问起它们。这些有关 Rust、WebAssembly 和 Podman 的文章是开始学习软件技术一些即将出现的趋势的好地方。为什么要在 WebAssembly 中使用 Rust？使用 WebAssembly 扩展命令行领域免 root 的 Podman 如何工作？为什么选择 Rust 作为你的下一种编程语言老兵不死尽管新技术层出不穷，但回顾过去可以帮助你前进。当今惊人的技术是建立在昨天的工具之上的，即使我们从未使用这些传奇语言编写过代码，它也将使我们很好地理解这些工具的功能和局限性。这一系列文章集中于 C 语言、用 AWK 编写的实际应用程序，以及对流行度下降但也许还没有消亡的计算机语言的有趣讨论。C 的巨大影响如何写好 C main 函数（译文）用 AWK 喝咖啡（译文）你最喜欢的“死”语言是什么？学习不止无论你在编程过程中处于何处，总有更多东西需要学习。我希望这些来自 2019 年的顶级资源能帮助你为 2020 年做好准备。感谢作者们和你——读者。via: https://opensource.com/article/19/12/programming-resources作者：Erik O'Shaughnessy 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
简单介绍一下编程方式的历史演变。在计算机诞生不久的早期年代，硬件非常昂贵，而程序员比较廉价。这些廉价程序员甚至都没有“程序员”这个头衔，并且常常是由数学家或者电气工程师来充当这个角色的。早期的计算机被用来快速解决复杂的数学问题，所以数学家天然就适合“编程”工作。什么是程序？首先来看一点背景知识。计算机自己是做不了任何事情的，它们的任何行为都需要程序来引导。你可以把程序看成是非常精确的菜谱，这种菜谱读取一个输入，然后生成对应的输出。菜谱里的各个步骤由操作数据的指令构成。听上去有点儿复杂，不过你或许知道下面这个语句是什么意思：1 + 2 = 3其中的加号是“指令”，而数字 1 和 2 是数据。数学上的等号意味着等式两边的部分是“等价”的，不过在大部分编程语言中对变量使用等号是“赋值”的意思。如果计算机执行上面这个语句，它会把这个加法的结果（也就是“3”）储存在内存中的某个地方。计算机知道如何使用数字进行数学运算，以及如何在内存结构中移动数据。在这里就不对内存进行展开了，你只需要知道内存一般分为两大类：“速度快/空间小”和“速度慢/空间大”。CPU 寄存器的读写速度非常快，但是空间非常小，相当于一个速记便签。主存储器通常有很大的空间，但是读写速度就比寄存器差远了。在程序运行的时候，CPU 不断将它所需要用到的数据从主存储器挪动到寄存器，然后再把结果放回到主存储器。汇编器当时的计算机很贵，而人力比较便宜。程序员需要耗费很多时间把手写的数学表达式翻译成计算机可以执行的指令。最初的计算机只有非常糟糕的用户界面，有些甚至只有前面板上的拨动开关。这些开关就代表一个内存“单元”里的一个个 “0” 和 “1”。程序员需要配置一个内存单元，选择好储存位置，然后把这个单元提交到内存里。这是一个既耗时又容易出错的过程。程序员Betty Jean Jennings (左) 和 Fran Bilas (右) 在操作 ENIAC 的主控制面板后来有一名 电气工程师 认为自己的时间很宝贵，就写了一个程序，能够把人们可以读懂的“菜谱”一样的输入转换成计算机可以读懂的版本。这就是最初的“汇编器”，在当时引起了不小的争议。这些昂贵机器的主人不希望把计算资源浪费在人们已经能做的任务上（虽然又慢又容易出错）。不过随着时间的推移，人们逐渐发现使用汇编器在速度和准确性上都胜于人工编写机器语言，并且计算机完成的“实际工作量”增加了。尽管汇编器相比在机器面板上切换比特的状态已经是很大的进步了，这种编程方式仍然非常专业。上面加法的例子在汇编语言中看起来差不多是这样的：01 MOV R0, 1 02 MOV R1, 2 03 ADD R0, R1, R2 04 MOV 64, R0 05 STO R2, R0每一行都是一个计算机指令，前面是一个指令的简写，后面是指令所操作的数据。这个小小的程序首先会将数值 1 “移动”到寄存器 R0，然后把 2 移动到寄存器 R1。03 行把 R0 和 R1 两个寄存器里的数值相加，然后将结果储存在 R2 寄存器里。最后，04 行和 05 行决定结果应该被放在主存储器里的什么位置（在这里是地址 64）。管理内存中存储数据的位置是编程过程中最耗时也最容易出错的部分之一。编译器汇编器已经比手写计算机指令要好太多了，不过早期的程序员还是渴望能够按照他们所习惯的方式，像书写数学公式一样地去写程序。这种需求推动了高级编译语言的发展，其中有一些已经成为历史，另一些如今还在使用。比如 ALGO 就已经成为历史了，但是像 Fortran 和 C) 这样的语言仍然在不断解决实际问题。ALGO 和 Fortran 编程语言的谱系树这些“高级”语言使得程序员可以用更简单的方式编写程序。在 C 语言中，我们的加法程序就变成了这样：int x; x = 1 + 2;第一个语句描述了该程序将要使用的一块内存。在这个例子中，这块内存应该占一个整数的大小，名字是 x。第二个语句是加法，虽然是倒着写的。一个 C 语言的程序员会说这是 “X 被赋值为 1 加 2 的结果”。需要注意的是，程序员并不需要决定在内存的什么位置储存 x，这个任务交给编译器了。这种被称为“编译器”的新程序可以把用高级语言写的程序转换成汇编语言，再使用汇编器把汇编语言转换成机器可读的程序。这种程序组合常常被称为“工具链”，因为一个程序的输出就直接成为另一个程序的输入。编译语言相比汇编语言的优势体现在从一台计算机迁移到不同型号或者品牌的另一台计算机上的时候。在计算机的早期岁月里，包括 IBM、DEC、德州仪器、UNIVAC 以及惠普在内的很多公司都在制造除了大量不同类型的计算机硬件。这些计算机除了都需要连接电源之外就没有太多共同点了。它们在内存和 CPU 架构上的差异相当大，当时经常需要人们花费数年来将一台计算机的程序翻译成另一台计算机的程序。有了高级语言，我们只需要把编译器工具链迁移到新的平台就行了。只要有可用的编译器，高级语言写的程序最多只需要经过小幅修改就可以在新的计算机上被重新编译。高级语言的编译是一个真正的革命性成果。1983 发布的 IBM PC XT 是硬件价格下降的早期例子。程序员们的生活得到了很好的改善。相比之下，通过高级语言表达他们想要解决的问题让事情变得轻松很多。由于半导体技术的进步以及集成芯片的发明，计算机硬件的价格急剧下降。计算机的速度越来越快，能力也越来越强，并且还便宜了很多。从某个时间点往后（也许是 80 年代末期吧），事情发生了反转，程序员变得比他们所使用的硬件更值钱了。解释器随着时间的推移，一种新的编程方式兴起了。一种被称为“解释器”的特殊程序可以直接读取一个程序将其转换成计算机指令以立即执行。和编译器差不多，解释器读取程序并将它转换成一个中间形态。但和编译器不同的是，解释器直接执行程序的这个中间形态。解释型语言在每一次执行的时候都要经历这个过程；而编译程序只需要编译一次，之后计算机每次只需要执行编译好的机器指令就可以了。顺便说一句，这个特性就是导致人们感觉解释型程序运行得比较慢的原因。不过现代计算机的性能出奇地强大，以至于大多数人无法区分编译型程序和解释型程序。解释型程序（有时也被成为“脚本”）甚至更容易被移植到不同的硬件平台上。因为脚本并不包含任何机器特有的指令，同一个版本的程序可以不经过任何修改就直接在很多不同的计算机上运行。不过当然了，解释器必须得先移植到新的机器上才行。一个很流行的解释型语言是 perl。用 perl 完整地表达我们的加法问题会是这样的：$x = 1 + 2虽然这个程序看起来和 C 语言的版本差不多，运行上也没有太大区别，但却缺少了初始化变量的语句。其实还有一些其它的区别（超出这篇文章的范围了），但你应该已经注意到，我们写计算机程序的方式已经和数学家用纸笔手写数学表达式非常接近了。虚拟机最新潮的编程方式要数虚拟机（经常简称 VM）了。虚拟机分为两大类：系统虚拟机和进程虚拟机。这两种虚拟机都提供一种对“真实的”计算硬件的不同级别的抽象，不过它们的作用域不同。系统虚拟机是一个提供物理硬件的替代品的软件，而进程虚拟机则被设计用来以一种“系统独立”的方式执行程序。所以在这个例子里，进程虚拟机（往后我所说的虚拟机都是指这个类型）的作用域和解释器的比较类似，因为也是先将程序编译成一个中间形态，然后虚拟机再执行这个中间形态。虚拟机和解释器的主要区别在于，虚拟机创造了一个虚拟的 CPU，以及一套虚拟的指令集。有了这层抽象，我们就可以编写前端工具来把不同语言的程序编译成虚拟机可以接受的程序了。也许最流行也最知名的虚拟机就是 Java 虚拟机（JVM）了。JVM 最初在 1990 年代只支持 Java 语言，但是如今却可以运行 许多 流行的编程语言，包括 Scala、Jython、JRuby、Clojure，以及 Kotlin 等等。还有其它一些不太常见的例子，在这里就不说了。我也是最近才知道，我最喜欢的语言 Python 并不是一个解释型语言，而是一个 运行在虚拟机上的语言！虚拟机仍然在延续这样一个历史趋势：让程序员在使用特定领域的编程语言解决问题的时候，所需要的对特定计算平台的了解变得越来越少了。就是这样了希望你喜欢这篇简单介绍软件背后运行原理的短文。有什么其它话题是你想让我接下来讨论的吗？在评论里告诉我吧。via: https://opensource.com/article/19/5/primer-assemblers-compilers-interpreters作者：Erik O'Shaughnessy 选题：lujun9972 译者：chen-ni 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
学习如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数。我知道，现在孩子们用 Python 和 JavaScript 编写他们的疯狂“应用程序”。但是不要这么快就否定 C 语言 —— 它能够提供很多东西，并且简洁。如果你需要速度，用 C 语言编写可能就是你的答案。如果你正在寻找稳定的职业或者想学习如何捕获空指针解引用，C 语言也可能是你的答案！在本文中，我将解释如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数。我：一个顽固的 Unix 系统程序员。你：一个有编辑器、C 编译器，并有时间打发的人。让我们开工吧。一个无聊但正确的 C 程序C 程序以 main() 函数开头，通常保存在名为 main.c 的文件中。/* main.c */ int main(int argc, char *argv[]) { }这个程序可以编译但不干任何事。$ gcc main.c $ ./a.out -o foo -vv $正确但无聊。main 函数是唯一的。main() 函数是开始执行时所执行的程序的第一个函数，但不是第一个执行的函数。第一个函数是 _start()，它通常由 C 运行库提供，在编译程序时自动链入。此细节高度依赖于操作系统和编译器工具链，所以我假装没有提到它。main() 函数有两个参数，通常称为 argc 和 argv，并返回一个有符号整数。大多数 Unix 环境都希望程序在成功时返回 0（零），失败时返回 -1（负一）。参数名称描述argc参数个数参数向量的个数argv参数向量字符指针数组参数向量 argv 是调用你的程序的命令行的标记化表示形式。在上面的例子中，argv 将是以下字符串的列表：argv = [ "/path/to/a.out", "-o", "foo", "-vv" ];参数向量在其第一个索引 argv[0] 中确保至少会有一个字符串，这是执行程序的完整路径。main.c 文件的剖析当我从头开始编写 main.c 时，它的结构通常如下：/* main.c */ /* 0 版权/许可证 */ /* 1 包含 */ /* 2 定义 */ /* 3 外部声明 */ /* 4 类型定义 */ /* 5 全局变量声明 */ /* 6 函数原型 */ int main(int argc, char *argv[]) { /* 7 命令行解析 */ } /* 8 函数声明 */下面我将讨论这些编号的各个部分，除了编号为 0 的那部分。如果你必须把版权或许可文本放在源代码中，那就放在那里。另一件我不想讨论的事情是注释。“评论谎言。” - 一个愤世嫉俗但聪明又好看的程序员。与其使用注释，不如使用有意义的函数名和变量名。鉴于程序员固有的惰性，一旦添加了注释，维护负担就会增加一倍。如果更改或重构代码，则需要更新或扩充注释。随着时间的推移，代码会变得面目全非，与注释所描述的内容完全不同。如果你必须写注释，不要写关于代码正在做什么，相反，写下代码为什么要这样写。写一些你将要在五年后读到的注释，那时你已经将这段代码忘得一干二净。世界的命运取决于你。不要有压力。1、包含我添加到 main.c 文件的第一个东西是包含文件，它们为程序提供大量标准 C 标准库函数和变量。C 标准库做了很多事情。浏览 /usr/include 中的头文件，你可以了解到它们可以做些什么。#include 字符串是 C 预处理程序（cpp）指令，它会将引用的文件完整地包含在当前文件中。C 中的头文件通常以 .h 扩展名命名，且不应包含任何可执行代码。它只有宏、定义、类型定义、外部变量和函数原型。字符串 <header.h> 告诉 cpp 在系统定义的头文件路径中查找名为 header.h 的文件，它通常在 /usr/include 目录中。/* main.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <libgen.h> #include <errno.h> #include <string.h> #include <getopt.h> #include <sys/types.h>这是我默认会全局包含的最小包含集合，它将引入：#include 文件提供的东西stdio提供 FILE、stdin、stdout、stderr 和 fprint() 函数系列stdlib提供 malloc()、calloc() 和 realloc()unistd提供 EXIT_FAILURE、EXIT_SUCCESSlibgen提供 basename() 函数errno定义外部 errno 变量及其可以接受的所有值string提供 memcpy()、memset() 和 strlen() 函数系列getopt提供外部 optarg、opterr、optind 和 getopt() 函数sys/types类型定义快捷方式，如 uint32_t 和 uint64_t2、定义/* main.c */ <...> #define OPTSTR "vi:o:f:h" #define USAGE_FMT "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]" #define ERR_FOPEN_INPUT "fopen(input, r)" #define ERR_FOPEN_OUTPUT "fopen(output, w)" #define ERR_DO_THE_NEEDFUL "do_the_needful blew up" #define DEFAULT_PROGNAME "george"这在现在没有多大意义，但 OPTSTR 定义我这里会说明一下，它是程序推荐的命令行开关。参考 getopt(3) man 页面，了解 OPTSTR 将如何影响 getopt() 的行为。USAGE_FMT 定义了一个 printf() 风格的格式字符串，它用在 usage() 函数中。我还喜欢将字符串常量放在文件的 #define 这一部分。如果需要，把它们收集在一起可以更容易地修正拼写、重用消息和国际化消息。最后，在命名 #define 时全部使用大写字母，以区别变量和函数名。如果需要，可以将单词放连在一起或使用下划线分隔，只要确保它们都是大写的就行。3、外部声明/* main.c */ <...> extern int errno; extern char *optarg; extern int opterr, optind;extern 声明将该名称带入当前编译单元的命名空间（即 “文件”），并允许程序访问该变量。这里我们引入了三个整数变量和一个字符指针的定义。opt 前缀的几个变量是由 getopt() 函数使用的，C 标准库使用 errno 作为带外通信通道来传达函数可能的失败原因。4、类型定义/* main.c */ <...> typedef struct { int verbose; uint32_t flags; FILE *input; FILE *output; } options_t;在外部声明之后，我喜欢为结构、联合和枚举声明 typedef。命名一个 typedef 是一种传统习惯。我非常喜欢使用 _t 后缀来表示该名称是一种类型。在这个例子中，我将 options_t 声明为一个包含 4 个成员的 struct。C 是一种空格无关的编程语言，因此我使用空格将字段名排列在同一列中。我只是喜欢它看起来的样子。对于指针声明，我在名称前面加上星号，以明确它是一个指针。5、全局变量声明/* main.c */ <...> int dumb_global_variable = -11;全局变量是一个坏主意，你永远不应该使用它们。但如果你必须使用全局变量，请在这里声明，并确保给它们一个默认值。说真的，不要使用全局变量。6、函数原型/* main.c */ <...> void usage(char *progname, int opt); int do_the_needful(options_t *options);在编写函数时，将它们添加到 main() 函数之后而不是之前，在这里放函数原型。早期的 C 编译器使用单遍策略，这意味着你在程序中使用的每个符号（变量或函数名称）必须在使用之前声明。现代编译器几乎都是多遍编译器，它们在生成代码之前构建一个完整的符号表，因此并不严格要求使用函数原型。但是，有时你无法选择代码要使用的编译器，所以请编写函数原型并继续这样做下去。当然，我总是包含一个 usage() 函数，当 main() 函数不理解你从命令行传入的内容时，它会调用这个函数。7、命令行解析/* main.c */ <...> int main(int argc, char *argv[]) { int opt; options_t options = { 0, 0x0, stdin, stdout }; opterr = 0; while ((opt = getopt(argc, argv, OPTSTR)) != EOF) switch(opt) { case 'i': if (!(options.input = fopen(optarg, "r")) ){ perror(ERR_FOPEN_INPUT); exit(EXIT_FAILURE); /* NOTREACHED */ } break; case 'o': if (!(options.output = fopen(optarg, "w")) ){ perror(ERR_FOPEN_OUTPUT); exit(EXIT_FAILURE); /* NOTREACHED */ } break; case 'f': options.flags = (uint32_t )strtoul(optarg, NULL, 16); break; case 'v': options.verbose += 1; break; case 'h': default: usage(basename(argv[0]), opt); /* NOTREACHED */ break; } if (do_the_needful(&options) != EXIT_SUCCESS) { perror(ERR_DO_THE_NEEDFUL); exit(EXIT_FAILURE); /* NOTREACHED */ } return EXIT_SUCCESS; }好吧，代码有点多。这个 main() 函数的目的是收集用户提供的参数，执行最基本的输入验证，然后将收集到的参数传递给使用它们的函数。这个示例声明一个使用默认值初始化的 options 变量，并解析命令行，根据需要更新 options。main() 函数的核心是一个 while 循环，它使用 getopt() 来遍历 argv，寻找命令行选项及其参数（如果有的话）。文件前面定义的 OPTSTR 是驱动 getopt() 行为的模板。opt 变量接受 getopt() 找到的任何命令行选项的字符值，程序对检测命令行选项的响应发生在 switch 语句中。如果你注意到了可能会问，为什么 opt 被声明为 32 位 int，但是预期是 8 位 char？事实上 getopt() 返回一个 int，当它到达 argv 末尾时取负值，我会使用 EOF（文件末尾标记）匹配。char 是有符号的，但我喜欢将变量匹配到它们的函数返回值。当检测到一个已知的命令行选项时，会发生特定的行为。在 OPTSTR 中指定一个以冒号结尾的参数，这些选项可以有一个参数。当一个选项有一个参数时，argv 中的下一个字符串可以通过外部定义的变量 optarg 提供给程序。我使用 optarg 来打开文件进行读写，或者将命令行参数从字符串转换为整数值。这里有几个关于代码风格的要点：将 opterr 初始化为 0，禁止 getopt 触发 ?。在 main() 的中间使用 exit(EXIT_FAILURE); 或 exit(EXIT_SUCCESS);。/* NOTREACHED */ 是我喜欢的一个 lint 指令。在返回 int 类型的函数末尾使用 return EXIT_SUCCESS;。显示强制转换隐式类型。这个程序的命令行格式，经过编译如下所示：$ ./a.out -h a.out [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]事实上，在编译后 usage() 就会向 stderr 发出这样的内容。8、函数声明/* main.c */ <...> void usage(char *progname, int opt) { fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME); exit(EXIT_FAILURE); /* NOTREACHED */ } int do_the_needful(options_t *options) { if (!options) { errno = EINVAL; return EXIT_FAILURE; } if (!options->input || !options->output) { errno = ENOENT; return EXIT_FAILURE; } /* XXX do needful stuff */ return EXIT_SUCCESS; }我最后编写的函数不是个样板函数。在本例中，函数 do_the_needful() 接受一个指向 options_t 结构的指针。我验证 options 指针不为 NULL，然后继续验证 input 和 output 结构成员。如果其中一个测试失败，返回 EXIT_FAILURE，并且通过将外部全局变量 errno 设置为常规错误代码，我可以告知调用者常规的错误原因。调用者可以使用便捷函数 perror() 来根据 errno 的值发出便于阅读的错误消息。函数几乎总是以某种方式验证它们的输入。如果完全验证代价很大，那么尝试执行一次并将验证后的数据视为不可变。usage() 函数使用 fprintf() 调用中的条件赋值验证 progname 参数。接下来 usage() 函数就退出了，所以我不会费心设置 errno，也不用操心是否使用正确的程序名。在这里，我要避免的最大错误是解引用 NULL 指针。这将导致操作系统向我的进程发送一个名为 SYSSEGV 的特殊信号，导致不可避免的死亡。用户最不希望看到的是由 SYSSEGV 而导致的崩溃。最好是捕获 NULL 指针以发出更合适的错误消息并优雅地关闭程序。有些人抱怨在函数体中有多个 return 语句，他们喋喋不休地说些“控制流的连续性”之类的东西。老实说，如果函数中间出现错误，那就应该返回这个错误条件。写一大堆嵌套的 if 语句只有一个 return 绝不是一个“好主意”™。最后，如果你编写的函数接受四个以上的参数，请考虑将它们绑定到一个结构中，并传递一个指向该结构的指针。这使得函数签名更简单，更容易记住，并且在以后调用时不会出错。它还可以使调用函数速度稍微快一些，因为需要复制到函数堆栈中的东西更少。在实践中，只有在函数被调用数百万或数十亿次时，才会考虑这个问题。如果认为这没有意义，那也无所谓。等等，你不是说没有注释吗！？！！在 do_the_needful() 函数中，我写了一种特殊类型的注释，它被是作为占位符设计的，而不是为了说明代码：/* XXX do needful stuff */当你写到这里时，有时你不想停下来编写一些特别复杂的代码，你会之后再写，而不是现在。那就是我留给自己再次回来的地方。我插入一个带有 XXX 前缀的注释和一个描述需要做什么的简短注释。之后，当我有更多时间的时候，我会在源代码中寻找 XXX。使用什么前缀并不重要，只要确保它不太可能在另一个上下文环境（如函数名或变量）中出现在你代码库里。把它们组合在一起好吧，当你编译这个程序后，它仍然几乎没有任何作用。但是现在你有了一个坚实的骨架来构建你自己的命令行解析 C 程序。/* main.c - the complete listing */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <libgen.h> #include <errno.h> #include <string.h> #include <getopt.h> #define OPTSTR "vi:o:f:h" #define USAGE_FMT "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]" #define ERR_FOPEN_INPUT "fopen(input, r)" #define ERR_FOPEN_OUTPUT "fopen(output, w)" #define ERR_DO_THE_NEEDFUL "do_the_needful blew up" #define DEFAULT_PROGNAME "george" extern int errno; extern char *optarg; extern int opterr, optind; typedef struct { int verbose; uint32_t flags; FILE *input; FILE *output; } options_t; int dumb_global_variable = -11; void usage(char *progname, int opt); int do_the_needful(options_t *options); int main(int argc, char *argv[]) { int opt; options_t options = { 0, 0x0, stdin, stdout }; opterr = 0; while ((opt = getopt(argc, argv, OPTSTR)) != EOF) switch(opt) { case 'i': if (!(options.input = fopen(optarg, "r")) ){ perror(ERR_FOPEN_INPUT); exit(EXIT_FAILURE); /* NOTREACHED */ } break; case 'o': if (!(options.output = fopen(optarg, "w")) ){ perror(ERR_FOPEN_OUTPUT); exit(EXIT_FAILURE); /* NOTREACHED */ } break; case 'f': options.flags = (uint32_t )strtoul(optarg, NULL, 16); break; case 'v': options.verbose += 1; break; case 'h': default: usage(basename(argv[0]), opt); /* NOTREACHED */ break; } if (do_the_needful(&options) != EXIT_SUCCESS) { perror(ERR_DO_THE_NEEDFUL); exit(EXIT_FAILURE); /* NOTREACHED */ } return EXIT_SUCCESS; } void usage(char *progname, int opt) { fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME); exit(EXIT_FAILURE); /* NOTREACHED */ } int do_the_needful(options_t *options) { if (!options) { errno = EINVAL; return EXIT_FAILURE; } if (!options->input || !options->output) { errno = ENOENT; return EXIT_FAILURE; } /* XXX do needful stuff */ return EXIT_SUCCESS; }现在，你已经准备好编写更易于维护的 C 语言。如果你有任何问题或反馈，请在评论中分享。via: https://opensource.com/article/19/5/how-write-good-c-main-function作者：Erik O'Shaughnessy 选题：lujun9972 译者：MjSeven 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出