如何看懂man page?

Posted by djkings

看懂man page是做Linux开发最基本的要求,然而很多新手非常不喜欢看man page,我们在教
学中发现,虽然从第一天讲编程就开始强调一定要看man page,rtfm=read the f*cking
manual,但结果是很多学生都想方设法绕过看man page,一个月以后,从没来仔细看过一个
man page的学生仍然有半数以上。

比如有一本《Linux常用C函数(中文版)》就是学生们的最爱,虽然我们从来没有推荐过也
没有提供过这本书的电子版或印刷版,但是学生几乎人手一份。这本书的风格和man page截
然不同,函数接口的说明非常简略,远远没有涵盖man page的要点,然而每个函数后面都不
厌其烦地举一个例子,即使这个函数的用法已经像秃头上的虱子那么明显了也要举个例子,
而且通常这个例子写得极不规范,例如从来不判断出错返回值。让我说,这本书就是垃圾,
这本书的存在不仅浪费空间,而且害人不浅。适合新手速查是没有错,但人都是有惰性的,
新手往往都会依赖上这本书,不用去看man page,也不想去看,看man page干吗?东拉西扯
说了那么多,费半天劲也看不懂,而且最后连个例子都没有,看完还是不知道怎么调用这个
函数,哪有看这本书学得轻松,连字都不用看,直接把例子粘贴到自己的代码中就行了。

新手就这样被毒害了:第一,刚才说了,这些例子极不规范,bug很多,就是垃圾代码,谁用
了它谁的代码也就成了垃圾代码;第二,说明得太简略,容易让人产生片面理解和误解。第
三,助长了新手的惰性,虽然靠这本书能写出很多程序,但英文能力、理解能力和技术水平
都长期停滞不前,根本不能算是学习提高了;第四,这本书毕竟只介绍了数量有限的C函数,
实际工作中当然会用到很多书上没有的函数,本来看看man page就会用了,但是新手们已经
离不开这本书了,必然会想一些凑合应付的办法,用书上有的函数代替书上没有的函数去应
付工作。就这样,这本masterpiece培养出了一大批合格的垃圾代码制造者。

还有一本《Linux C函数库详解词典》也是这一类书的典型代表,和上面说的那本大同小异。
扯点离题的话,我有一个更极端的观点:任何给程序员看的文档都不应该翻译成中文,因为
不具备流畅地阅读英文的能力就不是一个合格的程序员,应该先去学好英文再学编程,更何
况翻译总会引入新的错误和不准确,使文档的质量下降。只有给用户看的文档才应该翻译成
中文,因为不能要求用户达到多高的水平才可以使用这个软件。

把难理解的、难掌握的都回避了,把本来很复杂的man page阉割了之后再去教给新手,让他
们以为掌握技术就是这么简单,一书在手,万事不愁,这根本不算是教育。真正的教育不应
该回避任何复杂性,而应该是举一反三,把一个复杂的问题给学生分析透了,然后启发学生
自己去解决其它的复杂问题。下面我来仔细剖析一个man page,通过这一个例子说明man
page的行文中存在的普遍规律,说明应该如何理解一个man page,以达到举一反三的目的,
我相信我这一篇文章比以上两本烂书对新手更为有用。

这是POSIX规范中正则表达式的C函数的man page,读者要用这些函数首先要对正则表达式的
概念非常清晰,知道正则表达式能用来干什么,不能用来干什么,要干的话怎么干,并且能
够很熟练地写出正则表达式来,每个man page都是高度cohesive的,不会教你这些偏离主题
的东西。也就是说,首先你期望要用这些函数完成什么工作必须非常清楚,如果自己都不知
道自己要干什么,man page是帮不了你的。

REGEX(3)                   Linux Programmer’s Manual                  REGEX(3)

NAME
       regcomp, regexec, regerror, regfree - POSIX regex functions

SYNOPSIS
       #include <sys/types.h>
       #include <regex.h>

       int regcomp(regex_t *preg, const char *regex, int cflags);

       int regexec(const regex_t *preg, const char *string, size_t nmatch,
                   regmatch_t pmatch[], int eflags);

       size_t regerror(int errcode, const regex_t *preg, char *errbuf,
                       size_t errbuf_size);

       void regfree(regex_t *preg);

这个man page描述了四个函数的用法。本来我只是想用一个正则表达式匹配一个字符串,并取得
匹配结果,也就是说我想要的是这样一个函数:

int my_expect_func(传入:正则表达式, 传入:目标字符串, 传出:匹配结果);
返回:错误码

怎么会有四个函数呢?哪个跟我想要的函数最相关?其它函数又是做什么的?这是一个好的
阅读习惯:你要主动去猜测,而不是被动地接受信息。理解的过程应该是拿你的猜测
去和文字描述相比较,如果相符就说明理解对了,如果不符就要提出一个新的猜测去比较,
完全被动地接受信息那不叫理解。

传入参数和传出参数是一个重要的提示,Linux的库函数原型都是非常规范的,const指针一
定是传入参数,非const指针一定有传出值(可能是传出参数,也可能是传入-传出参数),
所以,函数原型就已经非常清楚地告诉你应该怎么调用这个函数了,根本没必要给出代码例
子。看第一个函数:

int regcomp(regex_t *preg, const char *regex, int cflags);

preg是传出参数,需要事先分配该对象的内存然后把地址传给regcomp函数,regex是传入参
数,cflags是标志位,preg不知道是什么,但regex就是regular expression,正则表达式,
又是char *型的,应该没错了,不用看下面的说明就可以猜测这个函数是这样调用的:

regex_t regobj;
regcomp(&regobj, "正则表达式", 标志位1|标志位2|...);

再强调一遍,要想理解一段文字,就要充分调动经验和推理,主动去猜测,然后看下文验证
你的猜测,而不是被动接受信息。怎么推理呢?以上函数传入一个正则表达式,指定几个标
志,传出一个值,应该是把正则表达式转换格式了吧?这就叫推理。相反,如果我根本不管
preg是一个传出参数,而且也不是字符串型的,非要往my_expect_func的形式上套,既然
regex参数是正则表达式,那么preg参数就应该是目标字符串,这就不叫推理和猜测,叫瞎蒙。

如果对正则表达式的机理有一定了解,就可以借助这个经验猜到这个函数大概是把正则表达
式字符串转换成状态机以便高效地匹配目标字符串。如果以前用过其它编程语言的正则表达
式库函数,也可以借助这些经验知道正则表达式在使用之前大多有一个预处理的步骤。另
外,对英文缩写要有一定敏感性,函数名是regcomp,reg就是正则表达式,comp是compare还
是compile?如果是compare,那应该有两个相同类型的参数来做比较,就像strcmp,这里显
然是compile,编译,把字符串形式转为二进制形式,从另一个侧面也验证了前面的猜测。这
些都是靠经验而不是推理得到的,经验有助于更快更准确地理解,但不是必须的,因为事实
上我们通过上面基于传入传出参数的推理已经猜出正确结论了,只不过有经验的人会对自己
的猜测更自信。

对英文缩写敏感是看man page和看代码需要具备的最基本的能力,但这需要长期的练习才能
找到感觉。也许你要学会一个函数怎么用并不必知道函数名和各个参数名是什么的缩写,你
通过以上列举的两本烂书就可以学会怎么用,但如果总是回避man page,总是不去做猜缩写
的练习,就不可能看懂别人的代码,不看别人的代码就自己乱写代码,连变量名该怎么起都
不知道,写出来的永远是垃圾代码。对于regcomp这个函数名以及各参数名,regex是
regular expression,regcomp是regular expression compile。那么preg是什么?reg是
regular expression,p表示什么呢?表示指针?那是微软的infamous的hungarian
notation,Linux上肯定不是这么用的,这里的p我猜是precompiled。cflags的c是什么?不
知道,但是跟下面一个函数对比来看:

int regexec(const regex_t *preg, const char *string, size_t nmatch,
            regmatch_t pmatch[], int eflags);

这个函数有个参数叫eflags。所以c是regcomp的c,而e是regexec的e,一个是编译时的
flags,一个是执行时的flags,这两种flags的取值必然不同,下文必然会分别说明。这又是
一种猜测:猜测下文的行文逻辑。这种猜测同样是非常有助于理解的。后面几个函数的函数
名和参数名是怎么缩写的,留给读者自己练习。

preg参数在regcomp中是传出参数,在regexec中却是传入参数,根据推理,preg是由
regcomp函数填写好之后传给regexec函数用的,也就是说正则表达式以转换之后的二进制格
式传给regexec函数来用。regexec又有一个字符串传入参数string,还有两个match参数表示
匹配结果,pmatch是传出参数,表示缓冲区首地址,nmatch表示缓冲区长度(根据经验,这
类似于strncpy),这必然就是我一开始想要的my_expect_func了:

int my_expect_func(传入:正则表达式, 传入:目标字符串, 传出:匹配结果);
返回:错误码

preg对应正则表达式,pmatch和nmatch对应匹配结果,因此string这个传入参数必然是目标
字符串了。pmatch是一个指针变量,但是写成pmatch[],说明它指向的是一组而不是一个
regmatch_t类型的对象,这一组有多少个呢?用nmatch参数表示。和strncpy类似,这一组
regmatch_t对象应该由我们事先分配好再传给函数。因此这两个函数应该是这样调用的:

regex_t regobj;
regcomp(&regobj, "正则表达式", 标志位1|标志位2|...);
regmatch_t matchbuf[10];
regexec(&regobj, "目标字符串", 10, matchbuf, 标志位1|标志位2|...);

regmatch_t对象如何表示一个匹配呢?如果一个正则表达式模式在一个目标字符串中有五次
出现,如何表示这五次出现呢?可以猜测这个regmatch_t结构体一定包含了在目标字符串中
的匹配位置信息。另外,我传进去10个regmatch_t对象,如果只有五次匹配,函数返回后我
怎么知道前面五个对象是有效的匹配信息而后面是无效的呢?是不是通过一个参数或返回值
表示匹配次数的?该函数并没有额外的参数,而且快速翻看一下man page的RETURN
VALUE节,这个函数返回值是错误码,也不表示匹配次数。那这个函数一定会在后面无效的
regmatch_t对象里填充一个特殊值,这就是推理,这个猜测将会在阅读后面的文字时证实或
证伪,不管猜得对不对,一定会在后面得到答案。

后面还有两个函数:

size_t regerror(int errcode, const regex_t *preg, char *errbuf,
                size_t errbuf_size);

void regfree(regex_t *preg);

根据以往的经验regerror相当于perror或者strerror,将错误码翻译成一个可读性好的字符
串,regfree相当于free,用来释放preg。但是preg不是我们自己事先分配的对象么?既然不
是由这一组函数动态分配的,为什么需要用这一组函数来free?由这个问题引出一个新的猜
测,regex_t这种结构体中一定有指针类型的成员,regcomp函数一定是动态分配了一块内存
然后让preg中的指针成员指向该内存,所以需要用regfree来释放一下,后者循着preg参数找
到它的指针成员,然后释放先前分配的内存。再结合经验,正则表达式的长短不同,复杂程
度肯定不同,如果用状态机表示那么需要的状态数量肯定不同,不可能所有正则表达式的二
进制表示都用sizeof(regex_t)这么大就够用,必然需要动态分配内存。这种推理和猜测不仅
有助于解决如何使用函数的问题,而且对于这些函数的实现机制也获得了一些insight,这种
能力对于读代码尤其重要。注意,释放内存的函数虽然是传入参数的,不传出任何有意义的
值,但是函数原型中的参数不使用const修饰,因为释放内存也是一种修改。

刚把SYNOPSIS看完,还没有看下面的说明,就已经差不多会用这些函数了,靠的是什么?1、
推理 2、经验 3、对英文缩写敏感。下面一边看说明,一边验证以上猜测。

DESCRIPTION
   POSIX Regex Compiling
       regcomp()  is  used to compile a regular expression into a form that is
       suitable for subsequent regexec() searches.

没错,regcomp确实是用来把正则表达式转换成一种二进制格式以适合subsequent的
regexec()处理。这个subsequent就说明先调用regcomp再调用regexec。理解文档的时候,表
示概念的文字和表示概念之间关系的文字是最重要的。像man page这种简洁的文档中,表示
概念之间关系的文字尤其容易被忽视,因为不像下定义那么明显,往往一个词就带过。作为
练习,请读者注意后面的文字中有哪些表示概念之间关系的词。

regcomp() is supplied with preg, a pointer to a pattern buffer  storage
area;  regex, a pointer to the null-terminated string and cflags, flags
used to determine the type of compilation.

All regular expression searching must be done via  a  compiled  pattern
buffer,  thus  regexec()  must always be supplied with the address of a
regcomp() initialized pattern buffer.

preg, a pointer to a pattern buffer storage area就说明preg这个对象的空间是需要我
们自己分配的,分配完了再传一个地址也就是preg给regcomp。man page不会直接说你应该自
己分配了空间再传给我,这么说也太贰了。但你要自己体会出它真正想传达给你的信息。

       cflags may be the bitwise-or of one or more of the following:

       REG_EXTENDED
              Use POSIX Extended Regular Expression syntax  when  interpreting
              regex.   If  not  set,  POSIX Basic Regular Expression syntax is
              used.

       REG_ICASE
              Do not differentiate case.  Subsequent regexec() searches  using
              this pattern buffer will be case insensitive.

       REG_NOSUB
              Support  for  substring  addressing  of matches is not required.
              The nmatch and pmatch parameters to regexec() are ignored if the
              pattern buffer supplied was compiled with this flag set.

       REG_NEWLINE
              Match-any-character operators don’t match a newline.

              A  non-matching list ([^...])  not containing a newline does not
              match a newline.

              Match-beginning-of-line operator (^) matches  the  empty  string
              immediately  after  a newline, regardless of whether eflags, the
              execution flags of regexec(), contains REG_NOTBOL.

              Match-end-of-line operator ($) matches the empty string  immedi‐
              ately  before  a  newline, regardless of whether eflags contains
              REG_NOTEOL.

POSIX Regex Matching
       regexec() is used to match a null-terminated string against the precom‐
       piled  pattern  buffer,  preg.   nmatch  and pmatch are used to provide
       information regarding the location of any matches.  eflags may  be  the
       bitwise-or  of  one  or  both  of REG_NOTBOL and REG_NOTEOL which cause
       changes in matching behavior described below.

       REG_NOTBOL
              The match-beginning-of-line operator always fails to match  (but
              see  the  compilation  flag  REG_NEWLINE above) This flag may be
              used when different portions of a string are passed to regexec()
              and the beginning of the string should not be interpreted as the
              beginning of the line.

       REG_NOTEOL
              The match-end-of-line operator always fails to  match  (but  see
              the compilation flag REG_NEWLINE above)

前面猜测过了,cflags和eflags既然不叫同一个名字,肯定分别有不同的取值,并且通常这
些取值都是bitwise-or起来用的。本文重点在于讲如何阅读理解man page,而不在于讲具体
的技术,所以这些标志都起什么作用不详细解释了。但是再做几个猜缩写的练习,这不仅有
助于理解,更有助于记忆这些标志,有些常用的标志把它记住了就不必每次用都查手册了。
REG_ICASE,ICASE表示ignore case,这种缩写很常见。REG_NOSUB,SUB有些时候表示
substitute,有些时候表示substring,在这里就表示substring。REG_NOTBOL,初看不知道
BOL是什么,看是再看和它对称的REG_NOTEOL,根据经验,我们已经知道EOF是end of file,
那么这个EOL应该是end of line,那么相对地BOL就应该是beginning of line。

   BYTE OFFSETS
       Unless  REG_NOSUB was set for the compilation of the pattern buffer, it
       is possible to obtain substring match addressing  information.   pmatch
       must be dimensioned to have at least nmatch elements.  These are filled
       in by regexec() with substring match addresses.  Any  unused  structure
       elements will contain the value -1.

       The  regmatch_t  structure  which  is  the type of pmatch is defined in
       <regex.h>.

           typedef struct {
               regoff_t rm_so;
               regoff_t rm_eo;
           } regmatch_t;

       Each rm_so element that is not -1 indicates the  start  offset  of  the
       next  largest  substring  match  within the string.  The relative rm_eo
       element indicates the end offset of the match.

没错,先前我们猜测,regmatch_t对象表示匹配的位置信息,从regexec函数返回后,那组
regmatch_t对象后面无效的部分一定是用一个特殊值来表示无效,这个特殊值就是-1。匹配
位置信息包括起始位置和结束位置,再一猜就知道,rm_so表示regmatch start
offset,rm_eo表示regmatch end offset,要有这样的敏感性,rm_so和rm_eo,别的字母都
一样,就s和e不一样,表示相对概念的s和e就是start和end,这在程序代码中很常见。还有
一个很常见的现象是结构体成员名字有一个前缀是结构体名字的缩写,比如这里的rm_表示
regmatch。

Posix Error Reporting
       regerror() is used to turn the error codes that can be returned by both
       regcomp() and regexec() into error message strings.

       regerror() is passed the error code, errcode, the pattern buffer, preg,
       a pointer to a character string buffer, errbuf, and  the  size  of  the
       string buffer, errbuf_size.  It returns the size of the errbuf required
       to contain the null-terminated error message string.   If  both  errbuf
       and  errbuf_size  are  nonzero,  errbuf  is  filled  in  with the first
       errbuf_size - 1 characters of the error message and a terminating null.

POSIX Pattern Buffer Freeing
       Supplying  regfree()  with a precompiled pattern buffer, preg will free
       the memory allocated to the pattern buffer by  the  compiling  process,
       regcomp().

这也和先前猜测的一致。regerror是把错误码翻译成可读性好的字符串。regfree是把preg对
象中分配的内存释放掉。

RETURN VALUE
       regcomp()  returns  zero  for a successful compilation or an error code
       for failure.

       regexec() returns zero for a successful match or REG_NOMATCH for  fail‐
       ure.

man page为了保持形式上的整齐,把RETURN VALUE单独拿出来凑成一节,这一直让我觉得很
不舒服。如果在一个man page里描述了多个函数,那么每看完一个函数的说明都应该跳到这
里来看一下返回值是什么,而不是把其它函数的说明全部看完了再看这里。事实上这个man
page做得也不够整齐,regerror的返回值就写在上面的说明文字中而没有写在这里。可见把
返回值在最后单列出来很不符合书写和阅读习惯。现在这样搞得很不好,有的返回值单列在
后面,有的又写在说明文字中,看手册就得满世界找返回值在哪儿。我认为这是man page的
一大缺点。相反,让新手很不舒服的是man page太过简洁,并且没有代码例子,这不是man
page的缺点而应该是优点。

ERRORS
       The following errors can be returned by regcomp():

       REG_BADBR
              Invalid use of back reference operator.

       REG_BADPAT
              Invalid use of pattern operators such as group or list.

       REG_BADRPT
              Invalid  use  of  repetition  operators such as using ’*’ as the
              first character.

       REG_EBRACE
              Un-matched brace interval operators.

       REG_EBRACK
              Un-matched bracket list operators.

       REG_ECOLLATE
              Invalid collating element.

       REG_ECTYPE
              Unknown character class name.

       REG_EEND
              Non specific error.  This is not defined by POSIX.2.

       REG_EESCAPE
              Trailing backslash.

       REG_EPAREN
              Un-matched parenthesis group operators.

       REG_ERANGE
              Invalid use of the range operator, e.g., the ending point of the
              range occurs prior to the starting point.

       REG_ESIZE
              Compiled  regular  expression  requires  a pattern buffer larger
              than 64Kb.  This is not defined by POSIX.2.

       REG_ESPACE
              The regex routines ran out of memory.

       REG_ESUBREG
              Invalid back reference to a subexpression.

CONFORMING TO
       POSIX.1-2001.

有个学生看完了这一段之后问我,上面说regexec成功返回0失败返回
REG_NOMATCH,REG_NOMATCH这个错误码表示什么?怎么在ERRORS节中没有解释?这是一个典
型的没有理解到位的例子。上面说regcomp成功返回0失败返回错误码,却没有说返回哪些错
误码,而是详细列在ERRORS节中,regcomp失败的原因有很多,这些错误码大多是描述正则表
达式的各种语法错误的。而regexec是判断匹配不匹配的,匹配就返回0不匹配就返回
REG_NOMATCH,NOMATCH就是no match,这句话本身就说明了这个错误码是什么意思,所以就
没有在ERRORS节中再解释了,这也体现了man page的简洁性,一句废话都没有。

这个学生为什么会没有理解到位呢?还是因为对英文不敏感,REG_NOMATCH在他看来就是一串
大写字母,一个符号,而没看出来是no match,因此觉得这个符号必须在后面详细解释,而
没有想到这个符号用在这里是双关的,它自己就解释了自己。

SEE ALSO
       grep(1), regex(7), GNU regex manual

COLOPHON
       This page is part of release 2.77 of the Linux  man-pages  project.   A
       description  of  the project, and information about reporting bugs, can
       be found at http://www.kernel.org/doc/man-pages/.

GNU                               1998-05-08                          REGEX(3)

man page的最后这一段比较有价值的是SEE ALSO。由于每个man page都有自己的主题,而不
会去扯一些离题的话,有时候就需要把几个相关的man page结合起来看,从一系列的相关主
题中把握一个overview。有的man page有BUGS节,这也是非常重要的,最典型的是gets(3),
前面描述了半天这个函数是干吗用的,最后在BUGS节里面说,Never use gets(),如
果没看见这一句,前面的都白看。

转自:http://songjinshan.com/blog/index.php/2011/04/get-used-to-man-page-howto/

posted @ 2011-08-16 15:42  夏大王  阅读(2249)  评论(1编辑  收藏  举报