第七章 格式
Perl 有一个机制帮助你产生简单的报告和图表.为了实现这个机制,Perl 帮助你格式化你
的输出,使它打印出来的时候看起来比较接近于你想要的结果.它能保持跟踪象一页里面有
多少行,当前的页码,以及什么时候打印页头等等的东西.使用的关键字是从 FORTRAN
里面借来的:format 用来声明而 write 用来执行;参看第二十九章,函数,获取相关
内容.所幸,布局时非常易读的,很象 BASIC 中的 PRINT USING 语句.也可以将它想象
成 nroff(如果你知道 nroff,这也许不象是一个比较).
格式输出,和包和子过程一样,是声明而不是执行,因此它们可以在你的程序中任何地方
出现.(通常最好将所有的格式输出放在一起).它们有他们自己的名字空间,与 Perl 中
其它类型的名字空间是区分开来的.这就是说如果你有一个函数 "Foo",但它不同于一个
名字为 "Foo" 的格式输出.然而和一个文件句柄相关联的格式输出的缺省名字和该文件
句柄的名字相同.因而,STDOUT 的缺省格式输出的名字是 "STDOUT",文件句柄 TEM P的
缺省格式输出名字为 "TEMP",它们看起来是一样的,实际上是不一样的.
输出纪录格式输出象下边一样定义:
format NAME = FORMLIST .
如果省略 NAME,将定义格式输出 STDOUT.FORMLIST 由一些有序的行组成,每一行都是
下面三种类型中的一种:
- 注释,以第一列为 # 来表示.
- 一个格式行,用来定义一个输出行的格式
- 参数行,用来向前面的格式行中插入值
格式行除了那些需要被替换的部分外,严格按照它们的声明被输出.(注:而且,甚至那些
你放进去维护列完整性的域也如此.在一个格式行中没有任何东西可以导致域的伸缩或者
移位.你看到的列是按照 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);