编码风格之(4)GNU软件标准风格(2)

GNU软件编码标准风格(2)

Author:Onceday Date: 2023年2024年1月8日

漫漫长路,才刚刚开始…

本文主要翻译自《GNU编码标准》(GNU Coding Standards)一文。

参考文档:

  • Linux kernel coding style — The Linux Kernel documentation
  • GNU Coding Standards - GNU Project - Free Software Foundation

文章目录

      • GNU软件编码标准风格(2)
        • 5. 充分利用C
          • 5.1 格式化源代码
          • 5.2 注释代码
          • 5.3 干净地使用C结构
          • 5.4 命名变量、函数和文件
          • 5.5 系统类型之间的可移植性
          • 5.6 cpu之间的可移植性
          • 5.7 调用系统功能
          • 5.8 国际化
          • 5.9 字符集
          • 5.10 引用字符
          • 5.11 Mmap文件映射
        • 6. 记录项目文档
          • 6.1 GNU参考手册
          • 6.2 文档字符串和手册
          • 6.3 手册结构详细说明
          • 6.4 手册License
          • 6.5 手册名誉归属
          • 6.6 印刷手册
          • 6.7 新闻文件
          • 6.8 变更日志
          • 6.9 更改日志的概念和约定
          • 6.10 简易更改信息
          • 6.11 条件修改信息
          • 6.12 部分修改信息
          • 6.13 帮助页
          • 6.14 阅读其他手册

5. 充分利用C
5.1 格式化源代码

为了在最广泛的环境中获得最大的可读性,请将源行长度保持在79个字符或以下。将开始C函数体的开括号放在第一列中是很重要的,这样它们将开始定义函数。有几个工具在第一列中查找开括号来查找C函数的开头。这些工具无法处理未以这种方式格式化的代码。

当它们在函数内部时,避免在第一列中放入左括号、左括号或左括号,这样它们就不会开始定义函数。开始结构体的开括号可以放在第一列,如果您认为将该定义视为函数定义有用的话。

对于函数定义来说,在第一列开始函数名也是很重要的。这可以帮助人们搜索函数定义,也可以帮助某些工具识别它们。因此,使用标准C语法,格式如下:

static char *
concat (char *s1, char *s2)
{
...
}

或者,如果你想使用传统的C语法,像这样格式化定义:

static char *
concat (s1, s2) /* Name starts in column one here */
	char *s1, *s2;
{ 				/* Open brace in column one here */
...
}

在标准C中,如果参数不能很好地放在一行中,那么像这样拆分它:

int
lots_of_args (int an_integer, long a_long, short a_short,
			  double a_double, float a_float)
...

对于结构体和枚举类型,同样将大括号放在第一列,除非整个内容可以放在一行:

struct foo
{
int a, b;
}
or
struct foo { int a, b; }

本节的其余部分给出了关于C格式风格的其他方面的建议,这也是1.2及更新版本中indent程序的默认风格。它对应于选项

-nbad -bap -nbc -bbo -bl -bli2 -bls -ncdb -nce -cp1 -cs -di2
-ndj -nfc1 -nfca -hnl -i2 -ip5 -lp -pcs -psl -nsc -nsob

我们不认为这些建议是需求,因为如果两个不同的程序有不同的格式样式,对用户来说不会造成问题。但是无论您使用什么风格,请始终使用它,因为在一个程序中混合使用风格往往看起来很难看。如果您正在对现有程序做出更改,请遵循该程序的风格。对于函数体,我们推荐的样式是这样的:

if (x < foo (y, z))
	haha = bar[4] + 5;
else
	{
		while (z)
			{
				haha += foo (z, z);
				z--;
			}
		return ++x + bar ();
	}

我们发现,当程序在开括号之前和逗号之后都有空格时,读起来更容易。尤其是在逗号之后。当将表达式拆分为多行时,请在操作符之前拆分,而不是在操作符之后。以下是正确的方法:

if (foo_this_is_long && bar > win (x, y, z)
	&& remaining_condition)

尽量避免在同一层次的缩进中有两个不同优先级的操作符。例如,不要这样写:

mode = (inmode[j] == VOIDmode
    || GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])
    ? outmode[j] : inmode[j]);

相反,使用额外的圆括号,以便缩进显示嵌套:

mode = ((inmode[j] == VOIDmode
	|| (GET_MODE_SIZE (outmode[j]) > GET_MODE_SIZE (inmode[j])))
	? outmode[j] : inmode[j]);

插入额外的圆括号,以便Emacs能够正确地缩进代码。例如,如果你手工缩进,下面的缩进看起来很漂亮,

v = rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
	+ rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000;

但是Emacs会改变它。添加一组括号产生的结果看起来同样不错,Emacs将保留它:

v = (rup->ru_utime.tv_sec*1000 + rup->ru_utime.tv_usec/1000
	+ rup->ru_stime.tv_sec*1000 + rup->ru_stime.tv_usec/1000);

像这样格式化do-while语句:

do
	{
		a = foo (a);
	}
while (a > 0);

请使用换行符(control-L)在逻辑位置将程序划分为页面(但不要在函数内)。这与页面的长度无关,因为它们不必适合打印页面。表单提要应该单独显示在行上。

5.2 注释代码

每个程序都应该以一个简短的注释开始,说明它的用途。例如:fmt - filter for simple filling of text。该注释应该位于包含程序main函数的源文件的顶部。

此外,请在每个源文件的开头写一个简短的注释,包括文件名和一两行关于文件总体目的的注释。

请用英语在GNU程序中写注释,因为英语是几乎所有国家的所有程序员都能读懂的语言。如果你的英文写得不好,请尽量用英文写评论,然后请其他人帮忙重写。如果你不能用英语写评论,请找个人帮你翻译。

请在每个函数上加注释,说明这个函数是做什么的,它得到什么类型的参数,参数的可能值意味着什么,用于什么。如果C类型以其习惯方式使用,则没有必要在文字中重复C参数声明的含义。如果它的使用有任何不标准的地方(比如char *类型的参数实际上是字符串的第二个字符的地址,而不是第一个字符的地址),或者任何可能的值都不能按预期的方式工作(比如,包含换行符的字符串不能保证工作),一定要这样说。同时说明返回值的意义(如果有的话)。

请在注释中的句子末尾加上两个空格,这样Emacs的句子命令才能正常工作。另外,请写出完整的句子,第一个单词大写。如果一个小写标识符出现在句子的开头,不要大写它。更改拼写使其成为不同的标识符。如果你不喜欢句子以小写字母开头,那么可以换一种更合适的写法来避免这种情况。

如果您使用参数名称来说明参数值,则对函数的注释会清晰得多。变量名本身应该是小写的,但是当你谈论值而不是变量本身时,应该用大写。因此,the inode number NODE NUM而不是an inode

通常没有必要在函数前面的注释中重述函数名,因为读者可以自己看到。当注释太长以至于函数本身会离开屏幕底部时,可能会有一个例外。

每个静态变量也应该有注释,像这样:

/* Nonzero means truncate lines in the display;
	zero means continue them. */
int truncate_lines;

每个#endif都应该有注释,除非是没有嵌套的短条件(只有几行)。注释应该说明结束条件句的条件,包括它的意义。#else应该有一个注释,描述后面代码的条件和意义。例如:

#ifdef foo
	...
#else /* not foo */
	...
#endif /* not foo */
#ifdef foo
	...
#endif /* foo */

但是,相反,为#ifndef这样写注释:

#ifndef foo
	...
#else /* foo */
	...
#endif /* foo */
#ifndef foo
	...
#endif /* not foo */
5.3 干净地使用C结构

请显式声明所有对象的类型。例如,应该显式地声明函数的所有参数,并且应该声明函数返回int而不是省略int。

一些程序员喜欢使用GCC的-Wall选项,并在发出警告时更改代码。如果你想这么做,那就这么做。其他程序员不喜欢使用
-Wall,因为它对他们不想更改的有效和合法代码给出警告。如果你想这么做,那就这么做。编译器应该是你的仆人,而不是主人。

不要仅仅为了用额外的警告选项(如-Wconversion-Wundef)来安抚静态分析工具(如lintclangGCC)而使程序变得丑陋。这些工具可以帮助发现bug和不清晰的代码,但是它们也会产生很多错误警报,使用不必要的强制类型转换、包装和其他复杂的操作会损害可读性。

例如,请不要仅仅为了安抚检查器而将强制类型转换为void或调用不做任何事情的函数。

外部函数的声明和稍后在源文件中出现的函数都应该放在文件开头附近的一个地方(在文件中第一个函数定义之前的某个地方),否则应该放在头文件中。不要将外部声明放在函数内部。

对于一个函数内的不同值反复使用相同的局部变量(名称类似于tem)曾经是一种常见的做法。与其这样做,不如为每个不同的目的声明一个单独的局部变量,并给它起一个有意义的名字。

这不仅使程序更容易理解,而且还便于优秀的编译器进行优化。您还可以将每个局部变量的声明移动到包含其所有用途的最小作用域中。这使得程序更加简洁。

不要使用掩盖全局标识符的局部变量或参数。GCC的-Wshadow 选项可以检测到这个问题。

不要在一个跨行声明中声明多个变量。而是在每行上开始一个新的声明。例如,与其这样:

int foo,	==>   int foo,bar; //更好的写法
	bar;

(如果它们是全局变量,那么每个变量之前都应该有注释)。

当一个if-else语句嵌套在另一个if语句中时,总是用大括号括住if-else语句。因此,永远不要这样写:

if (foo)
	if (bar)
		win ();
	else
		lose ();

而是应该如下:

if (foo)
{
	if (bar)
		win ();
	else
		lose ();
}

如果你有一个If语句嵌套在else语句中,要么在一行中写else If,像这样,

if (foo)
	...
else if (bar)
	...

将它的then-part像前面的then-part一样缩进,或者像这样将嵌套的if写在大括号中:

不要在同一声明中同时声明结构标签和变量或类型。相反,应该单独声明结构标记,然后使用它来声明变量或类型。

尽量避免在if条件内赋值(在while条件内赋值是可以的)。例如,不要这样写:

if ((foo = (char *) malloc (sizeof *foo)) == NULL)
	fatal ("virtual memory exhausted");

相反,你应该这样写:

foo = (char *) malloc (sizeof *foo);
if (foo == NULL)
	fatal ("virtual memory exhausted");
5.4 命名变量、函数和文件

程序中全局变量和函数的名称可作为某种注释。因此,不要选择简洁的名称,而是寻找能够提供有关变量或函数含义的有用信息的名称。在GNU程序中,名称应该是英文的,就像其他注释一样。

局部变量名可以更短,因为它们只在一个上下文中使用,其中(大概)注释解释了它们的目的。

尽量限制在符号名称中使用缩写。做一些缩写,解释它们的意思,然后经常使用它们是可以的,但不要使用大量晦涩的缩写。

请使用下划线分隔名称中的单词,以便Emacs单词命令可以在其中使用。坚持小写;宏和枚举常量以及遵循统一约定的名称前缀保留大写。

例如,您应该使用ignore_space_change_flag;不要使用像iCantReadThis这样的名字。

指示是否指定了命令行选项的变量应该以选项的含义命名,而不是以选项字母命名。注释应该说明选项的确切含义及其字母。例如,

/* Ignore changes in horizontal whitespace (-b). */
int ignore_space_change_flag;

当要定义具有常量整数值的名称时,请使用enum而不是#define定义。GDB知道枚举常量。

如果将文件加载到缩短文件名的MS-DOS文件系统上,您可能希望确保没有文件名会发生冲突。您可以使用doschk程序进行测试。

一些GNU程序被设计为限制文件名为14个字符或更少,以避免在将它们读入旧的System V系统时发生文件名冲突。请在已有的GNU程序中保留这个特性,但是没有必要在新的GNU程序中这样做。Doschk还报告了文件名长度超过14个字符。

5.5 系统类型之间的可移植性

在Unix世界中,“可移植性”指的是移植到不同的Unix版本。对于一个GNU程序,这种可移植性是可取的,但不是最重要的。

GNU软件的主要目的是运行在GNU内核之上,用GNU C编译器在各种类型的CPU上进行编译。因此,这种绝对必要的便携性是相当有限的。但是支持基于linux的GNU系统是很重要的,因为它们是流行的GNU形式。

除此之外,支持其他自由操作系统(*BSD)是很好的,如果您愿意,支持其他类unix系统也是很好的。支持各种类unix系统是可取的,尽管不是最重要的。它通常不会太难,所以你不妨去做。但你不必认为这是一种义务,如果结果确实很难的话。

实现可移植性到大多数类unix系统的最简单方法是使用Autoconf。您的程序不太可能需要知道更多关于主机平台的信息
Autoconf可以提供,因为需要这些知识的大多数程序都已经编写好了。

当有更高级别的替代方法(readdir)时,避免使用半内部数据库(例如目录)的格式。

对于不像Unix的系统,如MS-DOS、Windows、VMS、MVS和较旧的Macintosh系统,支持它们通常需要大量的工作。在这种情况下,最好把时间花在添加对GNU和GNU/Linux有用的特性上,而不是花在支持其他不兼容的系统上。

通常我们把“Windows”这个名字写成全称,但是当简洁非常重要的时候(比如文件名和一些符号名),我们把它缩写为“w”。例如,在GNU Emacs中,我们在Windows特定文件的文件名中使用’ w32 ',但是用于Windows条件的宏称为WINDOWSNT。原则上也可以是“w64”。

在编译C文件时定义“特性测试宏”_GNU_SOURCE是个好主意。当您在GNU或GNU/Linux上进行编译时,这将启用GNU库扩展函数的声明,并且如果您在程序中以其他方式定义相同的函数名称,通常会给您一个编译器错误消息。(如果您希望使程序更易于移植到其他系统,则不必实际使用这些函数。)

但是,无论您是否使用这些GNU扩展,您都应该避免将它们的名称用于任何其他含义。这样做会使将代码移到其他GNU程序中变得困难。

5.6 cpu之间的可移植性

即使GNU系统也会因为CPU类型的不同而有所不同。例如,字节排序和对齐要求的不同。处理这些分歧是绝对必要的。但是,不要努力去迎合int小于32位的可能性,我们在GNU中不支持16位机器。

你不需要考虑long比指针和size_t小的可能性。我们知道一个这样的平台,微软Windows上的64位程序。如果你想让你的包使用Mingw64在Windows上运行,你需要处理8字节的指针和4字节的long类型,这会破坏下面的代码:

printf ("size = %lu\n", (unsigned long) sizeof array);
printf ("diff = %ld\n", (long) (pointer2 - pointer1));

在您的软件包中是否支持Mingw64以及一般的Windows是您的选择。GNU工程并没有说你有责任这样做。我们的目标是取代专有系统,包括Windows,而不是增强它们。如果有人强迫你让你的程序在Windows上运行,而你不感兴趣,你可以这样回答:

“切换到GNU/Linux,你的自由依赖于它。”

off_t这样的预定义文件大小类型是一个例外:它们在许多平台上都比long长,所以像上面这样的代码不能处理它们。可移植地打印off_t值的一种方法是自己一个一个地打印它的数字。

不要假设一个int对象的地址也是它的最低位字节的地址。这在大端机器上是错误的。因此,不要犯以下错误:

int c;
...
while ((c = getchar ()) != EOF)
	write (file_descriptor, &c, 1);

相反,使用unsigned char,如下所示(unsigned是为了可移植到不寻常的系统中,其中char是有符号的,并且有整数溢出检查)。

int c;
while ((c = getchar ()) != EOF)
	{
		unsigned char u = c;
		write (file_descriptor, &u, 1);
	}

尽量避免将指针强制转换为整数。这种强制类型转换大大降低了可移植性,并且在大多数程序中很容易避免。在必须将指针转换为整数的情况下(例如,Lisp解释器在一个单词中存储类型信息和地址),必须明确规定处理不同的单词大小。您还需要为可以从malloc获得的正常地址范围远离零的系统做准备。

5.7 调用系统功能

从历史上看,C实现的差异很大,许多系统缺乏ANSI/ISO C89的完整实现。然而,现在所有的实际系统都有一个C89编译器,GNU C支持几乎所有的C99和部分C11。类似地,大多数系统实现POSIX.1-2001库和工具,并且许多系统具有POSIX.1-2008。

因此,几乎没有理由支持旧的C或非POSIX系统,您可能希望利用标准C和POSIX编写更清晰、更可移植或更快的代码。您应该尽可能使用标准接口;但是,如果GNU扩展使您的程序更易于维护,功能更强大,或者更好,请不要犹豫使用它们。在任何情况下,不要自己声明系统函数;这就是冲突的根源。

尽管有这些标准,但几乎每个库函数在某些系统上都存在某种可移植性问题。下面是一些例子:

  • open,在许多平台上,以/结尾的open name被错误处理。
  • printflong double可能未实现,浮点值InfinityNaN经常被错误处理,大精度的输出可能不正确。
  • readlink,可以返回int而不是ssize_t
  • scanf,在Windows上,失败时没有设置errno。

Gnulib在这方面提供了很大的帮助。Gnulib在许多缺乏标准接口的系统上提供了标准接口的实现,包括增强的GNU接口的可移植实现,从而使它们的使用可移植,以及POSIX-2.2008接口,其中一些甚至在最新版本中也缺失GNU系统。

Gnulib还提供了许多有用的非标准接口,例如,标准数据结构(哈希表、二叉树)的C实现、用于内存分配函数(xmalloc、xrealloc)的错误检查类型安全包装器,以及错误消息的输出。

Gnulib集成了GNU Autoconf和Automake,从而减轻了程序员编写可移植代码的负担:Gnulib使您的配置脚本自动确定缺少哪些特性,并使用Gnulib代码来提供缺失的部分。

Gnulib和Autoconf手册中有大量关于可移植性的章节:Gnulib中的“介绍”章节和Autoconf中的“可移植C和c++”章节。更多详情请咨询他们。

5.8 国际化

GNU有一个名为GNU gettext的库,它可以很容易地将程序中的消息翻译成各种语言。你应该在每个程序中使用这个库。在程序中出现消息时使用英语,并让gettext提供将它们翻译成其他语言的方法。

使用GNU gettext需要在每个可能需要翻译的字符串周围调用gettext宏,就像这样:

printf (gettext ("Processing file ’%s’..."), file);

这允许GNU gettext用翻译后的版本替换字符串"Processing file ’%s’..."

一旦程序使用了gettext,在添加需要转换的新字符串时,请注意编写对gettext的调用。

在包中使用GNU gettext需要为包指定文本域名。文本域名用于将此包的翻译与其他包的翻译分开。

通常,文本域名应该与包的名称相同,例如,GNU核心实用程序的coreutils

为了使gettext能够很好地工作,请避免编写对单词或句子结构进行假设的代码。当您希望句子的精确文本根据数据而变化时,请使用两个或更多替代字符串常量,每个字符串常量都包含一个完整的句子,而不是将条件化的单词或短语插入到单个句子框架中。

下面是一个不要做的例子:

printf ("%s is full", capacity > 5000000 ? "disk" : "floppy disk");

如果你对所有字符串应用gettext,像这样,

printf (gettext ("%s is full"),
	capacity > 5000000 ? gettext ("disk") : gettext ("floppy disk"));

翻译人员几乎不知道diskfloppy disk是要在另一个字符串中替换的。更糟糕的是,在某些语言(如法语)中,这种结构不起作用:单词“full”的翻译取决于句子第一部分的性别,diskfloppy disk的意思不一样。

完整的句子可以毫无问题地翻译:

printf (capacity > 5000000 ? gettext ("disk is full")
	: gettext ("floppy disk is full"));

类似的问题出现在这个代码的句子结构层面:

printf ("# Implicit rule search has%s been done.\n",
	f->tried_implicit ? "" : " not");

向该代码添加gettext调用并不能为所有语言提供正确的结果,因为某些语言中的否定需要在句子中的多个位置添加单词。

相比之下,如果代码像这样开始,添加gettext调用就可以直接完成工作:

printf (f->tried_implicit
	? "# Implicit rule search has been done.\n",
	: "# Implicit rule search has not been done.\n");

另一个例子是:

printf ("%d file%s processed", nfiles, nfiles != 1 ? "s" : "");

这个例子的问题是,它假设复数是通过加s构成的。如果像这样对格式字符串应用gettext,

printf (gettext ("%d file%s processed"), nfiles, nfiles != 1 ? "s" : "");

信息可以使用不同的单词,但它仍然会被迫使用’ s '作为复数。这里有一个更好的方法,gettext分别应用于两个字符串:

printf ((nfiles != 1 ? gettext ("%d files processed")
	: gettext ("%d file processed")),
	nfiles);

但这仍然不适用于像波兰语这样的语言,它有三种复数形式: 一种是nfiles == 1,另一种是nfiles == 2,3,4,22,23,24,…,剩还有一种表示剩下的元素。GNU的ngettext函数解决了这个问题:

printf (ngettext ("%d files processed", "%d file processed", nfiles),
	nfiles);
5.9 字符集

在GNU源代码注释、文本文档和其他上下文中,首选使用ASCII字符集(纯文本、7位字符),除非由于应用程序领域的原因有很好的理由做其他事情。例如,如果源代码处理法国大革命日历,那么如果其文本字符串在月份名称中包含重音字符(如Flor´eal)是可以的。同样,在更改日志中使用非ascii字符来表示贡献者的专有名称也是可以的(但不是必需的)。

如果需要使用非ascii字符,通常应该坚持使用一种编码,当然是在单个文件中,UTF-8可能是最好的选择。

5.10 引用字符

在C语言环境中,对于发给用户的消息中的引号字符,GNU程序的输出应该坚持使用纯ASCII:

  • 对于开引号和闭引号,最好使用0x22(' " ')0x27(" ')

虽然GNU程序传统上使用0x60(``)作为开引号,0x27(' ')作为闭引号,但现在引号`like this’通常是不对称的,所以引用’ ‘like this’ '或’like this’通常看起来更好。

对于GNU程序来说,在非c语言环境中生成特定于语言环境的引号是可以的,但不是必需的。例如:

printf (gettext ("Processing file '%s'..."), file);

在这里,法语翻译可能会导致gettext返回字符串"Traitement de fichier < %s > ...",产生更适合法语区域设置的引号。

有时程序可能需要直接使用开、闭引号。按照惯例,gettext将字符串""``""转换为开始引号,将字符串"'"转换为结束引号,程序可以使用这些翻译。但是,一般来说,最好在较长的字符串上下文中翻译引号字符。

如果您的程序的输出可能会被另一个程序解析,那么最好提供一个使解析可靠的选项。例如,您可以使用C语言或Bourne shell中的约定转义特殊字符。例如,请参阅GNU ls的选项,引用样式(--quoting-style)。

5.11 Mmap文件映射

如果使用mmap读取或写入文件,不要认为它对所有文件都有效,或者对所有文件都失败。它可能在某些文件上工作,而在其他文件上失败。

使用mmap的正确方法是在您想要使用它的特定文件上试用它,如果mmap不起作用,那么就用另一种方法使用读写来完成这项工作。

需要这种预防措施的原因是GNU内核(HURD)提供了一个用户可扩展的文件系统,其中可以有许多不同类型的“普通文件”。
他们中的许多人支持mmap,但有些人不支持。让程序处理所有这些类型的文件是很重要的。

6. 记录项目文档

理想情况下,GNU程序应该附带完整的自由文档,以满足参考和教程的需要。如果包可以编程或扩展,文档应该包括编程或扩展它,以及仅仅使用它。

6.1 GNU参考手册

GNU系统的首选文档格式是Texinfo格式语言。每个GNU包都应该(理想情况下)在Texinfo中提供文档,以供参考和学习者使用。

Texinfo使制作高质量的格式化书籍成为可能,使用并生成一个Info文件。也可以从Texinfo源生成HTML输出。

请参阅Texinfo手册,无论是硬拷贝还是通过info或Emacs info子系统(C-h - i)提供的在线版本。

现在一些其他的格式,如Docbook和Sgmltexi可以自动转换为Texinfo。通过这种方式转换生成Texinfo文档是可以的,只要它能给出好的结果。

确保你的手册对一个对主题一无所知的读者来说是清晰的,并直接阅读它。这意味着在开始的时候学习基本的主题,然后再学习高级的主题。这也意味着在第一次使用专门化术语时对其进行定义。

请记住,GNU手册(和其他GNU文档)的受众是全球性的,它将被使用数年,甚至数十年。这意味着读者可能有非常不同的文化参考点。几十年后,除了老年人,所有人都将有非常不同的文化参照点;今天,许多“人人都知道”的事情可能大多被遗忘了。

出于这个原因,尽量避免以一种依赖于文化参考点的方式来正确理解,或者以一种会阻碍不认识它们的人阅读的方式来引用它们。

同样,在选择单词(除了专业术语)、语言结构和拼写时要保守:目标是让十年前的读者能够理解这些内容。在任何关于潮流的竞赛中,GNU写作甚至不应该有资格进入。

偶尔引用空间或时间上的局部参考点或事实是可以的,如果它是直接相关的或作为旁白。当这些东西不再有意义时,改变它们(无论如何都是突出的)不会有很多工作。

相反,在相关的情况下,引用GNU和自由软件运动的概念总是恰当的。这些是我们信息的中心部分,所以我们应该利用机会提到它们。它们是基本的道德立场,所以它们很少会改变。

程序员倾向于将程序的结构作为其文档的结构。但是这种结构并不一定能很好地解释如何使用程序;对于用户来说,它可能是无关的和令人困惑的。

相反,构建文档的正确方法是根据用户在阅读文档时会想到的概念和问题。这个原则适用于每一个层次,从最低的(段落中的句子排序)到最高的(手册中的章节主题排序)。有时,这种思想结构与被记录的软件实现的结构相匹配,但通常它们是不同的。学习编写优秀文档的一个重要部分是学会注意到,当您不假思索地构建文档(如实现)时,停止自己,并寻找更好的替代方案。

例如,GNU系统中的每个程序可能都应该记录在一本手册中;但这并不意味着每个程序都应该有自己的手册。这将遵循实现的结构,而不是帮助用户理解的结构。

相反,每个手册应该涵盖一个连贯的主题。例如,我们没有diff手册和diff3手册,而是有一个“文件比较”手册,它涵盖了这两个程序以及cmp。通过将这些程序记录在一起,我们可以使整个主题更清晰。

讨论程序的手册当然应该记录程序的所有命令行选项及其所有命令。它应该给出它们使用的例子。但是不要把手册组织成一个功能列表。相反,要按子主题进行逻辑组织。

解决用户在考虑程序所做的工作时会问的问题。不要只是告诉读者每个功能都能做什么,说它适合什么工作,并展示如何在这些工作中使用它。解释推荐的用法,以及用户应该避免的用法。

一般来说,GNU手册应该同时作为教程和参考。它应该通过信息方便地访问每个主题,并直接阅读(附录)。

GNU手册应该给一个从头开始阅读的初学者一个很好的介绍,也应该提供黑客想要的所有细节。Bison手册就是一个很好的例子,请看看它来理解我们的意思。

这并不像听起来那么难。按照主题的逻辑顺序排列每一章,但要按章节顺序排列,并写下章节内容,这样从头到尾读一章就有意义了。在将书组织成章节和将一节组织成段落时,也要这样做。口号是,在每一点上,解决前一案文提出的最基本和最重要的问题。

如果有必要,在手册的开头添加额外的章节,这些章节纯粹是教程,涵盖了该主题的基础知识。这些为初学者理解手册的其余部分提供了框架。Bison手册为如何做到这一点提供了一个很好的例子。

作为参考,手册应该有一个索引,其中列出了程序的所有函数、变量、选项和重要概念。一个组合索引适用于简短的手册,但有时对于复杂的包,最好使用多个索引。Texinfo手册包括关于准备好的索引条目的建议,请参阅GNU Texinfo中的“制作索引条目”小节,并参见GNU Texinfo中的“定义索引条目”小节。

不要使用Unix手册页作为编写GNU文档的模型;它们中的大多数都很简洁,结构不好,对基本概念的解释也不充分。(当然,也有一些例外),此外,Unix手册页使用一种特殊的格式,与我们在GNU手册中使用的格式不同。

请在手册中包含一个电子邮件地址,以便在手册文本中报告错误。

请不要使用Unix文档中使用的术语“pathname”;使用“filename”(两个单词)代替。我们只对搜索路径使用术语“path”,它是目录名列表。

请不要使用“illegal”一词来指代计算机程序的错误输入。对此,请使用“invalid”,并保留“illegal”一词用于法律禁止的活动。

请不要在函数名后面写“()”,只是为了表明它是一个函数。Foo()不是一个函数,它是一个没有参数的函数调用。

只要有可能,请坚持使用主动语态,避免被动语态,使用现在时,而不是将来时。例如,写

“The function foo returns a list containing a and b”

而不是:

A list containing a and b will be returned.

主动语态的一个优点是它要求你陈述句子的主语,使用被动语态时,你可能会省略主语,这会导致模糊。

当语法要求时,使用将来时是合适的,比如,“如果你输入x,电脑将在10秒内自毁。”

6.2 文档字符串和手册

一些编程系统,如Emacs,为每个函数、命令或变量提供文档字符串。您可能想通过编译文档字符串并编写一些附加文本来编写参考手册,但您一定不要这样做。这种做法是一个根本性的错误。编写良好的文档字符串的文本对于手册来说是完全错误的。

文档字符串需要独立存在,当它出现在屏幕上时,不会有其他文本来介绍或解释它。同时,它可以是相当非正式的风格。

  • 说明书中描述函数或变量的文字不能单独存在,它出现在一个章节或小节的上下文中。

  • 本节开头的其他文本应该解释一些概念,并且通常应该提出一些适用于几个函数或变量的一般观点。

  • 本节之前对函数和变量的描述也将提供有关该主题的信息。

  • 单独写的描述会重复一些信息,这种冗余看起来很糟糕。

同时,在文档字符串中可以接受的非正式性在手册中是完全不可接受的。

在编写好的手册时使用文档字符串的唯一好方法是将它们用作编写好的文本的信息来源。

6.3 手册结构详细说明

手册的扉页应说明手册中所记录的程序或包的版本。手册的Top节点也应该包含这些信息。如果手册的变化比程序更频繁或独立于程序,也要在这两个地方说明手册的版本号。

手册中记录的每个程序都应该有一个名为“program”的节点“Invocation”或“Invoking program”。这个节点(连同它的子节点,如果有的话)应该描述程序的命令行参数以及如何运行它(人们会在手册页中寻找的那种信息)。从一个’ @example '开始,其中包含程序使用的所有选项和参数的模板。

或者,将菜单项放在菜单项名称符合上述模式之一的某个菜单中。无论节点的实际名称是什么,这都将该项所指向的节点标识为用于此目的的节点。

Info阅读器的--usage功能查找这样的节点或菜单项以查找相关文本,因此每个Texinfo文件都必须有一个。

如果一本手册描述了几个程序,它应该为手册中描述的每个程序都有这样一个节点。

6.4 手册License

对于所有超过几页的GNU手册,请使用GNU自由文档许可证。同样地,对于一组简短的文档,您只需要一份GNU FDL副本就可以完成整个集合。对于一个简短的文档,您可以使用非常宽松的non-copyleft许可证,以避免使用长许可证占用空间。

有关如何使用GFDL的更多解释,请参阅https://www.gnu.org/copyleft/fdl-howto.html。

请注意,在许可证既不是GPL也不是LGPL的手册中包含GNU GPL或GNU LGPL的副本并不是强制性的。将程序的许可证包含在大型手册中可能是个好主意;在一个简短的手册中,如果包括程序的许可证,它的大小将大大增加,最好不要包括它。

6.5 手册名誉归属

请在本手册的扉页上注明本手册的主要作者。如果有公司赞助了这项工作,请在手册中适当的位置感谢该公司,但不要引用该公司作为作者。

6.6 印刷手册

自由软件基金会以印刷形式出版了一些GNU手册。为了鼓励这些手册的销售,手册的联机版本应该在一开始就提到印刷版手册的可用性,并指出获取手册的信息,例如,链接到https://www.gnu.org/order/order.html页面。这一点不应该包括在印刷手册中,因为它是多余的。

在联机形式的手册中解释用户如何从来源打印出手册也是有用的。

6.7 新闻文件

除了它的手册之外,包应该有一个名为NEWS的文件,其中包含一个值得提及的用户可见的更改列表。

在每个新版本中,将项目添加到文件的前面,并确定它们属于哪个版本。

不要丢弃旧物品;将它们放在新条目之后的文件中。这样,从以前的任何版本升级的用户都可以看到新的内容。

如果NEWS文件很长,可以将一些较旧的项目移到名为ONEWS的文件中,并在文件末尾添加注释,让用户参考该文件。

6.8 变更日志

保持一个变更日志来描述对程序源文件所做的所有变更。这样做的目的是为了让将来调查bug的人了解可能引入bug的更改。通常可以通过查看最近更改的内容来发现新的错误。

更重要的是,变更日志可以帮助您消除程序的不同部分之间概念上的不一致,通过向您提供冲突概念是如何产生的历史,它们来自谁,以及为什么要进行冲突变更。

因此,更改日志应该足够详细和准确,以提供此类软件取证通常所需的信息。具体来说,更改日志应该使查找以下问题的答案变得容易:

  • 哪些更改影响了特定的源文件?
  • 是否对特定的源文件进行了重命名或移动,如果是这样,是什么变化的一部分?
  • 什么变化影响了给定的函数或宏或数据结构的定义?
  • 函数(或宏或数据结构的定义)是否从另一个文件重命名或移动,如果是这样,作为其中的一部分更改?
  • 什么变化删除了一个函数(或宏或数据结构)?
  • 变革的基本原理是什么,其主要思想是什么?
  • 是否有关于变更的其他信息,如果有,在哪里可以找到?

历史上,更改日志保存在特殊格式的文件中。如今,项目通常将它们的源文件保存在版本控制系统(VCS)下,比如Git、Subversion或Mercurial。

如果VCS存储库是可公开访问的,并且更改是单独提交给它的(每个逻辑更改集一次提交),并记录每个更改的作者,那么VCS记录的信息可以用于从VCS日志中生成更改日志,并通过使用合适的VCS命令来回答上述问题。(但是,VCS日志消息仍然需要提供一些支持信息,如下所述。)维护这种VCS存储库的项目可以决定不维护单独的更改日志文件,而是依赖于VCS来保存更改日志。

如果您决定不维护单独的变更日志文件,您仍然应该考虑在发布tarball中提供它们,以方便那些希望在不访问项目VCS存储库的情况下查看变更日志的用户。存在可以从VCS日志生成ChangeLog文件的脚本,例如,gitlog-to-changelog脚本,它是Gnulib的一部分,可以为Git存储库做到这一点。

在Emacs中,C-x v a (vc-updatechange-log)命令用于从VCS日志中增量更新ChangeLog文件。如果维护单独的更改日志文件,它们通常被称为ChangeLog,并且每个这样的文件覆盖整个目录。每个目录都可以有自己的更改日志文件,或者一个目录可以使用其父目录的更改日志,这取决于您。

6.9 更改日志的概念和约定

您可以将更改日志看作一个概念性的“撤消列表”,它说明了早期版本与当前版本的不同之处。人们可以看到当前版本,他们不需要变更日志来告诉他们变更日志中有什么内容。他们想从变更日志中得到的是对早期版本的不同之处的清晰解释。变更日志中的每个条目要么描述单个变更,要么描述属于一起的最小一批变更,也称为变更集。

用标题行开始更改日志条目是一个好主意:单行是一个完整的句子,它总结了更改集。如果您将更改日志保存在VCS中,这应该是一个要求,因为VCS命令以缩写形式显示更改日志,例如git log ——online,对标题行进行特殊处理。(在ChangeLog文件中,头行后面的一行说明了谁是更改的作者以及何时安装的。)

在更改日志条目的标题行后面跟踪总体更改的描述。这应该尽可能长,以便给出一个清晰的描述。要特别注意更改集的一些方面,这些方面不容易从差异或从修改的文件和函数的名称中收集到:更改的总体思想和对它的需求,以及对不同文件/函数所做更改之间的关系(如果有的话)。

如果更改或其原因在某些公共论坛上进行了讨论,例如项目的问题跟踪器或邮件列表,那么在更改描述中总结讨论的要点是一个好主意,并为那些想要完整阅读它的人提供一个指向该讨论或问题ID的指针。

解释部分新代码如何与其他代码协同工作的最佳位置是代码中的注释,而不是更改日志。

如果您认为更改需要解释为什么需要更改,也就是说,旧代码有什么问题需要进行更改。请将解释放在代码的注释中,人们在看到代码时都会看到它。这种解释的一个例子是,“这个函数以前是迭代的,但是当MUMBLE是树的时候就失败了。(尽管如此简单的原因不需要这种解释。)

其他类型的变更解释的最佳位置是在变更日志条目中。特别是,注释通常不会说明为什么某些代码被删除或移动到另一个地方,这属于对执行该操作的更改的描述。

在对更改的自由文本描述之后,最好根据您所更改的实体或定义所在的文件以及每个文件中更改的内容列出它们的名称。

如果一个项目使用现代VCS来保存更改日志信息,明确列出被更改的文件和函数并不是严格必要的,在某些情况下(如许多地方相同的机械更改)甚至是乏味的。

是否允许项目开发人员从日志条目中省略已更改的文件和函数列表,以及在某些特定条件下是否允许这样的省略,由您来决定。但是,在做出这个决定时,请考虑提供每次更改的更改实体列表的以下好处:

(1) 如果更改日志条目没有列出修改的函数/宏,那么从VCS日志生成有用的ChangeLog文件将变得更加困难,因为VCS命令不能仅从提交信息可靠地复制它们的名称。例如,当函数定义的头部分发生变化时,VCS日志命令中显示的diff块的头将错误地命名为被修改的函数(通常是在被修改的函数之前定义的函数),因此使用这些diffs来收集被修改函数的名称将产生不准确的结果。您将需要使用专门的脚本,如下面提到的gnulib的vcs-to-changelog.py,来解决这些困难,并确保它支持项目使用的源语言。

(2) 虽然现代VCS命令,如Git的Git log -LGit log -G,提供了强大的方法来查找影响某个函数、宏或数据结构的更改(因此,如果您有可用的存储库,可能会使ChangeLog文件变得不必要),但它们有时会失败。例如,git log -L不支持开箱即用的某些编程语言的语法。明确地提及修改过的函数/宏,可以简单而可靠地找到相关的更改。

(3) 一些VCS命令在跟踪跨文件移动或重命名的更改时存在困难或限制。同样,如果明确地提到实体,这些困难是可以克服的。

(4) 使用生成的ChangeLog文件检查更改的用户可能没有可用的存储库和VCS命令。为修改后的实体命名可以缓解这个问题。

由于这些原因,为每个更改提供修改的文件和函数列表使更改日志更有用,因此我们建议在可能和实际的情况下包括它们。

还可以通过运行脚本生成命名已修改实体的列表。其中一个脚本是mklog.py(用Python 3编写),它被GCC项目使用。

Gnulib提供了这种脚本的另一个变体,称为vcs-to-changelog.py,它是vcs-tochangelog模块的一部分。请注意,这些脚本目前支持的编程语言比Emacs提供的手动命令要少。

因此,上面提到的从VCS提交历史生成ChangeLog文件的方法,例如通过gitlog-to-ChangeLog脚本,通常会产生更好的结果,前提是贡献者坚持提供良好的提交消息。

下面是更改日志条目的一些简单示例,从标题行开始,说明谁进行了更改以及何时安装了更改,然后是对特定更改的描述。请记住,显示更改日期、作者姓名和电子邮件地址的那一行只需要单独使用ChangeLog文件,而不是当变更日志保存在VCS中时。

2019-08-29 Noam Postavsky 

Handle completely undecoded input in term (Bug#29918)

* lisp/term.el (term-emulate-terminal): Avoid errors if the whole
decoded string is eight-bit characters. Don’t attempt to save the
string for next iteration in that case.
* test/lisp/term-tests.el (term-decode-partial)
(term-undecodable-input): New tests.

2019-06-15 Paul Eggert 

Port to platforms where tputs is in libtinfow

* configure.ac (tputs_library): Also try tinfow, ncursesw (Bug#33977).

2019-02-08 Eli Zaretskii 

Improve documentation of ’date-to-time’ and ’parse-time-string’

* doc/lispref/os.texi (Time Parsing): Document
’parse-time-string’, and refer to it for the description of
the argument of ’date-to-time’.

* lisp/calendar/time-date.el (date-to-time): Refer in the doc
string to ’parse-time-string’ for more information about the
format of the DATE argument. (Bug#34303)

如果您提到修改过的函数或变量的名称,请将其完整命名。不要缩写函数名或变量名,也不要将它们组合在一起。后续的维护者经常会搜索一个函数名来找到与它相关的所有更改日志条目;如果你缩写名字,他们在搜索时找不到。

例如,有些人倾向于通过以下方式来缩写函数名称组* register.el ({insert,jump-to}-register),这不是一个好主意,因为搜索跳转到寄存器或插入寄存器将找不到该条目。

用空行分隔不相关的更改日志条目。不要在条目的个别更改之间放置空白行。当连续的单个更改位于同一文件中时,可以省略文件名和星号。

通过使用’)‘而不是’,‘来结束长函数名列表,并使用’('打开连续行。这使得Emacs中的高亮显示工作得更好。下面是一个例子:

* src/keyboard.c (menu_bar_items, tool_bar_items)
(Fexecute_extended_command): Deal with ’keymap’ property.

向ChangeLog添加条目的最简单方法是使用Emacs命令M-x addchange-log-entry,或其变体c - x4a (add-change-log-entry-other-window)。这将自动收集已更改的文件和已更改的函数或变量的名称,并根据上述约定格式化更改日志条目,由您来描述对该函数或变量所做的更改。

当您安装其他人的更改时,请将贡献者的名字放在更改日志条目中,而不是放在条目的文本中。换句话说,这样写:

2002-07-14 John Doe 
	* sewing.c: Make it sew.

而不是这样:

2002-07-14 Usual Maintainer 
	* sewing.c: Make it sew. Patch by [email protected].

当将其他人的更改提交到VCS中时,使用VCS的特性来指定作者。例如,使用Git,使用Git commit ——author=author

至于日期,应该是您应用更改的日期。(对于VCS,使用适当的命令行开关,例如,git commit ——date=date)

现代VCS有命令来应用通过电子邮件发送的更改(例如,Git有Git am);在这种情况下,将自动从电子邮件消息中收集变更集的作者及其生成日期,并记录在存储库中。

如果补丁是用合适的VCS命令准备的,比如git format-patch,电子邮件消息体也会有更改集的原作者,所以重新发送或转发消息不会干扰将更改归给其作者。因此,我们建议您请求您的贡献者使用git format-patch等命令来准备补丁。

6.10 简易更改信息

某些简单类型的更改不需要在更改日志中记录太多细节。如果更改的描述足够短,它可以作为自己的标题行:

2019-08-29 Eli Zaretskii 

* lisp/simple.el (kill-do-not-save-duplicates): Doc fix. (Bug#36827)

当您以一种简单的方式更改函数的调用顺序,并且更改函数的所有调用者以使用新的调用顺序时,不需要为更改的所有调用者创建单独的条目。只需在被调用的函数的条目中写入“所有调用者都改变了”,就像这样:

* keyboard.c (Fcommand_execute): New arg SPECIAL.
All callers changed.

当您只更改注释或文档字符串时,为文件编写一个条目就足够了,而不需要提及函数。对于更改日志来说,只需“文档修复”就足够了。

当您在许多文件中进行更改时,这些更改机械地遵循一个基础更改,描述基础更改就足够了。下面是一个影响存储库中所有文件的更改示例:

2019-01-07 Paul Eggert 

Update copyright year to 2019

Run ’TZ=UTC0 admin/update-copyright $(git ls-files)’.

测试套件文件是软件的一部分,因此我们建议将它们作为代码处理,以用于更改日志。

在技术上不需要为非软件文件(手册、帮助文件、媒体文件等)制作更改日志条目。这是因为它们不容易受到难以理解的bug的影响。要纠正错误,你不需要知道错误文章的历史;把文件里说的和实际情况比较一下就够了。

然而,当项目从贡献者那里获得版权分配时,您应该保留非软件文件的更改日志,以便使作者身份的记录更加准确。出于这个原因,我们建议保留项目手册的Texinfo源的更改日志。

6.11 条件修改信息

源文件通常可以包含以构建时条件或静态条件为条件的代码。例如,C程序可以包含编译时的#if条件,用解释型语言实现的程序可以包含函数定义的模块导入,这些函数定义只对特定版本的解释器执行,和Automake Makefile.Am文件可以包含变量定义或目标声明,只有在配置时Automake条件为真。

许多更改也是有条件的,有时您添加了一个新的变量、函数,甚至是一个新的程序或库,这些都完全依赖于构建时条件。在更改日志中指出应用更改的条件是很有用的。我们指示条件更改的惯例是在条件名称周围使用方括号。

条件更改可能发生在许多情况下,并且有许多变化,因此这里有一些示例来帮助说明。第一个示例描述了C、Perl和有条件但没有关联函数或实体名称的Python文件:

* xterm.c [SOLARIS2]: Include .
* FilePath.pm [$^O eq ’VMS’]: Import the VMS::Feature module.
* framework.py [sys.version_info < (2, 6)]: Make "with" statement
	available by importing it from __future__,
	to support also python 2.5.

为了简单起见,我们的其他示例将仅限于C,因为使它们适应其他语言所需的微小更改应该是不言而喻的。

接下来,有一个条目描述了一个完全有条件的新定义: C语言宏FRAME_WINDOW_P只有在宏HAVE_X_WINDOWS被定义时才会被定义(和使用):

* frame.h [HAVE_X_WINDOWS] (FRAME_WINDOW_P): Macro defined.

接下来是init_display函数中的一个更改条目,它的定义作为一个整体是无条件的,但是更改本身包含在#ifdefHAVE_LIBNCURSES的条件:

* dispnew.c (init_display) [HAVE_LIBNCURSES]: If X, call tgetent.

最后,这里有一个更改条目,仅在未定义某个宏时生效:

* host.c (gethostname) [!HAVE_SOCKETS]: Replace with winsock version.
6.12 部分修改信息

用尖括号标明函数中发生变化的部分,并注明变化部分的作用。下面是sh-while-getopts函数中处理sh命令部分的修改:

* progmodes/sh-script.el (sh-while-getopts) : Handle case that
user-specified option string is empty.
6.13 帮助页

在GNU项目中,手册页是次要的。这不是每个人都需要或期望的,GNU程序很少有手册页,但是有些程序有。您可以选择是否在程序中包含手册页。

当您做出这个决定时,要考虑到每次更改程序时支持手册页都需要持续的努力。花在手册页上的时间占用了更有用的工作时间。

对于变化不大的简单程序,更新手册页可能是一件小事。如果您有手册页,那么就没有理由不包含手册页。

对于大量更改的大型程序,更新手册页可能是一个很大的负担。如果一个用户提供一个手册页,你可能会发现接受这个礼物很昂贵。最好拒绝使用手册页,除非同一个人同意承担维护手册页的全部责任,这样您就可以完全洗手不干了。如果这个志愿者后来不再做这项工作了,你不必觉得有义务自己去捡;最好从发行版中撤回手册页,直到其他人同意更新它。

当程序只更改了一点时,您可能会觉得差异很小,以至于手册页在不更新的情况下仍然有用。如果是这样,在手册页的开始处放一个显著的注释,说明你不维护它,并且Texinfo手册更权威。注释应该说明如何访问Texinfo文档。

确保手册页包含版权声明和自由许可证。简单的全许可许可适用于简单的手册页(请参阅“其他文件(参见GNU维护者信息)。

对于冗长的手册页,有足够的解释和文档,它们可以被认为是真正的手册,请使用GFDL。

最后,GNU help2man程序是自动生成手册页的一种方法,在本例中是通过--help输出生成手册页。这在许多情况下是足够的。

6.14 阅读其他手册

可能有非自由书籍或文档文件描述了您正在编写文档的程序。

使用这些文件作为参考是可以的,就像新代数教科书的作者可以阅读其他代数书籍一样。任何非小说类书籍的很大一部分都是由事实组成的,在这种情况下,事实是关于某个程序如何工作的,这些事实对于每个写这个主题的人来说都是相同的。但要注意,不要从已有的非自由文档中复制你的大纲结构、措辞、表格或示例。复制自由文档可能是可以的;请与FSF核实个别情况。

你可能感兴趣的:(Iinux小白之路,编程语言,gnu,linux,c语言)