目录


第七章 格式

Perl 有一个机制帮助你产生简单的报告和图表.为了实现这个机制,Perl 帮助你格式化你 的输出,使它打印出来的时候看起来比较接近于你想要的结果.它能保持跟踪象一页里面有 多少行,当前的页码,以及什么时候打印页头等等的东西.使用的关键字是从 FORTRAN 里面借来的:format 用来声明而 write 用来执行;参看第二十九章,函数,获取相关 内容.所幸,布局时非常易读的,很象 BASIC 中的 PRINT USING 语句.也可以将它想象 成 nroff(如果你知道 nroff,这也许不象是一个比较).

格式输出,和包和子过程一样,是声明而不是执行,因此它们可以在你的程序中任何地方 出现.(通常最好将所有的格式输出放在一起).它们有他们自己的名字空间,与 Perl 中 其它类型的名字空间是区分开来的.这就是说如果你有一个函数 "Foo",但它不同于一个 名字为 "Foo" 的格式输出.然而和一个文件句柄相关联的格式输出的缺省名字和该文件 句柄的名字相同.因而,STDOUT 的缺省格式输出的名字是 "STDOUT",文件句柄 TEM P的 缺省格式输出名字为 "TEMP",它们看起来是一样的,实际上是不一样的.

输出纪录格式输出象下边一样定义:


   format NAME =
   FORMLIST
   .

如果省略 NAME,将定义格式输出 STDOUT.FORMLIST 由一些有序的行组成,每一行都是 下面三种类型中的一种:

  1. 注释,以第一列为 # 来表示.
  2. 一个格式行,用来定义一个输出行的格式
  3. 参数行,用来向前面的格式行中插入值

格式行除了那些需要被替换的部分外,严格按照它们的声明被输出.(注:而且,甚至那些 你放进去维护列完整性的域也如此.在一个格式行中没有任何东西可以导致域的伸缩或者 移位.你看到的列是按照 WYSIWYG 的概念分布的---假设你用的是定宽字体.就连控制字符 都假设是宽度为一的字符.)格式行中每个被替换的部分分别以 @ 或者 ^ 开头.这些行 不作任何形式的变量代换.@ 域(不要同数组符号 @ 相混淆)是普通的域.另一种域,^ 域 用来进行多行文本块填充.域的长度通过在格式符号 @,^ 后跟随特定长度的 <, >,| 来定义,同时,<,>,| 还分别表示,左对齐,右对齐,居中对齐.如果变量超出定义的 长度,那么它将被截断.

作为右对齐的另外一种方式,你可以使用 #(在 @ 或 ^ 后边)来指定一个数字域.你可以在 这种区域中插入一个 . 来制定小数点的位置.如果这些区域的值包含一个换行符,那么 只输出换行符前面的文本.最后,特殊的域 @* 可以被用来打印多行不截断的值;这种区域 通常在一个格式行中出现.

参数行指定参数的顺序必须跟相应的格式行的域顺序一致.不同参数的表达式需要使用逗号 分隔.参数行被处理之前所有的参数表达式都在列表环境中求值,因此单个列表表达式会 产生多个列表元素.通过使用圆括弧将表达式括起来,可以使表达式扩展到多行 (因此, 圆括弧必须是第一行的第一个标志).这样就可以将值同相应的格式域对应起来方便阅读.

如果一个表达式求出的值是一个有小数部分的数字,并且如果对应的格式域指定了输出的 小数部分的格式(除了没有内嵌 . 的多个 # 字符的格式),用来表示小数点的字符总是由 LC_NUMERIC 区域参数确定.这就是,如果运行时环境恰好是德国本地化参数,一个逗号 总是用来替代句点.参看 perllocale 手册页获取更多的内容.

在一个表达式中,空白字符 \n,\t,和 \f 总是被解释成单个空格.因而,你可以认为 这样的过滤表达式作用于每个格式中的表达式:

   $value =~ tr/\n\t\f/ /;

余下的空白字符,\r, 如果格式行允许的话,将强制输出一个换行符.

以 ^ 开头的格式域不同于 @ 格式域,它会被特殊对待.例如一个 # 区域,如果值没有 定义,那么这个区域将变为空白.对于其他的区域类型,^ 会使用一种特殊的填充模式. 提供的值必须是一个包含字符串的标量变量名,而不是一个强制表达式.Perl 在这个区域 中放入尽可能多的文本,并且将已经打印过的字符截去,这样当下次引用该变量的时候, 就可以打印更多的文本.(这就是说在 write 调用过程中,变量本身将发生变化,原来的 值将不被保留.因此如果你想保持最初的值,你需要使用一个临时变量来代替原来的变量). 通常你应该使用一组垂直对齐的格式区域打印一块文本.你也许会想用文本 "..." 来结束 最后的区域,这样当文本太长不能完整地打印的时候,指定的文本将会被打印.你也可以 改变变量 $:(当你使用 English 模块,那么就是 $FORMAT_LINE_BREAK_CHARACTERS)来 改变用来表示中断的合法字符.

使用 ^ 区域能够产生变长的纪录.如果格式区域太短,你可以重复几次带有 ^ 区域的格式 行.如果你用这个方法处理一块较短的数据,那么你将会得到几个空白输出行.为了避免 空白行,你可以在格式行中的任意地方放置一个 ~ (波浪号).(在输出中波浪号本身会被 转换成一个空格).如果你使用第二个 ~ 波浪号,该格式行会被重复直到在该格式行中所有 域中的文本被用尽为止.(因为 ^ 区域会吃掉所要打印的字符串,因此前面的格式行能够 运行,但是如果你使用和两个波浪号结合的一个 @ 域,你最好每次给这表达式不同的值! 使用 shift 或者其他带有副作用的操作符,来用尽所有的值.)

标头的处理缺省使用当前文件句柄名加上 _TOP 后缀的格式来处理.它在每一页的开头被 触发.参看二十九章的 write.

# a report on the /etc/passwd file
format STDOUT_TOP =
                         Passwd File
Name                Login    Office   Uid   Gid Home
------------------------------------------------------------------
.
format STDOUT =
@<<<<<<<<<<<<<<<<<< @||||||| @<<<<<<@>>>> @>>>> @<<<<<<<<<<<<<<<<<
$name,              $login,  $office,$uid,$gid, $home
.

# a report from a bug report form
format STDOUT_TOP =
                         Bug Reports
@<<<<<<<<<<<<<<<<<<<<<<<     @|||         @>>>>>>>>>>>>>>>>>>>>>>>
$system,                      $%,         $date
------------------------------------------------------------------
.
format STDOUT =
Subject: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
         $subject
Index: @<<<<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
       $index,                       $description
Priority: @<<<<<<<<<< Date: @<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
          $priority,        $date,   $description
From: @<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
      $from,                         $description
Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
             $programmer,            $description
~                                    ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                                     $description
~                                    ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                                     $description
~                                    ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                                     $description
~                                    ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
                                     $description
~                                    ^<<<<<<<<<<<<<<<<<<<<<<<...
                                     $description
.

除非一个格式是在词法变量的作用范围内定义,否则该词法变量在格式中是不可见的.

在同一个输出通道中将 print 和 write 混合起来是可以的,但是你必须自己操作特殊变量 $- (在 English 模块中是 $FORMAT_LINES_LEFT).

7.1.0 格式变量

当前的格式名字存储在 $~ 中 ($FORMAT_NAME),当前的表头格式名字存储在 $^ ($FORMAT_TOP_NAME).当前输出的页号在 $% ($FORMAT_PAGE_NUMBER),每页中的行数在 $= ($FORMAT_LINES_PER_PAGE).是否自动刷新输出缓冲区存储在 $| ($FORMAT_AUTOFLUSH). 在每一页(除了第一页)表头之前需要输出的字符串存储在 $^L ($FORMAT_FORMFEED).这些 变量以文件句柄为基础设定,因此你需要 select 与特定格式关联的文件句柄来影响这些 格式变量:

   select((select(OUTF),
      $~ = "My_Other_Format",
      $^ = "My_Top_Format"
      )[0]);

是不是很难看?可是这是一个习惯用法,因此当你看见它时不要感到惊讶.你至少可以使用 一个临时变量来保持前一个文件句柄:

   $ofh = select(OUTF);
   $~   = "My_Other_Format";
   $^   = "My_Top_Format";
   select($ofh);

通常这是一个更好的方式,因为这不仅仅是增加了可读性, 但是你现在在代码有了一个 中间语句,这样你可以在单步调试的时候可以在这里停下来,如果你使用 English 模块, 你甚至可以这样读取变量名字:

   use English;
   $ofh = select(OUTF);
   $FORMAT_NAME    = "My_Other_Format";
   $FORMAT_TOP_name = "My_Top_Format";
   select($ofh);

但是你仍然要调用这些 select,如果你想避免使用他们,使用 Perl 集成的 FileHandle? 模块.现在你就可以使用小写的方法名来访问这些特殊变量:

   use FileHandle;
   OUTF->format_name("My_Other_Format");
   OUTF->format_top_name("My_Top_Format");

这样看起来更好!

因为跟在格式行后面的数值行可以包含任意的表达式(提供给 @ 域,而不是 ^ 域),所以 你可以使用一些高级的处理,象 sprintf 或者一个你自己的函数.例如为了在一个数字 里面插入一些逗号,你可以使用下面的方法:


format Ident = 
    @<<<<<<<<<<<<<<<
    commify($n)
.

为了在格式区域的实际输出中得到一个真的 @,~ 或者 ^,可以象下面一样:

format Ident = 
I have an @ here.
         "@"
.

将整行文本居中,可以使用下面的方法:

format Ident = 
@||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
                          "Some text line"
.

> 域长度标识保证在域中的文本将是右对齐,但只是在你定义的域内精确地对齐.Perl 中
没有一种内置的方法可以获得 "将这个区域在页右侧对齐,而不管它的宽度" 这样的效果, 你必须指定用于对齐的左侧位置.你可以基于当前列号(不提供)来产生它们自己的格式, 来达到上面的目的,然后 eval 它:

$format  = "format STDOUT = \n"
         . '^' . '<' x $cols . "\n"
         . '$entry' . "\n"
         . "\t^" . "<" x ($cols-8) . "~~\n"
         . '$entry' . "\n"
         . ".\n";
print $format if $Debugging;
eval $format; 
die $@ if $@;

上边最重要的行恐怕就是 print.print 将打印出下边这样的格式:

format STDOUT = 
^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$entry
    ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~
$entry
.

这里有一个小程序来达到 fmt(1) Unix 工具的功能:


format = 
^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ~~
$_

.

$/ = "";
while (<>) {
    s/\s*\n\s*/ /g;
    write;
}

7.2.0 页脚

$^ ($FORMAT_TOP_NAME) 包含了当前表头格式的名称,目前没有对应的机制来自动获得页脚 的定义.除非你认为它是一个主要问题,否则你不会知道一个格式将会有多大.它现在已经 在我们的 TODO 列表里面.(注:不过这并不能保证我们就一定会做它.格式在 WWW, Unicode,XML,XSLT,和任何它们后来的事物占统治地位的时代显得有些过时了.)

这里由一个策略:如果你有一个给定尺寸的页脚,你可以在每个 write 之前通过检查 $- ($FORMAT_LINES_LEFT) 来获得页脚,然后你自己打印页脚.

也有另一个策略:使用 open(MESELF, "|-") 打开一个指向你自己的管道并且总是 write 到 MESELF 而不是 STDOUT.让你的子过程处理它的 STDIN 来重新安排页头和页脚.这种 方法不是很方便,但是的确可以运行.

7.2.1 访问格式的内部

为了对内部格式化机制进行低级访问,你可以使用内建的 formline 操作符和直接访问 $^A ($ACCUMULATOR 变量).(格式最终编译成为一系列的对 formline 的调用)例如:

$str = formline <<'END', 1,2,3;
@<<<  @|||  @>>>
END

print "Wow, I just stored `$^A' in the accumulator!\n";

或者创建一个 swrite 子过程,它对于 write 的作用就像 sprintf 对 printf 的作用, 可以象下边的代码一样使用:

use Carp;
sub swrite {
        croak "usage: swrite PICTURE ARGS" unless @_;
        my $format = shift;
        $^A = "";
        formline($format, @_);
        return $^A;
}

$string = swrite(<<'END', 1, 2, 3);
Check me out
@<<<  @||| @>>>
END
print $string;

如果你在使用 FileHandle? 模块,你可以使用象下边代码一样使用 formline,使一块文本 在第 72 列处折行.

   use FileHandle;
   STDOUT->formline("^" . ("<" x 72 ) . "~~\n", $long_text);



to top