第二十九章,函数 (S-Y)

29.2 按照字母顺序排列的 Perl 函数.

29.2.136 s///

替换操作符。参阅第五章里的“模式匹配操作符”。

29.2.137. scalar

这个伪函数可以用于 LIST 里,当在列表环境中计算会生成一个不同的结果的时候,强迫 EXPR 在 标量环境中计算。比如:

   my ($nextvar) = scalar ;

避免 在做赋值之前从标准输入把所有的行都读了进来,因为给一个列表(甚至是一个 my 列表)赋值都会产生一个列表环境。(在这里例子里如果没有 scalar,那么来自 的 第一行仍然会赋予 $nextvar,但是随后的行将会被读取并抛弃,因为我们赋值的目标列表只能接受 一个标量数值。)

当然,简单些而且没有那么混乱的方法是不使用圆括弧,这样就把标量环境改变成了列表环境:

   my $nextvar = ;

因为 print 函数是一个 LIST 操作符,所以如果你想把 @ARRAY 的长度打印出来,那么你不得不 说:

   print "Length is ", scalar(@ARRAY), "\n";

Perl 里没有与 scalar 对应的 “list”函数,因为实际上我们从来不需要强迫在列表环境里 计算。这是因为任何需要 LIST 的操作已经给他的列表参数免费提供了一个列表环境。

因为 scalar 是单目操作符,如果你不小心给 EXPR 使用了圆括弧的列表,那么这个东西的行为 就象一个标量逗号表达式一样,在空环境中计算除最后一个列表元素之外的所有其他元素,然后 返回在标量环境中计算的最后一个元素。你很少想要这样的东西。下面的一个语句:

   print uc(scalar(&foo, $bar)), $baz;

在道义上是等效于下面两个的:

   &foo;
   print(uc($bar), $baz);

参阅第二章获取关于逗号操作符的更多细节。参阅第六章的“原型”获取关于单目操作符更多的 信息。

29.2.138 seek

这个函数为 FILEHANDLE 定位文件指针,就好象用于标准 I/O 的 fseek(3) 调用一样。文件里的 第一个位置是在偏移量 0 处,而不是 1 处。同样,偏移量指的是字节位置,而不是行数。通常, 因为行的长度是变化的,所以我们不可能不检查到该点之间的所有文件内容就能访问某一行,除非 你的所有行数都已知是特定的长度,或者你已经做了一个把行数转换成字节数的索引。(同样的 限制也适用于有着变长字符编码的字符位置:操作系统不知道什么是字符,它们只知道字节。)

FILEHANDLE 可以是一个表达式,其值给出实际的文件句柄的名字或者是一个指向任何类似文件 句柄对象的引用。该函数成功时返回真,失败时返回假。为了方便,该函数可以从各种文件位置 计算偏移量。WHENCE 的值声明你的 OFFSET 使用文件的哪个偏移量做它的开始位置:0,文件 开头;1 文件的当前位置;2,文件结尾。如果 WHENCE 的值是 1 或 2。那么 OFFSET 可以为 负值。如果你喜欢用 WHENCE 的符号值,你可以用来自 IO::Seekable 或者 POSIX 模块的 SEEK_SET,SEEK_CUR,和 SEEK_END,或者在 Perl 5.6 里的 Fcntl 模块。

如果你想为 sysread 或者 syswrite 定位文件,那么不要使用 seek;标准 I/O 缓冲技术会令 seek 对文件在系统位置上的作用变得不可预料而且也不能移植。应该用 sysseek。

因为 ANSI C 的规则和严格,在一些系统上,如果你在在读取和写出之间做切换,那么你必须做 一次搜寻。这样做的效果就好象调用标准 I/O 库的 clearerr(3) 函数。你可以用 WHENCE 为 1 (SEEK_CUR)和 OFFSET 为 0 实现这个目的而又不会移动文件位置:

   seek(TEST, 0, 1);

这个函数的一个有趣的用途是允许你跟随文件的增长,比如:

   for (;;) {
      while () {
         grok($_);         # 处理当前行
      }
      sleep 15;
      seek LOG, 0, 1;         # 重置 end-of-file 错误。
   }

最后一个 seek 在不移动指针的情况下清理文件结束错误。因你的 C 库的标准 I/O 实现的标准 程度的不同而异,你可能需要一些更象下面这样的东西:

   for (;;) {
      for ($curpos = tell FILE;   ;   $curpos = tell FILE) {
         grok($_);         # 处理当前行
      }
      sleep $for_a_while;
      seek FILE, $curpos, 0;      # 重置 end-of-file 错误。
   }

类似的策略可以用于在一个数组里记住 seek 每一行的地址。

29.2.139 seekdir

这个函数为下一次 readdir 对 DIRHANDLE 的调用设置当前位置。POS 必须是 telldir 返回的 值。这个函数与对应的系统库过程在可能的目录压缩问题上有相同的注意事项。该函数可能不是在 所有实现了 readdir 的系统上都实现了。而且如果 readdir 没有实现,它也肯定没实现。

29.2.140 select (输出文件句柄)

由于历史原因,Perl 里有完全互不相关的两个 select 操作符。参阅下一节获取另外一个的描述。 这个版本的 select 操作符返回当前选定的输出操作符,并且,如果你提供了 FILEHANDLE,那么 把它设置为当前缺省输出操作符。这样做有两个效果:首先,一个没有文件句柄的 write 或者 print 将缺省输出到这个 FILEHANDLE。其次,与输出相关的特殊变量将指向这个文件句柄。 比如,如果你想为多个输出文件句柄设置了相同的页顶格式,那么你可能需要做这些:

   select REPORT1;
   $^ = 'MyTop';
   select REPROT2;
   $^ = 'MyTop';

但请注意这样就把 REPORT2 当作当前选定的文件句柄了。这种做法可以说是反社会的做法,因为 它可能会真的把一些其他过程的 print 或者 write 语句搞坏。写的好的库过程会在退出的时候把 当前选定文件句柄设置为和进入过程时相同的那个。为了支持这个,FILEHANLDE 可以是一个 表达式,该表达式的值给出实际文件句柄的名字。因此,你可以用下面的代码保存并恢复当前 选顶的文件句柄:

   my $oldfh = select STDER; $| = 1; select $oldfh;

或者使用惯用的但有点模糊的方法:

   select((select(STDERR), $| = 1)[0])

这个例子是这样运转的:制作一个由 select(STDERR) (副作用是选定了 STDERR)的返回值和 $|=1 (它总是 1)组成的列表,但同时,又作为副作用设置了现在选定的 STDERR 的自动冲刷。 该列表的第一个元素(前面那个选定的文件句柄)现在用做外层 select 的一个参数。古怪吧? 这些足够让你知道 List 比较危险了。

你还可以使用标准的 SelectSaver? 模块在退出范围的时候自动恢复前面一个 select。

不过,虽然我们给你解释了上面的全部东西,我们还是可以指出在当今的情况下,你很少需要使用 这种形式的 select,因为你象设置的大多数特殊变量都有面向对象的封装方法帮你做这些事情。 所以,你不用直接设置 $|,而是:

   use IO::Handle;      # 糟糕的是,这可不是个*小*模块。
   STDOUT->autoflush(1);

而前面的格式化例子可以这样编码:

   use IO::Handle;
   REPORT1->format_top_name("MyTop");
   REPORT2->format_top_name("MyTop");

29.2.141 select (准备文件描述符)

四个参数的 select 和前面那个 select 完全无关。这个操作符用于发现你的文件描述符中那个 (如果有的话)已经准备好做输入或者输出了,或者报告一个例外条件。(这样就避免你做轮询。 )它用你声明的位掩码调用 select(2) 系统调用,你可以用 fileno 和 vec 构造这个位掩码, 象这样:

   $rin = $win = $ein = "";
   vec($rin, fileno(STDIN), 1) = 1;
   vec($win, fileno(STDIN), 1) = 1;
   $ein = $rin | $win;

如果你想在许多文件句柄上 select,你可能会写这样的子过程:

   sub fhbits {
      my @fhlist = @_;
      my $bits;
      for (@fhlist) {
         vec($bits, fileno($_), 1) = 1;
      }
      return $bits;
   }
   $rin = fhbits(qw(STDIN TTY MYSOCK));

如果你想重复使用同样的位掩码(这样做更高效),常用的惯用法是:

   ($nfound, $timeleft) = 
      select($rout=$rin, $wout=$win, $eout=$ein, $timeout);

或者阻塞住直到任意文件描述符准备好:

   $nfound = select($rout=$rin, $wout=$win, $eout=$ein, undef);

如你所见,在标量环境中调用 select 只返回 $nfound,找到的准备好的文件描述符数量。

可以用 $wout=$win 技巧是因为一个赋值语句的数值是它自身的左边,因此 $wout 先被赋值删除, 然后又被 select 删除,而 $win 不会变化。

这些参数任何一个都可以是 undef,这个时候它们被忽略。如果 TIMEOUT 不是 undef,那么就是 以秒计,而且可以是分数。(超时时间为 0 的效果就是轮询。)不是所有实现都能返回 $timeleft。如果不能返回 $timeleft,那么它总是返回等于你提供的 $timeout 的 $timeleft。

标准的 IO::Select 模块提供了 select 的更加友善的接口,主要是因为它为你做了所有你能用的 位掩码。

select 的一个应用就是实现比 sleep 分辨率更好的睡眠。要实现这个目的,声明所有位映射为 undef。因此,如果要睡眠(至少)4.75 秒钟,用:

   select undef, undef, undef, 4.75;

(在一些非 Unix 系统上,三个 undef 的形式可能不能用,你可能需要为一个有效的描述符至少 伪装一个位掩码,而那个描述符可以是从来准备不好的。)

我们不应该把缓冲的 I/O(比如 read 或 )和 select 混在一起用,除了 POSIX 允许 的以外,而且就算 POSIX 允许,也只能在真正的 POSIX 系统上使用。这时应该用 sysread。

29.2.142 semctl

这个函数调用 System V IPC 函数 semctl(2)。你可能得先说 use IPC::SysV 以获取正确的 常量定义。如果 CMD 是 IPC_STAT 或者 GETALL,那么 ARG 必须是一个它可以保存返回的 semid_ds 结构或信号灯数值数组的变量。和 ioctl 和 fcntl 一样,返回值用 undef 代表 错误,“0 but true”代表零,其他情况则返回实际数值。

又见 IPC::Semaphore 模块。这个函数只有在那些支持 System V IPC 的机器上才能用。

29.2.143 semget

这个函数调用 System V IPC 系统调用 semget(2)。在调用之前,你应该 use IPC::SysV 以 获取正确的常量定义。该函数返回信号灯 ID,或者如果有错误返回 undef。

又见 IPC::Semaphore 模块。这个函数只能在那些支持 System V IPC 的机器上用。

29.2.144 semop

这个函数调用 System V IPC 系统调用 semop(2) 以执行信号灯操作,比如发信号和等待等等。 在调用之前,你应该使用 use IPC::SysV 以获取正确的常量定义。

OPSTRING 必须是一个 semop 结构的打包的数组。你可以通过说 pack("s*", $semnum, $semop, $semflag)做每一个 semop 结构。信号灯操作的数量是由 OPSTRING 的长度隐含的。该函数在 成功的时候返回真,在失败的时候返回假。

下面的代码在等待信号灯 id 为 $semid 的信号灯 $semnum:

   $semop = pack "s*", $semnum, -1, 0;
   semop $semid, $semop or die "Semaphore trouble: $!\n";

要给信号灯发出信号,只需要把 -1 换成 1 就可以了。

参阅第十六章的“System V IPC”一节。又见 IPC::Semaphore 模块。这个函数只有支持 Systerm V IPC 的机器上可以用。

29.2.145 send

这个函数在套接字上发送一条信息。它和同名系统调用接收相同的标志——参阅 send(2)。在 未联接的套接字上,你必须声明一个要发送的目的 TO,这样就会令 Perl 的 send 象 sendto(2) 那样运行。C 的系统调用 sendmsg(2) 目前没有在标准的 Perl 里实现。send 函数 在成功时返回发送的字节数,失败时返回 undef。

(有些非 Unix 系统错误地把套接字当作与普通文件描述符不同的东西对待,结果就是你必须总是 在套接字上 send 和 recv,而不能使用方便的标准 I/O 操作符。)

我们中至少有一个人会常犯的错误就是把 Perl 的 send 和 C 的 send 和写混淆起来:

   send SOCK, $buffer, length $buffer      # 错

这行代码会莫名其妙地失败,具体情况取决于字串长度和系统需要的 FLAG 位之间的关系。参阅 第十六章中的“消息传递”一节。

29.2.146. setpgrp

这个函数为指定的 PID(对当前进程使用 PID 等于 0)设置当前进程组(PGRP)。如果在那些 没有实现 setpgrp(2) 的系统上调用 setpgrp 将会抛出一个例外。注意:有些系统上会总是忽略 你提供的参数并总是做 setpgrp(0, $$)。幸运的是,这些就是我们最常用的参数。如果省略了 参数,它们缺省是 0, 0。BSD 4.2 版本的 setpgrp 不接受任何参数,但在 BSD 4.4 里,它是 setpgid 函数的同义词。如果需要更好的移植性(从某种角度来看),直接使用 POSIX 模块里的 setpgid 函数。如果你实际上想干的事是把你的脚本作成守护进程,那么请考虑使用 POSIX::setsid() 函数。请注意 POSIX 版本的 getpgrp 并不接受参数,所以只有 setpgrp(0, 0) 是真正可以移植的。

29.2.147. setpriority

这个函数为 WHICH 和 WHO 里声明的一个进程,进程组,或者一个用户设置当前 PRIORITY,参阅 setpriority(2)。在那些没有实现 setpriority(2) 的机器上调用 setpriority 将抛出一个 例外。要把你的程序“nice”下四个单位(和用 nice(1) 处理你的程序一样),用:

   setpriority 0, 0, getpriority(0, 0) + 4;

一个给定的优先级的解释可能会因不同的系统而异。有些权限可能是那些非特权用户所不能使用的。

又见 CPAN 的 BSD::Resource 模块。

29.2.148. setsockopt

这个函数设置你需要的套接字选项。出错时该函数返回 undef。LEVEL 表示你的调用瞄准的是 哪一个协议层。或者就是 SOL_SOCKET,指向在所有层之上的套接字本身。如果你不想传递参数, 那么可以把 OPTVAL 声明为 undef。在套接字上一个常用的选项是 SO_REUSEADDR,这样才能绕开 因为前一个在该端口的 TCP 联接仍然认为固执地认为它在关闭的时候,我们不能绑定特定的地址的 问题。它看起来象这样:

   use Socket;
   socket(SOCK, ...) or die "Can't make socket: $!\n";
   setsocket(SOCK, SOL_SOCKET, SO_REUSEADDR, 1)
      or warn "Can't do setdosockotp: $!\n";

参阅 setsockopt(2) 获取其他可能数值。

29.2.149 shift

这个函数把数组的第一个值移出并且返回它,然后把数组长度减一并且把所有的东西都顺移。 如果在数组中不再存在元素,它返回 undef。

如果省略了 ARRAY,那么该函数在子过程和格式的词法范围里移动 @_;它在文件范围(通常是主程 序)或者在由 eval STRING,BEGIN { },CHECK { },INIT { },和 END {} 这样的构造里面的 词法范围里移动 @ARGV。

子过程通常以拷贝它们的参数到词法变量里开始,而 shift 可以用于这个目的:

   sub marine {
      my $fathoms = shift;      # 深度
      my $fishies   = shift;   # 鱼的数量
      my $o2           = shift;   # 氧气问题
      # ...
   }

shift 还可以用于在你的程序前面处理参数:

while (defined($_ = shift)) {
        /^[^-]/     && do { unshift @ARGV, $_; last };
        /^-w/       && do { $WARN = 1;         next };
        /^-r/       && do { $RECURSE = 1;      next };
        die "Unknown argument $_\n";
}

你还可以考虑使用 Getopt::Std 和 Getopt::Long 模块来处理程序参数。

又见 unshift,push,pop,和 splice。shift 和 unshift 函数在数组左边做的事情和 pop 和 push 在数组右边干的事情是一样的。

29.2.150 shmctl

这个函数调用 System V IPC 系统调用 shmctl(2)。在调用之前,你应该 use IPC::SysV 以 获取正确的常量定义。

如果 CMD 是 IPC_STAT,那么 ARG 必须是一个将要保存返回的 shmid_ds 结构的变量。跟 ioctl 和 fcntl 一样,该函数错误时返回 undef,“0 but true”表示零,其他情况下返回 实际返回值。

该函数只能在那些支持 System V IPC 的机器上用。

29.2.151 shmget

这个函数调用 System V IPC 系统调用 shmget(2)。该函数返回共享内存段的 ID,如果有错误 则返回 undef。在调用之前,先 use SysV?::IPC。

该函数只能在那些支持 System V IPC 的机器上用。

29.2.152 shmread

这个函数从共享内存段 ID 的位置 POS 处开始读取 SIZE 大小的数据(方法是附着在该内存段 上,拷贝出数据,然后与该内存段分离。)。VAR 必须是一个将保存读取出的数据的变量。如果 成功,该函数返回真,如果失败返回假。

该函数只能在那些支持 System V IPC 的机器上用。

29.2.153 shmwrite

这个函数向共享内存段 ID 的位置 POS 处开始写入 SIZE 大小的数据(方法是附着在该内存段 上,拷贝入数据,然后与该内存段分离。)。如果 STRING 太长,那么只写入 SIZE 字节;如果 STRING 太短,那么在后面补空直到 SIZE 字节。如果成功,该函数返回真,如果有错误,返回假。

该函数只能在那些支持 System V IPC 的机器上用。(你可能都读烦了——我们已经写烦了。)

29.2.154 shutdown

这个函数以 HOW 声明的方式关闭一个套接字联接。如果 HOW 为 0,那么不再允许进一步的接收。 如果 HOW 为 1,那么不再允许进一步的发送。如果 HOW 为 2,那么任何事情都不允许。

   
   shutdown(SOCK, 0);   # 不许再读
   shutdown(SOCK, 1);   # 不许再写
   shutdown(SOCK, 2);   # 不许再 I/O

如果你想告诉对端你完成数据写出了,但还没有完成数据读取,或者反过来,在这些情况下它都 非常有用。而且它还是一种更执着的关闭方式,因为同时还关闭任何这些文件描述符在派生出的 进程中的的拷贝。

让我们想象有一个服务器想读取它的客户端的请求,直到文件结尾,然后发送一个回答。如果 客户端调用 close,那么该套接字现在将不能用于 I/O,因此不会有回答能送回来。因此,客户端 应该使用 shutdown 以半关闭这次联接:

   print SERVER "my request\n";   # 发送一些数据
   shutdown(SERVER, 1);         # 发送完毕,没有更多要发的东西了
   $answer = < SERVER >;         # 但你还可以读

(如果你找到这里是为了找到关闭你的系统的办法,那么你就要执行一个外部的程序干这件事。 参阅 system。)

29.2.155. sin

抱歉,这个操作符什么罪都没犯(译注:英文“sin”也有“罪恶”的含义)。它只是返回 EXPR (用弧度表示)的正弦。

如果需要正弦的逆操作,你可以使用 Math::Trig 或者 POSIX 模块的 asin 函数,或者用下面的 关系:

   sub asin { atan2($_[0], sqrt(1 - $_[0] * $_[0])) }

29.2.156 sleep

这个函数令脚本睡眠 EXPR 秒,如果没有 EXPR 则是永久睡眠,并且返回睡眠的秒数。 你可以 通过给该进程发送一个 SIGALRM 的方法来中断睡眠。在一些老式系统里,它可能比你要求的描述 整整少睡一秒,具体情况取决于它是如何计算秒的。大多数现代的系统都是睡足秒数。不过,在 这些系统上它们很有可能睡眠的时间要长一些,因为在一台繁忙的多任务系统上,你的系统可能 无法马上得到调度。如果可能,select (等待文件描述符)调用可以给你更好的分辨率。你还 可以用 syscall 调用一些 Unix 系统支持的 getitimer(2) 和 setitimer(2) 过程。你不应该 混合 alarm 和 sleep 调用,因为 sleep 通常是用 alarm 实现的。

又见 POSIXE 模块的 sigpause 函数。

29.2.157 socket

这个函数打开一个指定类型的套接字,并且把它附着在 SOCKET 文件句柄上。DOMAIN,TYPE,和 PROTOCOL 都是和 socket(2) 一样的声明。如果没有定义 SOCKET,那么 Perl 将自动激活它。 在使用这个函数之前,你的程序应该包含下面这行:

   use Socket;

它给你正确的常量。该函数成功时返回真,参阅在第十六章里的“套接字”节里的例子。

在那些支持对文件的 exec 时关闭(close-on-exec)的系统上,该标记将为新打开的文件描述符 设置,就象 $^F 判定的那样。参阅第二十八章里的 $^F($SYSTEM_FD_MAX)。

29.2.158 socketpair

这个函数在声明的域中创建一个指定类型的匿名套接字对。DOMAIN,TYPE,和 PROTOCOL 都和 socketpair(2) 里声明的一样。如果两个套接字参数都没有声明,那么它们自动激活。该函数成功 时返回真,失败时返回假。在那些没有实现 socketpair(2) 的系统上,调用这个函数会抛出一个 例外。

这个函数的通常用法是在 fork 之前使用。生成的进程中有一个关闭 SOCKET1,而另外一个关闭 SOCKET2。你可以双向使用这些套接字,而不象 pipe 函数创建的文件句柄那样是单向的。有些 系统用 socketpair 的方式定义 pipe,这时候调用 pipe(Rdr, Wtr) 相当于:

   use Socket;
   socketpair(Rdr, Wtr, AF_UNIX, SOCK_STREAM, PF_UNSPEC);
   shutdown(Rdr, 1);      # 不允许读者写
   shutdown(Wtr, 0);      # 不允许写者读

在那些支持对文件的 exec 时关闭(close-on-exec)的系统上,该标记将为新打开的文件描述符 设置,就象 $^F 判定的那样。参阅第二十八章里的 $^F($SYSTEM_FD_MAX)。又见在第十六章里 的“双向通讯”一节尾部的例子。

29.2.159 sort

这个函数对 LIST 进行排序并返回排好序的列表值。缺省时,它以标准字串比较顺序排序(未定义 数值排在已定义空字串前面,而空字串又在其他任何东西前面)。如果 use locale 用法起作用, 那么 sort LIST 根据当前的区域集的数值对 LIST 排序。

如果给出了 USERSUB,那么它就是一个返回小于,等于,或者大于 0 的整数的子过程名字,具体 返回什么取决于列表中的元素应该如何排序。(很便利的 <=> 和 cmp 操作符可以用于执行三向 数字和字串比较。)如果给出了 USERSUB,但该函数未定义,那么 sort 抛出一个例外。

为了提高效率,绕开了通常用于子过程的调用代码,这样就有了下面的结果:这个子过程不能是 递归子过程(你也不能用一个循环控制操作符退出该块或者过程),并且将要接受比较的两个元素 不是通过 @_ 传递进子过程的,而是通过临时设置 sort 编译所在的包的全局变量 $a 和 $b( 参阅后面的例子)。变量 $a 和 $b 是真实值的别名,所以不要在子过程中修改它们。

子过程要求的动作是比较。如果它返回的结果是不一致的(比如,有时候说 $x[1] 小于 $x[2], 而有时候说的正相反),那么结果就不会良好。(这也是你不能修改 $a 和 $b 的另外一个原因。)

USERSUB 可以是标量变量名字(未代换),这时,它的值要么是引用实际子过程的符号引用,要么 是硬引用。(符号名更好些,即使用了 use strict 'refs' 用法也如此。)在 USERSUB 的 位置,你可以提供一个 BLOCK 用做一个匿名内联排序子过程。

要做一次普通的数字排序,你可以说:

   sub numerically { $a <=> $b }
   @sortedbynumber = sort numerically 53, 29,11,32, 7;

要以降序排序,你可以简单地在 sort 后面应用 reverse,或者你可以在排序过程里把 $a 和 $b 反过来:

sub numerically { $a <=> $b }
@sortedbynumber = sort numerically 53,29,11,32,7;

   @descending = reverse sort numerically 53,29,11,32,7;

   sub reverse_numerically { $b <=> $a }
   @descending = sort reverse_numerically 53,29,11,32,7;

要对字串进行大小写不敏感的排序,在比较之前用 lc 处理 $a 和 $b:

    @unsorted = qw/sparrow Ostrich LARK catbird blueJAY/;
    @sorted = sort { lc($a) cmp lc($b) } @unsorted;

(在 Unicode 里,用 lc 做大小写规范化要比用 uc 好,因为有些语言里抬头体和大写是不一样 的。不过它对普通的 ASCII 排序没有什么影响,并且如果你想让 Unicode 能正确排序,那么你的 规范化过程可能要比 lc 更别致一些。)

对散列按照数值排序是 sort 函数的常用法之一。比如,如果 %sales_amount 散列记录部门销售 情况,那么在排序过程里做一次散列查找就可以让我们将散列键字根据它们的数值排序:

# 从销售额最高的部门到最低的部门

    sub bysales { $sales_amount{$b} <=> $sales_amount{$a} }

    for $dept (sort bysales keys %sale_amount) {
        print "$dept => $sales_amount{$dept}\n";
    }

你可以通过使用 || 或者 or 操作符级连多个比较的方法进行额外层次的排序。这种方法相当漂亮, 因为比较操作符通常在相等的时候返回 0,这样就令它们能落到下一个比较。下面,散列键字首先 根据它们相关的销售额排序,然后在根据键字本身进行排序(以处理有两个或多个部门销售额 相同的情况):

   sub by_sales_then_dept {
      $sales_amount{$b} <=> $sales_amount{$a}
         ||
      $a cmp $b
   }

   for $dept (sort by_sales_then_dept keys %sale_amount) {
      print "$dept => $sales_smount{$dept}\n";
   }

假设 @recs 是一个散列引用的数组,而这里每个散列包含象 FIRSTNAME,LASTNAME,AGE, HEIGHT,和 SALARY 这样的域。下面的过程把那些记录中的人们按照下面的顺序排列:先是财富, 然后是身高,然后是年龄(越小越靠前),最后是名字的字母顺序:

sub prospects {
    $b->{SALARY}    <=>  $a->{SALARY}
        ||
    $b->{HEIGHT}    <=>  $a->{HEIGHT}
        ||
    $a->{AGE}       <=>  $b->{AGE}
        ||
    $a->{LASTNAME}  cmp  $b->{LASTNAME}
        ||
    $a->{FIRSTNAME} cmp  $b->{FIRSTNAME}
}

@sorted = sort prospects @recs;

任何可以从 $a 和 $b 中得到的有用信息都可以在一个排序过程中比较的基础来用。比如,如果 多行文本要根据特定域来排序,那么可以在排序过程中使用 split 以获取该域:

   @sorted_lines = sort {
      @a_fields = split /:/, $a;      # 冒号分隔的域
      @b_fields = split /:/, $b;
         
      $a_fields[3] <=> $b_fields[3]   # 对第四个域进行能够数字排序,然后
         ||
      $a_fields[0] cmp $b_fields[0]   # 对第一个域进行能够字串排序,然后
         ||
      $b_fields[2] <=> $a_fields[2]   # 对第而个域进行行能够数字反向排序
      ...            # 等等
   } @lines;

不过,因为 sort 使用给 $a 和 $b 的不同的数值对多次运行排序过程,所以前面的例子将会比对 每一行都做多余的重新分裂。

为了避免发生象为了比较数据域导致的多次的行分裂带来的开销,我们可以在排序之前对每个值进行 一次操作,然后把生成的信息保存起来。下面,我们创建了一个匿名数组以捕获每一行以及该行的 分裂结果:

   @temp = map { [$_, split /:/] } @lines;

然后,我们对数组引用排序:

   @temp = sort {
      @a_fields = @$a[1..$#$a];
      @b_fields = @$b[1..$#$b];

      $a_fields[3] <=> $b_fields[3]   # 对第四个域进行能够数字排序,然后
         ||
      $a_fields[0] cmp $b_fields[0]   # 对第一个域进行能够字串排序,然后
         ||
      $b_fields[2] <=> $a_fields[2]   # 对第而个域进行行能够数字反向排序
      ...               # 等等
   } @temp;

在这个数组引用排完序之后,我们就可以从这个匿名数组里检索原始行了:

   @sorted_lines = map {$_->[0] } @temp;

概而括之,这个 map-sort-map 技巧,就是我们通常称之为 Schwartzian 变换的东西,可以用 一个语句实现:

   @sorted_lines = map { $_->[0] }
         sort {
            @a_fields = @$a[1..$#$a];
            @b_fields = @$b[1..$#$b];

            $a_fields[3] <=> $b_fields[3]
               ||
            $a_fields[0] <=> $b_fields[0]
               ||
            $b_fields[2] <=> $b_fields[2]
            ...
         }
         map { [$_, split /:/]} @lines;

不要把 $a 和 $b 定义成词法变量(用 my)。它们都是包全局变量(如果它们可以免于 use strict 对普通全局变量的限制)。不过你的确需要保证你的排序过程是在同一个包里的, 或者用调用者的包名字修饰 $a 和 $b。

我们已经说过,在 Perl 5.6 里你可以用标准的参数传递方法(以及不一样的是,用 XS 子过程做 排序子过程)写排序子过程,前提是你用一个 ($$) 的原型声明了这个排序子过程。并且如果你是 这么用的,那么实际上你是可以把 $a 和 $b 声明为词法变量的:

   sub numerically ($$) {
      my ($a, $b) = @_;
      $a <=> $b;
   }

将来,当完整的原型都实现了以后,你就可以只用说:

      
   sub numerically ($a, $b) { $a <=> $b}

然后我们或多或少就能回到开始的地方。

29.2.160 splice

这个函数从一个 ARRAY 中删除 OFFSET 和 LENGTH 指明的元素,并且,如果给出了LIST,则用 LIST 的元素替换它。如果 OFFSET 是负数,那么该函数从数组的后面向前数,但如果该值会伸到 数组开头的前面,那么就会抛出一个例外。在列表环境中,splice 返回从该数组中删除的元素。 在标量环境中,它返回最后删除的元素,而如果没有的话返回 undef。如果新元素的数量不等于 旧元素的数量,那么该数组根据需要伸缩,并且元素的位置根据衔接后的情况进行改变。如果省略 了 LENGTH,那么该函数从数组里删除从 OFFSET 开始的所有东西。如果省略了 OFFSET,那么该 数组在读取的时候清空。下面的等式成立(假设 $[ 为 0):

直接方法 splice 等效
push(@a, $x, $y) splice(@a, @a, 0, $x, $y)
pop(@a) splice(@a, -1)
shift(@a) splice(@a, 0, 1)
unshift(@a, $x, $y) splice(@a, 0, 0, $x, $y)
$a[$x] = $y splice(@a, $x, 1, $y)
(@a, @a = ()) splice(@a)

splice 函数还可以方便地用于切开传递给子过程的参数列表。比如,假设列表长度在列表之前传递:

   sub list_eq {            # 比较两个列表值
      my @a = splice(@_, 0, shift);
      my @b = splice(@_, 0, shift);
      return 0 unless @a == @b;   #  长度相同?
      while(@a) {
         return 0 if pop(@a) ne pop(@b);
      }
      return 1;
   }
   if (list_eq($len, @foo[1..$len], scalar(@bar), @bar)) { ... }

不过,拿数组引用来干这事更清晰一些。

29.2.161 spit

这个函数扫描字串中 EXPR 给出的分隔符,并且把该字串劈成一个子字串列表,在列表环境中返回 生成的列表值,或者在标量环境中返回子字串的数量。(注:标量环境同时还令 split 把它的 结果写到 @_,不过这个用法现在废弃了。)分隔符是用重复的模式匹配进行判断的,用的是 PATTERN 里给出的正则表达式,因此分隔符可以是任意大小,并且不一定在每次匹配都是一样的 字串。(分隔符不象平常那样返回;我们在本节稍后讨论例外情况。)如果 PATTERN 完全不能匹配 该字串,那么 split 把原始字串当作子字串返回。如果它匹配了一次,那么你就得到两个子字串, 以此类推。你可以在 PATTERN 里使用正则表达式修饰词,比如 /PATTERN/i,/PATTERN/x,等等。 如果你以模式 /^/ 进行分裂,那么就假设是 //m 修饰词。

如果声明了 LIMIT 并且是正的,该函数分裂成不超过那么多的域(当然如果它用光了分隔符,那么 是可以分裂成比较少的子字串的)。如果 LIMIT 是负数,那就把它当作声明了任意大的 LIMIT。 如果省略了 LIMIT 或者是零,那么将从结果中删除结尾的空域(那些潜在的 pop 用户应该好好 记住)。如果省略了 EXPR,那么该函数就分裂 $_ 字串。如果还省略了 PATTERN 或者它是一个 文本空格,“ ”,那么该函数对空格进行操作,/\s+/,但是忽任何开头的空格。

可以分裂任意长度的字串:

   @chars  = split //,    $word;
   @fields  = split /:/,   $line;
   @words = split " ",   $paragraph;
   @lines = split /^/,   $buffer;

一个可以匹配空串或者其他的一些比空串长的字串的模式(比如,一个由任意一个字符加上 * 或者 ? 修饰的模式)将把 EXPR 的值分裂成独立的字符,只要它匹配字符之间的空串;非空匹配会象 通常的情况那样忽略匹配过的分隔符字符。(换句话来说,一个模式不会在一个点匹配多过一次, 即使它和一个零宽匹配也如此。)比如:

   print join ':', split / */, 'hi there';

生成输出“h:i:t:h:e:r:e”。空白消失了是因为它作为分隔符一部分匹配。举一个小例子,空模式 // 简单 地分裂成独立的字符,而空格并不消失。(对于正常模式匹配而言,// 模式会在上一次成功匹配处重复, 但是 split 的模式免受此过。)

LIMIT 参数只分裂字串的一部分:

   ($login, $passwd, $remainder) = split /:/, $_, 3;

我们鼓励你把你的字串分裂成这样的列表名字,这样你的代码就有了自文档的特性。(可以用于 出错检查,请注意如果字串里比三个域少,那么 $remainder 将会是未定义。)当给一个列表赋值 的时候,如果省略了 LIMIT,那么 Perl 提供一个 LIMIT,其数值比列表中的变量数量大一,以此 避免不必要的工作。对于上面的分裂,LIMIT 缺省时是 4,而 $remainder 将只收到第三个域,而 不是所有剩下的域。在时间要求很严格的应用里,避免分裂成比我们需要的更多的域是一个好习惯。 (强大的语言的问题就是,它给你强大的功能的是以花费在时间上的愚蠢为代价的。)

我们早先说过分隔符不会被返回,但是如果 PATTERN 包含圆括弧,那么每一对圆括弧匹配的子字串 都会包括在结果列表中,分散在那些平常返回的域之中。下面是一个简单的例子:

   split /([-,])/, "1-10,20";

生成列表:

   (1, '-', 10, ',', 20)

如果有更多圆括弧,那么为每个圆括弧对返回一个域,即使有些圆括弧对没有匹配也如此,这种情况 下,为那些位置返回未定义数值。因此,如果你说:

   split /(-)|(,)/, "1-10,20";

那么结果是:

   (1, '-', undef, 10, undef, 20);

/PATTERN 参数的位置可以放这么一个表达式,该声明在运行时生成不同的模式。和普通模式一样, 如果想只做一次运行时编译,那么用 /$varable/o。

有一个特殊的情况,如果该表达式是一个空格(“ ”),那么该函数会象没有参数的 split 那样 在空格上把字串分裂开。因此 split(" ") 可以用于模拟 awk 的缺省行为。相反,split(/ /) 将给你和前导空格一样多的空的初始化域。(除了这个特殊的例子以外,如果你提供的是一个字串 而不是一个正则表达式,那么它还是会被解释成一个正则表达式。)你可以用这个属性把开头和 结尾的空白删除,并且把中间的空白都压缩成一个空白:

   $string = join(' ', split(' ', $string));

下面的例子把一个 RFC 822 消息头分裂成一个包含 $head{Date},$head{Subject},等等的 散列。它使用了给一个散列赋予一个配对列表的技巧,理由是域和分隔符交错。它利用圆括弧把 每个分隔符的一部分当作返回列表值的一部分返回。因为 split 模式保证把返回的东西利用包含 圆括弧的好处按照配对的形式返回,所以散列赋值就可以保证收到一个包含键字/数值对的列表, 这里每个键字就是一个头域的名字。(糟糕的是,这个技巧会丢失有着相同域的多个行的信息, 比如 Received-By 行。啊,哦...)

   $header =~ s/\n\s+/ /g;      # 融合连续行
   %head = ('FRONTSTUFF', split /^(\S*?):\s*/m, $header);

下面的例子处理整个 Unix passwd(5) 文件。你可以忽略 chomp,这个时候 $shell 的结尾将有 换行符。

   open PASSWD, '/etc/passwd';
   while () {
      chomp;      # 删除结尾的换行符
      ($login, $passwd, $uid, $gid, $gcos, $home, $shell) = 
         split /:/;
      ...
   }

下面是一个如何处理每个输入文件里的每一行中的每个词,创建一个单词频率散列的例子:

   while (<>) {
      foreach $word (split) {
         $count{$word}++;
      }
   }

split 的逆操作由 join 执行(只不过 join 只能在所有域之间用同样的分隔符连接)。要用固定 位置的域分解字串,请使用 unpack。

29.2.162. sprintf

这个函数返回一个格式化字串,格式化习惯是 C 的库函数 sprintf 的是 printf 习惯。参阅你的 系统的 sprintf(3) 或 printf (3) 获取一些通用原则的解释。FORMAT 包含一个带有嵌入的域 指示符的文本,LIST 里的元素就是逐一替换到这些域中去的。

Perl 做自己的 sprintf 格式化——它模拟 C 函数 sprintf,但是它没有用 C 的 sprintf。 (注:除了浮点数以外,并且就算是浮点数也只允许标准的修饰词。)结果是,任何你本地的 sprintf(3) 函数的扩展都不能在 Perl 里使用。

Perl 的 sprintf 允许全局使用的已知转化在 表29-4 中列出。

表29-4。sprintf 的格式

含义
%% 一个百分号
%c 一个带有给定数字的字符
%s 一个字串
%d 一个有符号整数,十进制
%u 一个无符号整数,十进制
%o 一个无符号整数,八进制
%x 一个无符号整数,十六进制
%e 一个浮点数,科学记数法表示
%f 一个浮点数,用固定的小数点表示
%g 一个浮点数,以 %e 或 %f 表示

另外,Perl 允许下列广泛支持的转换:

含义
%x 类似 %x,但使用大写字符
%E 类似 %e,但使用大写的“E”
%G 类似 %g,但是带一个大写的“E”(如果正确的话)
%b 一个无符号整数,二进制
%p 一个指针(输出十六进制的 Perl 值的地址)
%n 特殊:把到目前为止输出的字符数放到参数列表中的下一个变量里

最后,为了向下兼容(我们的意思就是“向下”),Perl 允许下列不必要的但广泛支持的转换:

含义
%i %d 的同义词
%D %ld 的同义词
%U %lu 的同义词
%O %lo 的同义词
%F %f 的同义词

Perl 允许下列众所周知的标志出现在 % 和转换字符之间:

域 | 含义|

space 用空格前缀正数
+ 用加号前缀正数
- 在域内左对齐
- 用零而不是空格进行右对齐
# 给非零八进制前缀“0”,给非零十六进制前缀“0x”
number 最小域宽度
.number “精度”:浮点数的小数点后面的位数字串最大长度。整数最小长度
l | | 把整数解释成 C 类型的 long 或者 unsigned long|
h 把整数解释成 C 类型的 short 或者 unsigned short(如果没有提供标志,那么把整数解释成 C 类型 int 或者 unsigned)

还有两个 Perl 相关的标志

含义
V 把整数解释成 Perl 标准的整数类型
v 把字串解释成一个整数向量,输出成用点分隔的数字,或者是用任意参数列表里前面带 * 的字串分隔

如果你的 Perl 理解“四倍数”(64位整数),不管是该平台本机支持还是因为你指明 Perl 带着 该功能编译,那么字符 d u o x X b i D U O 打印64位整数,并且它们前面可以选择前缀 ll, L,或则 q。比如,%lld %16LX %qo。

如果 Perl 理解“long double”(要求该平台支持 long double),那么你可以在 e f g E F G 标志前面增加可选的 ll 或者 L。比如,%llf %Lg。

在标志里可以出现数字的位置,都可以用一个星号(“*”)代替,这时候 Perl 使用参数列表里的 下一个项作为给出的数字(也就是说,当作域宽度或者精度)。如果通过“*”获取的域宽度是负数, 那么它和“-”标志有一样的效果:左对齐。

v 标志可以用于显示任意字串里的序数值:

   sprintf "version is v%vd\n", $^V;      # Perl 的版本
   sprintf "address is %vd\n", %addr;   # IPv4 地址
   sprintf "address is %*vX\n", ":", $addr;   # IPv6 地址
   sprintf "bits are %*vb\n", " ", $bits;   # 随机的位串

29.2.163 sqrt

这个函数返回 EXPR 的平方根。如果需要其他的根,比如立方根,你可以使用 ** 操作符求那个 数字的分数幂。不要试图在着两种方法里使用负数,因为它有一些稍微有些复杂的问题(并且抛出 一个例外)。但是有一个模块可以处理这些事情:

   use Main::Complex;
   print sqrt(-2);      # 打印出 1.4142135623731i

29.2.164. srand

这个函数为 rand 操作符设置随机数种子。如果省略了 EXPR,那么它使用一个内核提供的半随机的 数值(如果内核支持 /dev/urandom 设备)或者是一个基于当前时间和进程号以及一些其他东西的 数值。通常我们完全没有必要调用 srand,因为如果你没有明确调用它,那么它也会在第一次调用 rand 操作符时隐含调用。不过,在早于 Perl 5.004 的版本里不是这样的,所以如果你的脚本 需要在老 Perl 版本上运行,那么你就应该调用 srand。

那些经常被调用的程序(比如 CGI 脚本),如果只是简单地用 time ^ $$ 做种子的话,那么很 容易惨遭下面的数学性质的攻击,那就是:有三分之一的机会 a^b == (a+1)^(b+1)。所以不要 这么干。应该用下面的代码:

   srand( time() ^ ($$ + ($$ << 15)) );

如果用于加密目的,那么你需要用比缺省的种子生成更随机的算法。有些系统上有 /dev/random 设备就比较合适,否则,拿一个或多个会迅速改变操作系统状态的程序的输出,压缩以后进行 校验和计算是常用的方法。比如:

   srand (time ^ $$ ^ unpack "%32L*", `ps wwaxl | gzip`);

如果你特别关心这些问题,那么请参阅 CPAN 上的 Math::TrulyRandom 模块。

不要在你的程序里多次调用 srand,除非你知道你在干什么并且知道为什么这么做。这个函数的 目的是给 rand 函数种子,这样 rand 就可以在你每次运行你的程序的时候生成不同的序列。只 需要在你的程序开始做一次,否则你就不能从 rand 中获得随机的数值!

29.2.165. stat

在标量环境里,这个函数返回一个布尔值,该值表示调用是否成功。在列表环境里,它返回一个 13 个元素的列表,给出一个文件的统计信息,该文件要么是通过 FILEHANDLE 打开,要么是用 EXPR 命名。它的典型应用如下:

   ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
      $atime, $mtime, $ctime, $blksize, $blocks)
         = stat $filename;

不是在所有文件系统类型上都支持这些域;不支持的域返回 0。表 29-5 列出了各个域的含义。

表 29-5。stat 返回的域


索引 含义
0 $dev 文件系统的设备号
1 $ino i 节点号码
2 $mode 文件模式(类型和权限)
3 $nlink 指向该文件的(硬)链接数量
4 $uid 文件所有者的数字用户 ID
5 $gid 文件所属组的数字组 ID
6 $rdev 设备标识符(只用于特殊文件)
7 $size 文件的总尺寸,以字节计
8 $atime 自纪元以来以秒计的上一次访问时间
9 $mtime 自纪元以来以秒计的上一次修改时间
10 $ctime 自纪元以来以秒计的上一次i节点改变的时间(不是创建时间!)
11 $blksize 选定的用于文件系统 I/O 的块尺寸
12 $blocks 实际分配的块数量

$dev 和 $ino 放在一起,在同一个系统里唯一地标识一个文件。$blksize 和 $blocks 很可能 只在 BSD 衍生出的文件系统里有。如果有 $block 域,那么它是以 512 字节的块汇报的。 $blocks*512 的值可能和 $size 差距相当大,因为有些文件包含未分配的块,或者说“洞”, 它们没有在 $blocks 中计算。

如果传递给 stat 一个特殊的文件句柄,该句柄里包含下划线,那么就不会做实际的 stat(2) 调用,而是返回上一次 stat,lstat,或者基于 stat 的文件测试操作符(比如 -r,-w,和 -x) 的 stat 结构。

因为模式包含文件类型及其权限,所以如果你想看真正的权限,那么你应该屏蔽掉文件类型部分, 并且在 printf 或者 sprintf 里用“%o”:

   $mode = (stat($filehane))[2];
   printf "Permissions are %04o\n", $mode &07777;

File::stat 模块提供一个方便的通过名字访问的机制:

   use File:;stat;
   $sb = stat($filename);
   printf "File is %s, size is %s, perm %04o, mtime %s\n",
      $filename, $sb->size, $sb->mode & 07777,
      scalar localtime $sb->mtime;

你还可以从 Fcntl 模块里输入各种不同模式位的符号定义。参阅联机文档获取更多细节。

提示:如果你只需要文件尺寸,那么可以用 -s 文件测试操作符,它直接返回以字节计的文件大小。 另外还有返回以天计的文件年龄的文件测试。

29.2.166. study

这个函数花了一些额外的时间用在研究 SCALAR 上,预测在进行下一次修改之前要做的匹配次数。 这么做也许能但也许不能节约时间,具体情况取决于你正在搜索的模式的天性和数量,以及待搜索 字串中字符出现频率的分布——你可能需要比较有它和没它的运行时间来看看哪个运行得快一些。 那些扫描许多常量字串(包括在更复杂的模式里的常量部分)的循环会从 study 中得到最大好处。 如果你的所有模式匹配都是前面有锚符号的常量字串,那么 study 一点忙都帮不上,因为那里没有 扫描。你每次只能拥有一个 study——如果你研究另外一个标量的话,那么前面那个就“没有研究” 了。

study 运转的方法是:做一个包含待搜索字串中的每个字符的链表,因此,打个比方,我们就知道了 所有的“k”字符的位置。从每个搜索字串里选出最少见的字符,这是基于从一些 C 程序和英文文本 构造出来的频率统计表的。只对那些包含这个最少见字符的位置进行检查。

比如,下面是一个循环,它在每个包含特定模式的行前面插入一个生成索引的记录:

   while(<>) {
      study;
      print ".IX foo\n"      if /\bfoo\b/;
      print ".IX bar\n"      if /\bbar\b/;
      print ".IX blurfl\n"      if /\bblurfl\b/;
      ...
      print;
   }

为了搜索 /\bfoo\b/,只查看 $_ 里的那些包含“f”的位置,因为“f”比“o”少见。除了在 病态的情况下,这样做是很有优势的。唯一的问题是它是否能节约比先制作链表花的时间更多的 时间。

如果你必须搜索那些你直到运行时才知道的字串,那么你可以把整个循环作成一个字串然后 eval 它以避免每次都要重新编译你的模式。再通过设置 $/ 把整个文件输入成一条记录,把这些组合 起来可以非常快,通常比那些专业程序,象 fgrep(1) 什么的都快。下面的程序扫描一个文件列表 (@files),搜索一个单词列表(@words),并且把那些包含大小写无关匹配的文件名字打印出来:

   $search = 'while (<>) { study;';
   foreach $word (@words) {
      $search .= "++\$seen{\$ARGV} if /\\b$word\\b/i;\n";
   }
   $search .= "}";
   @ARGV = @files;
   undef $/;      # 吃进整个文件
   eval $search;      # 这里运行程序
   die $@ if $@;      # 处理 eval 失败
   $/ = "\n";      # 恢复正常的输入终止符
   foreach $file (sort keys(%seen)) {
      print "$file\n";
   }

既然我们有 qr// 操作符,那么上面的编译完的运行时 eval 看上去没有必要。下面的做相同的 事情:

   @pats = ();
   foreach $word (@words) {
      push @pats, qr/\b${word}\b/i;
   }
   @ARGV = @files;
   undef $/;      # 吃进每个完整的文件
   while (<>) {
      for $pat (@pats) {
         $seen{$ARGV}++ if /$pat/;
      }
   }
   $/ = "\n";      # 恢复正常的输入终止符
   foreach $file (sort keys(%seen)) {
      print "$file\n";
   }

29.2.167 sub

子过程声明和定义的语法看起来挺复杂的,但是在实践上实际相当简单。所有东西都是基于下面 语法的:

   sub NAME PROTO ATTRS BLOCK

所有的四个域都是可选的;唯一的限制就是如果这些域的确存在的话那么它们必须以这些顺序出现, 并且你必须至少使用 NAME 或者 BLOCK 之一。目前,我们会忽略 PROTO 和 ATTRS;它们只是 基本语法的修饰词。NAME 和 BLOCK 都是保证正确重要部分:

在上面三种情况中的任何一种里,PROTO 和 ATTRS 之一或者全部都可以在 NAME 之后和/或 BLOCK 之前出现。原型是一个放在圆括弧里的字符列表,它告诉分析器如何对待该函数的参数。 属性是用一个冒号引入的,它告诉分析器有关这个函数的额外的信息。下面是一个包含四个域的 典型的定义:

   sub numstrcmp ($$) : locked {
      my ($a, %b) = @_;
      return $a <=> $b || $a cmp %b;
   }

有关属性列表和它们的操作的细节,请参阅第三十一章里的 attributes 用法。又见第六章和第八章的“匿名子过程”。

29.2.168. substr

这个函数从 EXPR 给出的字串中抽取一个子字串然后返回它。这个子字串是从字串前面 OFFSET 个 字符的位置开始抽取的。(注意:如果你曾经修改了 $[,那么字串的开头就不再是 0 了,不过 因为你没有修改过 $[,所以它的开头还是 0。)如果 OFFSET 是负数,那么子字串是从字串后面 数这么多偏移量位置开始的。如果省略了 LENGTH,那么把从该位置到字串结尾的东西都抽取出来。 如果 LENGTH 是负数,那么该长度是当作在字串尾部剩余那么多字符来理解的。否则,LENGTH 表示要抽取的子字串的长度,通常就是你想要的东西。

你可以把 substr 当作一个左值(可以给之赋值的东西)来用,这个时候 EXPR 也必须是一个合法 的左值。如果你给你的子字串赋予比它短的东西,那么该字串将收缩,而如果你给它赋予比它长的 东西,那么它会变长。要想保持该字串长度一致,你可能需要用 sprintf 或者 x 操作符填充或者 截断你的数值。如果你试图给一个跨过该字串尾部的未分配区域赋值,那么 substr 就会抛出一个 例外。

在 $_ 的当前值前面增加字串“Larry”,用:

   substr($var, 0, 0) = "Larry";

替换 $_ 的第一个字符为“Moe”,用:

   substr($var, 0, 1) = "Moe";

最后,把 $var 的最后一个字符换成“Curly”,用:

   substr($var, -1) = "Curly";

把 substr 当作左值使用的另外一个方面就是声明 REPLACEMENT 字串作为其第四个参数。这样就 允许你替换 EXPR 的某部分并且返回在一次操作之前的东西,就好象你用 splice 实现的功能那样。 下面一个例子也是把 $var 的最后一个字符替换成“Curly”,并且把那个被替换的字符放到 $oldstr 里:

   $oldstr = substr($var, -1, 1, "Curly");

你不一定只是在赋值语句中使用 substr 作为左值。下面的代码把任何空格替换成句点,但是只 替换字串中的最后十个字符:

   substr($var, -10) =~ s/ /./g;

29.2.169. symlink

这个函数创建一个新的文件,该文件是是指向一个旧文件的符号链接。此函数成功时返回真,否则 返回假。在那些不支持符号链接的系统上,它在运行时抛出一个例外。要想检查这个例外,你可以 用 eval 捕获所有可能的错误:

   $can_symlink = eval  { symlink("", ""); 1 };

或者使用 Config 模块。要注意的是如果你提供了一个相对符号链接,那么它会被解释成相对于 该符号链接本身的路径,而不是相对于你的当前工作目录。

又见本章早些时候的 link 和 readlink。

29.2.170 syscall

这个函数调用列表的第一个元素声明的系统调用(意思就是一次系统调用,而不是一个 shell 命令 ),同时把列表中其他元素作为参数传递给系统调用。(现在,许多这些调用可以通过 POSIX 模块 更容易地使用。)如果 syscall(2) 未实现,那么该函数抛出一个例外。

这些参数按照下面的方式解释:如果一个给定的参数是数字,那么该参数就作为一个 C 整数传递。 如果不是,那么就传递一个指向该字串值的指针。你有责任确保这个字串足够长,以便能够接收 任何可能写到它里面去的结果;否则,你就等着核心倾倒吧。你不能拿一个字串文本(或者其他 只读的字串)当作给 syscall 的参数,因为 Perl 已经假定任何字串指针都是可写的。如果你的 整数参数不是文本并且在数字环境里从不会被解释,那么你可能需要给它们加 0 以强迫它们看起来 象数字。

syscall 返回被调用的系统调用返回的任何数值。在 C 传统里,如果你那个系统调用失败,那么 syscall 返回 -1 并且设置 $!(errno)。有些系统调用在成功的时候合理地返回 -1。操作这样 的的调用的正确方法是在调用之前赋值给 $!=0;然后如果 syscall 返回 -1 的话检查 $! 的值。

不是所有的系统调用可以用这个方法访问。比如,Perl 支持最多给你的系统调用传递 14 个参数。 通常这么多已经足够了,但是,对于那些返回多个数值的系统调用就有问题了。比如 syscall($SYS_pipe):它返回创建的管道的读端文件号码。我们没有办法检查另外一端的文件 号码。你可以用 pipe 避免这个例子里的问题。要解决一般性的问题,写直接访问系统调用的 XSUB(外部过程模块,一种 C 写的程序)。然后把你的新模块放到 CPAN,使之流行开来。

下面的子过程以浮点数返回当前时间,而不是象 time 那样返回整数秒。(它只能在那些支持 gettimeofday(2) 系统调用的机器上用。)

   sub finetime() {
      package main;      # 用于下一个 require
      require 'syscall.ph';
      # 预先把缓冲区设置为 32 位长...
      my $tv = pack("LL", ());
      syscall(&SYS)gettimeofday, $tv, undef) >= 0
         or die "gettimeofday: $!";
      my($seconds, $microseconds) = unpack("LL", $tv);
      return $seconds + ($microseconde/ 1_000_000);
   }

假设 Perl 不支持 setgroups(2) 系统调用,(注:尽管它可以通过 $( 支持),但是你的核心 支持。那么你就可以用下面的方法获得它的支持:

   require 'syscall.ph';
   syscall(&SYS_setgroups, scalar @newgids, pack("i*", @newgids))
      or die "setgroups: $!";

你可能需要按照 Perl 安装指导里描述的那样运行 h2ph,检查 syscall.ph 是否存在。有些系统 可能要求使用 "II" 模板。更头疼的是,syscall 假设 C 类型 int,long,和 char* 的尺寸是 相等的。希望你不要把 syscall 当作移植性的体现。

参阅 CPAN 里的 Time::HiRes 模块获取一个有着更好的分辨率的时间装置的更严格的方法。

29.2.171 sysopen

sysopen 函数打开 FILENAME 给出文件名的文件,并且把它和 FILEHANDLE 关联起来。如果 FILEHANDLE 是一个表达式,那么它的值用做该文件句柄的名字或者引用。如果 FILEHANDLE 是一个未定义值的变量,那么 Perl 将会为你创建一个值。如果调用成功,那么返回值是真,否则 是假。

这个函数是你的系统 open(2) 系统调用后面跟着一个 fdopen(2) 库调用的接口。因此,在这儿你 需要略微把自己想象成一个 C 程序员。MODE 参数的可能数值和标志位可以通过 Fcntl 模块获得。 因为不同的系统支持不同的标志位,所以不要指望你的系统里能够用上所有这些标志位。参阅你的 open(2) 手册页或者它本地的等价物获取细节。当然,下面的标志在那些带有合理 C 库的系统里 是存在的:

标志 含义
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读和写
O_CREAT 如果文件不存在则创建之
O_EXCL 如果文件已经存在则失败
O_APPEND 附加到文件上
O_TRUNC 截断该文件
O_NONBLOCK 非阻塞访问

不过,还有许多其他选项。下面是一些不那么常见的标志:

标志 含义
O_NDELAY O_NONBLOCK 的老式同义词
O_SYNC 写块时直到数据已经从物理上写入下层的硬件以后(才返回)可能还会看到 O_ASYNC,O_DSYNC 和 O_RSYNC。
O_EXLOCK 带着 LOCK_EX 的 flock(只是劝告性的)
O_SHLOCK 带着 LOCK_SH 的 flock(只是劝告性的)
O_DIRECTORY 如果该文件不是目录则失败
O_NOFOLLOW 如果路径中最后一个部件是符号链接则失败
O_BINARY 为 Microsoft 系统 binmode 该句柄。有时候还会有一个 O_TEXT存在以获取相反的行为
O_LARGEFILE 有些系统需要这个标志来标识超过 2 GB 的文件
O_NOCTTY 如果你还没有控制终端,那么打开一个终端文件不会令该终端成为这个进程的控制终端,通常不再需要了。

O_EXCL 标志不是用于锁定的:在这里,排它意味着如果该文件已经存在,那么 sysopen 失败。

如果 FILENAME 命名的文件还不存在,并且 MODE 包括 O_CREAT 标志,那么 sysopen 将 在被你的当前 umask 修改后的参数 MASK 决定的权限范围内(或者如果省略这个参数时缺省 是 0666)创建该文件。这样的缺省是有道理的,参阅 unmask 里的记录获得一个解释。

用 open 和 sysopen 打开的文件句柄可以交互地使用。你不必因为碰巧用 sysopen 打开了文件 而使用 sysread 和它的朋友们来操作文件,而如果你用 open 打开它也不意味着你就不能用 sysread 等函数。open 和 sysopen 都可以做一些对方做不了的事情。普通的 open 可以打开 管道,派生进程,设置纪律,复制文件句柄,以及把一个文件描述符号码转换成一个文件句柄。 它还忽略文件名开头和结尾的空白,并且把“-”当作一个特殊的文件名。但是如果你要打开的是 一个真正的文件,那么 sysopen 就可以做 open 能做的任何事情。

下面的例子显示了对两个函数的等效调用。我们为了清晰起见省略了 or die $! 检查,不过你自己 的程序里可是一定要检查这些值的呦。我们将把我们限制于只检查那些实际上在所有操作系统里都 可以用的标志。这个活只是用位操作符 | 把传递给 MODE 参数的数值 OR (或)在一起而已。

而下面的事情是你可以用 sysopen 干的,但是却不能用普通的 open 干:

在第三十二章描述的 FileHandle? 模块提供了一套打开文件的面向对象的同义词(以及一点点新的 功能)。我们很欢迎你在任何用 open,sysopen,pipe,socket,或者 accept 创建的句柄上 调用 FileHandle? 方法(注:实际上是 IO::File 或者 IO::Handle 方法),就算你不用该模块 初始化这些句柄也可以。

29.2.172 sysread

这个函数试图使用低层系统调用 read(2) 从你声明的 FILEHANDLE 里读取 LENGTH 字节到变量 SCALAR 中。该函数返回读取的字节数量,或者在 EOF 时返回 0。(注:在 Perl 里没有 syseof 函数,但这样是对的,因为 eof 在设备文件(比如说终端)上运转的并不怎么正确。用 sysread 并且检查返回值是否为 0 来判断你是否读完了。)出错时,sysread 函数返回 undef。 SCALAR 将会根据实际读取的长度伸缩。如果声明了 OFFSET,那么它指明应该从字串里的哪个位置 开始读取字节,这样你就可以在一个用做缓冲区的字串中间读取。要获取使用 OFFSET 的例子, 请参阅 syswrite。如果 LENGTH 为负数或者 OFFSET 指向了该字串的外边,那么就会抛出一个 例外。

你应该准备处理那些标准 I/O 通常会为你处理的问题(比如中断了的系统调用)。因为它绕开了 标准的 I/O,所以不要把 sysread 和其他类型的读取,print ,printf,write,seek,tell, 或者 eof 在同一个文件句柄上混合使用,除非你准备承受极其希奇古怪(和/或痛苦)的东西。 同样,请注意,如果你从一个包含 Unicode 或者任何其他多字节编码的文件里读取数据,那么 缓冲区的边界有可能落在一个字符的中间。

29.2.173 sysseek

这个函数使用系统调用 lseek(2) 设置 FILEHANDLE 的系统位置。它绕开了标准 I/O,因此把它 和读(除了 sysread 以外),print,printf,write,seek,tell,或者 eof 混合起来使用将 会导致混乱。FILEHANDLE 可以是一个表达式,该表达式的值给出文件句柄的名字。WHENCE 的值为 0 时设置句柄新位置为 POSITION ,1 时设置为当前位置加 POSITION,2 时设置为 EOF 加 POSITION(通常为负数)。你可以用来自标准 IO::Seekable 和 POSIX 模块或者——Perl 5.6 里的 Fcntl 模块里面的 SEEK_SET,SEEK_CUR 和 SEEK_END 作为 WHENCE 的值,而 Fcntl 模块可能更容易移植和更方便一些。

成功时返回新位置,失败时返回 undef。位置零是以特殊字串“0 but true”返回的,该字串可以 直接当数字使用而不会导致警告。

29.2.174 system

这个函数为你执行任何系统里的程序并返回该程序的退出状态——而不是它的输出。要捕获命令行 上的输出,你应该用反勾号或者 qx//。system 函数的运转非常类似 exec,只不过 system 先做 一个 fork,然后在 exec 之后等待执行的程序的结束。也就是说它为你运行这个程序并且在它 完成之后返回,而 exec 用新的程序代替你运行的程序,所以如果替换成功的话它从不返回。

参数的处理因参数的数目的不同而不同,就象在 exec 里描述的那样,包括判断是否调用 shell 以及你是否用声明另外一个 PATHNAME 的方法使用了该函数其他的名称。

因为 system 和反勾号阻塞 SIGINT 和 SIGQUIT,所以向那些正在这样运行的程序发送这些信号 之一(比如通过一个 Control-C)时并不会中断你的主程序。但是你运行的另外一个程序的确收到 这个信号。请检查 system 的返回值,判断你运行的程序是否正常退出。

   @args = ("command", "arg1", "arg2");
   system(@args) == 0
      or die "system @args failed: $?"

返回值是和该函数通过 wait(2) 系统调用返回的一样的退出状态。在传统的语意里,要获取实际的 退出值,要除以 256 或者右移 8 位。这是因为低 8 位里有一些其他的东西。(实际上是其他的 两些东西。)最低七位标识杀死该进程的信号号码(如果有的话),而第八位标识该进程是否倾倒 了核心。你可以通过 $?($CHILD_ERROR)来检查所有失效可能性,包括信号和核心倾倒:

   $exit_value = $? >> 8;
   $exit_value = $? & 127;   # 或者 0x7f, 0177, 0b0111_1111
   $dumped_core = $? & 128;   #  或者 0x80, 0200, 0b1000_0000

如果该程序是通过系统 shell (注:定义为 /bin/sh 或者任何在你的平台上有意义的东西,但 不是那些用户碰巧在某个时候用到的 shell。)运行的,这可能是因为你只有一个参数而且该参数 里面有 shell 元字符,那么通常返回码受那个 shell 的怪癖和功能的影响。换句话说,在这种 情况下,你可能无法获取我们前面描述了详细信息。

29.2.175 syswrite

这个函数试图用 write(2) 系统调用向你声明的 FILEHANDLE 里写入从变量 SCALAR 里获取的 LENGTH 字节的数据。该函数返回实际写入的字节数,或者是出错时返回 undef。如果声明了 OFFSET,那么 它指明从字串里的哪个位置开始写。(比如,你可能用一个字串做一个缓冲区,这时你就需要这个 功能了,或者你需要从一个部分写中恢复过来。)负数 OFFSET 表示写应该从该字串的后面向前数 这么多个字节。如果 SCALAR 是空的,那么唯一允许的 OFFSET 是 0。如果 LENGTH 为负数或者 OFFSET 指向了字串的外面,那么就会抛出一个例外。

要从文件句柄 FROM 中拷贝数据到文件句柄 TO,你可以用下面这样的东西:

   use Errno qw/EINTR/;
   $blksize = (stat FROM)[11] || 16384;      # 选定的块大小?
   while ($len = sysread FROM, $buf, $blksize) {
      if (!defined $len) {
         next if $! == EINTR;
         die "System read error: $!\n"
      }
      $offset = 0;
      while ($len) {            # 处理部分写问题
         $written = syswrite TO, $buf, $len, $offset;
         die "System write error: $!\n" unless defined $written;
         $offset   += $written;
         $len   -= $written;
      }
   }

你必须准备处理标准 I/O 通常会为你处理的问题,比如部分写。因为 syswrite 绕开了 C 标准 I/O 库,所以不要把它的调用和读(除了 sysread 以外),写(象 print,printf,或者 write),或者其他 stdio 函数,比如 seek,tell,或者 eof 混合在一起用,除非你想自找 麻烦。

29.2.176 tell

这个函数返回 FILEHANDLE 的当前文件位置(以零为基的字节数)。该值通常可以在程序中稍后的 时候传递给 seek 函数以找回当前位置。FILEHANDLE 可以是一个给出实际文件句柄的表达式, 或者一个指向文件对象的引用。如果省略 FILEHANDLE,那么该函数返回最后一个读取的文件的 位置。只有普通文件的文件位置才有意义。设备,管道,和套接字都没有文件位置。

没有 systell 函数,你可以用 sysseek(FH, 0, 1) 来实现同样的功能。参阅 seek 获取一个 如何使用 tell 的例子。

29.2.177. telldir

这个函数返回在 DIRHANDLE 上的 readdir 的当前位置。而这个返回值可以给 seekdir 用于访问 一个目录里的某个特定的位置。该函数和对应的系统库过程在关于可能的目录压缩问题上有这一样的 注意事项。该函数可能不是在所有实现了 readdir 的地方有实现了,即使该平台实现了它,你也 不能计算它的返回值。因为该返回值只是一个晦涩的数值,只对 seekdir 有意义。

29.2.177. tie

此函数把一个变量和一个类绑定在一起,而该类提供了该变量的实现。VARIABLE 是要绑定的变量 (标量,数组,或者散列)或者类型团(代表一个文件句柄)。CLASSNAME 是实现一个正确类型的 类名字。

任何额外的参数都传递给该类的合适的构造方法,可能是 TIESCALAR,TIEARRAY,TIEHASH 或者 TIEHANDLE 之一。(如果没有找到合适的方法,则抛出一个例外。)通常,那些都是可能被传递给 象 dbm_open(2) 这样的 C 函数的参数,但是它们的含义是和包相关的。构造器返回的对象随后被 tie 函数返回,而如果你想在 CLASSNAME 里访问其他方法,那么这个对象就很有用了。(该对象 也可以通过 tied 函数访问。)因此,一个把散列与一个 ISAM 绑定的实现可以提供一些额外的 方法用于顺序地跨过一个键字的集合(ISAM 里的“S”就是 sequentially,顺序的意思),因为 你典型的 DBM 实现是不能做这些事情的。

象 keys 和 values 这样的函数在和 DBM 这样的大对象一起使用的时候可能返回非常巨大的数值 列表。你可能会更愿意使用 each 函数来遍历这样的列表。比如:

   use NDBM_File;
   tie %ALIASES, "NDBM_File", "/etc/aliases", 1, 0
      or die "Can't open aliases: $!\n";
   while (($key, $val) = each %ALIASES) {
      print $key, ' = ', $val, "\n";
   }
   untie %ALIASES;

一个实现散列的类应该提供下列方法:

TIEHASH CLASS, LIST
FETCH SELF, KEY
STORE SELF, KEY, VALUE
DELETE SELF, KEY
CLEAR SELF
EXISTS SELF, KEY
FIRSTKEY SELF
NEXTKEY SELF, LASTKEY
DESTROY SELF

一个实现普通数组的类应该提供下列方法:

    TIEARRAY CLASS, LIST
    FETCH SELF, SUBSCRIPT
    STORE SELF, SUBSCRIPT, VALUE
    FETCHSIZE SELF
    STORESIZE SELF, COUNT
    CLEAR SELF
    PUSH SELF, LIST
    POP SELF
    SHIFT SELF
    UNSHIFT SELF, LIST
    SPLICE SELF, OFFSET, LENGTH, LIST
    EXTEND SELF, COUNT
    DESTROY SELF

一个实现标量的类应该提供下列方法:

    TIESCALAR CLASS, LIST
    FETCH SELF,
    STORE SELF, VALUE
    DESTROY SELF

一个实现文件句柄的类应该提供下列方法:

TIEHANDLE CLASS, LIST
READ SELF, SCALAR, LENGTH, OFFSET
READLINE SELF
GETC SELF
WRITE SELF, SCALAR, LENGTH, OFFSET
PRINT SELF, LIST
PRINTF SELF, FORMAT, LIST
CLOSE SELF
DESTROY SELF

并不是上面提到的所有方法都需要实现:Tie::Hash,Tie::Array,Tie::Scalar,和 Tie::Handle 模块提供了有着合理的缺省的基类。参阅第十四章,捆绑变量,获取所有这些方法的 详细描述。和 dbmopen 不同,tie 函数将不会为你 use 或者 require 一个模块——你必须自己 明确地做这件事情。参阅 DB_File 和 Config 方法获取有趣的 tie 实现。

29.2.179 tied

这个函数返回一个引用,该引用指向包含在 VARIABLE 里的标量,数组,散列或者类型团的的下层 对象。(VARIABLE 是最初用 tie 调用把该变量和一个包绑定在一起的同一个值。)如果 VARIABLE 没有和一个包绑定,它返回未定义的数值。因此,比如,你可以用:

   ref tied %hash

找出你的散列与哪个包捆绑。(假设你忘记了。)

29.2.180. time

这个函数返回自“纪元”以来的没有润秒的秒数,纪元通常是 1970年1月1日 00:00:00 UTC。 (注:不要和创造 Unix 的“历史”相混淆。(其他操作系统可能有不同的纪元,更别说历史了。 ))返回值可以传递给 gmtime 和 localtime,可以用于比较 stat 返回的文件修改以及访问的 时间,还有就是传递给 utime。

  $start = time();
   system("some slow command");
   $end = time();
   if ($end - $start > 1) {
      print "Program started: ", scalar localtime($start), "\n";
      print "Program ended:  ", scalar localtime($end), "\n";
   }

29.2.181 times

在这个环境里,这个函数返回一个四元素的列表,该列表给出这个进程和它已结束的子进程以秒计 (可能是分数)的用户和系统 CPU 时间。

   ($user, $system, $cuser, $csystem) = times();
   printf "This pid and its kids have consumed %.3f seconds\n",
      $user + $system + $cuser + $csystem;

在标量环境里,只返回用户时间。比如,要计算一段 Perl 代码的执行速度:

   $stat = times();
   ...
   $end = times();
   printf "that took %.2f CPU seconds of user time\n",
      $end - $start;

29.2.182 tr///

这是转换(也称之为翻译)操作符,它和 Unix sed 程序里的 y/// 操作符类似,但不论从任何人 的角度来看都更好些。参阅第五章。

29.2.182 truncate

这个函数截断在 FILEHANDLE 上打开的或者 EXPR 命名的文件到指定的长度。如果在你的系统上, ftruncate(2) 或者等效的东西没有实现,那么该函数抛出一个例外。(如果你有磁盘空间的话, 你总是可以通过拷贝文件的开头来截断它。)该函数在成功的时候返回真,否则返回 undef。

29.2.183 uc

这个函数返回 EXPR 的大写的版本。它是实现双引号字串里的 \U 逃逸的内部函数。Perl 将试图 在考虑你的区域设置的前提下做正确的事情,不过我们仍在努力让这个功能也能用于 Unicode。 参阅 perllocalle 手册页获取最新的进展。在任何情况下,如果 Perl 使用 Unicode 表,uc 都会转换成大写字符而不是标题字符。参阅 ucfirst 获取转换成标题字符的信息。

29.2.184 ucfirst

这个函数返回将 EXPR 第一个字符标题化(“Unicode”里的标题字符)的版本。而其他字符则不 加触动。它是实现双引号字串里的 \u 逃逸的内部函数。如果你 use locale 并且你的数据看上去 不象 Unicode,那么 Perl 会考虑你当前的 LC_CTYPE 区域设置,但是我们现在不能做任何保证。

要强制字串里第一个字符是标题字符而其他的都是小写字符,你可以用:

   ucfirst lc $word

它等效于"\u\L$word"。

29.2.186 umask

这个函数用 umask(2) 系统调用为该进程设置 umask 并返回原来的那个。你的 umask 告诉操作 系统在创建新文件的时候,哪个权限位是不允许的,包括那些正好是目录的文件。如果省略了 EXPR,那么该函数只是返回当前 umask。比如,为了确保“user”位是允许,而“other”位是 不允许的,你可以用下面的代码:

   umask((umask() & 077) | 7);      # 不改变组的权限位

请记住 umask 是一个数字,通常是以八进制形式给出的;它不是八进制位的字串。如果你拿到的 是一个字串,又见 oct,还要记住这个 umask 位是普通权限位的补。

Unix 权限位 rwxr-x--- 是用三个三位集,或者三个八进制位来表示的:0750(前面的 0 表示它 是八进制而不是其中一位)。因为 umask 的位是翻转的,所以它代表关闭了的权限位。你提供给 你的 mkdir 或者 sysopen 的权限值(或者“模式”)都会被你的 umask 修改,所以就算你告诉 sysopen 创建一个权限为 0777 的文件,如果你的 umask 是 0022,那么创建出来的文件的权限 也是 0755。如果你的 umask 是 0027(组不能写,其他不能读,写,和执行),那么给 sysopen 传递一个 MASK 为 0666 的值将创建一个模式为 0640 的文件(因为 0666 & ~0027 是 0640)。

这里是一些建议:使用模式 0666 创建普通文件(在 sysopen 里)以及 0777 给目录(用 mkdir)和可执行文件。这样就给予用户自由的选择:如果它们想保护文件,那么它们选择进程 umask 022,027,或者甚至特别反社会的掩码 077。程序最好让用户自己做策略决策。这条规则的 例外是那些需要写私人文件的程序:邮件文件,网络浏览器的 cookie,.rhost 文件,等等。

如果你的系统里没有实现 umask(2) 并且你试图限制你自己的权限(也就是说,如果 (EXPR & 0700) > 0),那么你就会触发一个运行时例外。如果你的平台没有实现 umask(2) 并且 你不准备限制自己的权限,那么这个函数简单地返回 undef。

29.2.186 undef

undef 是我们所谓的“未定义值”的缩写。同时,它还是一个永远返回未定义值的函数的名字。 我们很高兴能混淆它们俩。

相同的是,如果你给 undef 函数提供一条记录作为它的参数,那么它还可以明确的解除该记录的 定义。如果声明了 EXPR 参数,那么它必须是一个左值。因此你可能只能在一个标量数值,整个 散列或者数组,一个子过程名字(用 & 前缀),或者一个类型团上这么用,任何和该对象关联的 存储空间都将被恢复用于重复使用(不过在大多数操作系统上都不是返回给系统)。undef 函数对 大多数特殊变量可能都不会做想你想象的处理。在象 $1 这样的只读变量上使用将抛出一个例外。

undef 函数是一个单目操作符,不是列表操作符,因此你只能每次解除一个东西的定义。下面是 一些 undef 作为单目操作符的用法:

   undef $foo;
   undef $bar{'blurfl'};      # 和 delete $bar{'blurfl'} 不同
   undef @ary;
   undef %hash;
   undef &mysub;
   undef *xyz;            # 删除 $xyz, @xyz, %xyz, &xyz 等等。

如果没有参数,undef 只是用做数值:

   select(undef, undef, undef, $naptime);

   return (wantarray ? () : undef) if $they_blew_it;
   return if $they_blew_it;   # 一样的东西

你可以把 undef 用做一个列表赋值中左边的一个占位符,这个时候右边的对应的数值只是简单地 抛弃。除此之外,你不能在其他地方拿 undef 做左值:

   ($a, $b, undef, $c) = &foo;   # 忽略返回的第三个数值

同样,不要拿任何东西和 undef 做比较——那样不会按照你想象的方式处理的。它所作的事情只是 与 0 或者空字串比较。使用 defined 函数判断一个数值是否定义。

29.2.188. unlink

这个函数删除一列文件。(注:实际上,在一个 POSIX 文件系统里,它删除指向真实文件目录记录 (文件名)。因为一个文件可以从一个或多个目录里引用(链接),该文件不会被删除,直到指向 它的最后一个引用被删除。)此函数返回被成功删除的文件名的个数。一些简单的例子:

   $count = unlink 'a', 'b', 'c';
   unlink @goners;
   unlink glob("*.orig");

除非你是超级用户或者给 Perl 使用了 -U 命令行选项,否则 unlink 函数不会删除目录。即使 符合这些条件,你也要注意删除一个目录可能回造成对你的文件系统的彻底损坏,应该用 rmdir 代替。

下面是一个带有非常简单的错误检查的 rm 命令:

   #!/usr/bin/perl
   @cannot = grep {no unlink} @ARGV;
   die "$0: could not unlink @cannot\n" if @cannot;

29.2.189. unpack

这个函数是 pack 的逆操作:它根据 TEMPLATE 把一个表示一个数据结构的字串(EXPR)扩展成 一列数值并返回那些数值。在标量环境里,它可以用于解包一个数值。这里的 TEMPLATE 有着和 pack 函数里的大多数格式——它声明要解包的数值的顺序和类型。参阅 pack 函数获取有关 TEMPLATE 的详细描述。如果 TEMPLATE 里有非法元素,或者试图跨过 x,X,或者 @ 格式字串的 外面,都会抛出例外。

该字串会分解成 TEMPLATE 里描述的片段。每个片段都独立地转化成一个数值。通常,该字串的 字节要么是 pack 的结果,要么代表某种类型的 C 结构。

如果一个域的重复计数比输入字串剩余部分允许的尺寸大,那么重复计数就会被不声不响地缩小。 (不过,你通常会在这个地方放一个 * 做重复计数。)如果输入字串比 TEMPLATE 描述的长, 那么字串剩余的部分被忽略。

unpack 函数对纯文本数据也很有用,而不仅仅是对二进制数据管用。设想你有一个数据文件,它的 内容看起来象下面这样:

1986 Ender's Game           Orson Scott Card
1985 Neuromancer            William Gibson
1984 Startide Rising        David Brin
1983 Foundation's Edge      Isaac Asimov
1982 Downbelow Station      C. J. Cherryh
1981 The Snow Queen         Joan D. Vinge

你不能用 split 来分析出各个域,因为这里没有明显的分隔符。这里的域是由它们的字节偏移量来 决定的。因此就算这是一个普通的文本记录,但因为它是固定格式的,所以你就可以用 unpack 把 它们分解成数据域:

   while (<>) {
      ($year, $title, $author) = unpack("A4 x A23 A*", $_);
      print "$author won ${year}'s Hugo for $title.\n";
   }

(我们在这里写成 ${year}'s 的原因是 Perl 会把 $year's 当作 $year::s 看待。)

下面是一个完整的 undecode 程序:

   #! /usr/bin/perl
   $_ = <> until ($mode, $file) = /^begin\s*(\d*)\s*(\S*)/;
   open(OUT, "> $file") if $file ne "";
   while (<>) {
      last if /^end/;
      next if /[a-z]/;
      next unless int((((ord() - 32) & 077) + 2) / 3) ==
            int (length() / 4);
      print OUT unpack "u", $_;
   }
   chmod oct($mode), $file;

除了那些 pack 里允许的数据域以外,除了各个项自身以外,你还可能在一个数据域前面前缀一个 %number 作成一个所有项的简单的 number 位附加校验和。该校验和是通过类加扩展数值的数字 值来计算的(对于字串域,求 ord($char) 的和,而对于位域,计算零和一的和)。比如,下面的 代码计算和 SysV? sum(1) 相同的数字:

   undef $/;
   $checksum = unpack ("%32C*", <>) % 65535;

下面的代码高效的计算一个位流里的设置上的位的个数:

   $setbits = unpack "%32b*", $selectmask;

下面是一个简单的 BASE64 解码器:

   while (<>) {
      tr#A-Za-z0-9+/##cd;            # 删除非 base64 字符
      tr#A-Za-z0-9+/# -_#;            # 转换成 uuencode 格式
      $len = pack("c", 32 + 0.75*length);   # 计算长度字节
      print unpack("u", $len . $_);      # uudecode 并打印
   }

29.2.190. unshift

这个函数做 shift 的逆操作。(或者是 push 的逆操作,取决于你怎么看它。)它在数组前面 增加 LIST,并返回在数组里的新的元素个数:

   unshift @ARGV, '-e', $cmd unless ARGV[0] =~ /^-/;

请注意 LIST 是整个放到前面,而不是每次一个元素,因此放到前面的元素保持相同顺序。用 reverse 实现这些元素的翻转。

29.2.190. untie

打破 VARIABLE 里包含的变量或者或者类型团和与它捆绑的包之间的绑定。参阅 tie,以及第十四 章的全部,尤其是“一个精细的松绑陷阱”节。

29.2.190. use

use 声明装载一个模块(如果它还没有被装载),并且把子过程和变量从这个命名模块输入到当前 包。(从技术上来讲,它从那个命名模块向当前包输入一些语意,通常是通过把一些子过程或者 变量名作成你的包里的别名的方法。)大多数 use 的声明看起来象:

   use MODULE LIST;

这样和下面是完全一样的:

   BEGIN { require MODULE; import MODULE LIST; }

BEGIN 迫使 require 和 import 在编译时发生。require 确保该模块在还没有装载的时候装入 内存。import 不是内建的函数——它只是一个普通的类方法,调用名字叫 MODULE 的包,告诉该 模块把列表里的特性拖到当前包里来。模块可以用自己喜欢的任何方法实现它的输入方法,尽管 大多数只是通过从 Exporter 类中继承 import 方法。Exporter 类在 Exporter 模块中定义。 参阅第十一章,模块,以及 Exporter 模块获取更多信息。如果找不到 import 方法,那么调用将 不声不响地忽略。

如果你不希望你的名字空间被修改,那么明确地提供一个空列表:

   use MODULE ();

它和下面的代码完全一样:

   BEGIN { require MODULE; }

如果给 use 的第一个参数是象 5.6.2 这样的版本号,那么当前执行着的 Perl 版本必须至少和 声明的版本一样新。如果当前的版本比 VERSION 小,那么就会打印出一条错误信息然后 Perl 马上退出。这样就可以有效地在装载需要更新的版本的库模块之前检查当前 Perl 的版本,因为 有时候我们必须“破坏”老版本的错误特性。(我们总是试图尽可能不破坏任何东西。而实际上 我们总是试图少破坏东西。)

谈到不破坏其他东西,Perl 仍然接受下面形式的老版本号:

   use 5.005_03;

不过,为了和工业标准更好的看齐,Perl 5.6 现在接受(并且也更愿意使用)下面的三段式:

   use 5.6.0;      # 它是版本 5,子版本 6,补丁级 0。

如果 VERSION 参数在 MODULE 后面出现,那么 use 将在类 MODULE 里调用 VERSION 方法, 同时把给出的 VERSION 当作参数给他。请注意在 VERSION 后面没有逗号!缺省的 VERSION 方法(通常是从 UNIVERSAL 类里继承过来的。)会在给出的版本大于变量 $Module::VERSION 的值的情况下发表意见。

参阅第三十二章获取一个标准模块的列表。

因为 use 提供了一个非常开放的接口,所以用法(编译器指示器)也是通过模块来实现的。当前 实现了的用法包括:

   use autouse 'Carp' => qw(carp croak);
   use bytes;
   use constant PI => 4 * atan2(1,1);
   use diagnostics;
   use integer;
   use lib '/opt/projects/spectre/lib';
   use locale;
   use sigtrap qw(die INT QUIT);
   use strict qw(subs vars refs);
   use warnings "deprecated";

许多这些用法模块向当前词法范围输入语意。(它和普通模块不同,普通模块只是向当前包里输入 符号,而除了该词法范围是在带有该包的情况下编译的以外,那些符号和当前词法范围没有什么 关系。也就是说,哦,看看第十一章吧。)

还有一个对应的声明,no,它“戒除”任何原来用 use 输入的东西,让它们变得不再重要:

   no integer;
   no strice 'refs';
   no utf8;
   no warnings "unsafe";

参阅第三十一章获取一个标准用法的列表。

29.2.193 utime

该函数改变一列文件里的每一个文件的访问和修改时间。列表的头两个元素必须是数字化的访问和 修改时间,顺序是访问在前修改在后。该函数返回成功改变的文件的数目。每个文件的 inode 修改 时间设置成当前时间。下面是一个 touch 命令的例子,它设置该文件的修改日期(假设你是 所有者)为近一个月后:

   #! /usr/bin/perl
   # montouch - post-date files now + 1 month
   $day = 24 * 60 * 60;         # 24 小时的秒数
   $later = time() + 30 * $day;   # 30 天接近一个月
   utime $later, $later, @ARGV;

29.2.194. values

这个函数返回一个包含指定散列 HASH 里的所有值的列表。这些值是以看似随机的顺序返回的, 但是这个顺序和 keys 或 each 函数在同一个散列上生成的顺序相同。怪异的是,如果要通过一个 散列的数值对它进行排序,那么你通常需要使用 keys 函数,所以看看 keys 函数里的例子找找 灵感。

你可以用这个函数修改一个散列的数值,因为它返回的列表包含数值的别名,而不是拷贝。(在 早期的版本里,你需要用散列的片段来实现这个功能。)

   for (@hash{keys %hash}) { s/foo/bar/g }    # 老办法
   for (values %hash)      { s/foo/bar/g }   # 新手段

在一个和某个巨大的 DBM 文件捆绑的散列上使用 values 也肯定会生成一个巨大的列表,导致你 拥有一个巨大的进程。你可能应该使用 each 函数,它会一个一个地遍历散列记录,而不会把它们 的所有东西都吞进一个庞大的,哦,应该是巨大的列表里。

29.2.195 vec

vec 函数制造一个存储紧凑的无符号整数的列表。这些整数在一个普通的 Perl 字串里尽可能紧密 的绑在一起。EXPR 里的字串被当作一个位串对待,该未串是用若干元素组成的,而元素的数目取决 于字串的长度。

OFFSET 声明你关心的特定元素的索引。读和写元素的语法是一样的,因为 vec 根据你是在左值 环境还是右值环境里来存储和恢复元素值。

BITS 声明每个元素以位计算的时候宽度是多少,它必须是二的幂:1,2,4,8,16,或 32(有些 平台上还有 64)。(如果声明了任何其他数值,那么就会抛出一个例外。)因此每个元素都可以 包含一个范围在 0 .. (2**BITS)-1 的整数。对于小一些的尺寸,那么每个字节里都会尽可能多的 打包进去元素。如果 BITS 是 4,那么每个字节里有两个元素(通常它们被称为半字节(nybble ))。等等。大于一个字节的整数是以大头在前的字节序存储的。

一个无符号整数列表可以存储在一个标量变量里,方法是把它们分别赋予 vec 函数。(如果 EXPR 不是一个合法的左值,那么抛出一个错误。)在随后的例子里,那些元素是 4 位宽:

   $bitstring = "";
   $offset = 0;
   
   foreach $num (0, 5, 5, 6, 2, 7, 12, 6) {
      vec($bitstring, $offset++, 4) = $num;
   }

如果一个元素超出了它要写的字串的结尾,那么 Perl 先用足够的零内容字节扩展该字串。

存储在标量变量里的向量然后就可以通过声明正确的 OFFSET 来检索:

   $num_elements = length($bitstring)*2;   # 每个字节 2 元素

   foreach $offset (0 .. $num_elements-1) {
      print vec($bitstring, $offset, 4), "\n";
   }

如果选择的元素超出了字串的结尾,那么返回 0。

用 vec 创建的字串还可以用逻辑操作符 |,&,^,和 ~ 操作。如果两个操作数都是字串,那么 这些操作符将假定需要进行的是位串操作。参阅第三章,单目和双目操作符,“位操作符”一节里 的例子。

如果 BITS == 1,那么就可以创建一个所有位都存储在一个标量里的位序列。顺序是这样的, vec($bitstring, 0,1) 保证进入字串里的第一个字节的最低位。

   @bits = (0,0,1,0, 1,0,1,0, 1,1,0,0, 0,0,1,0);

   $bitstring = "";
   $offset = 0;

   foreach $bit (@bits) {
      vec($bitstring, $offset++, 1) = $bit;
   }

   print "$bitstring\n";      # "TC", 也就是 '0x54', '0x43'

一个位串可以通过声明一个“b*”模板给 pack 或者 unpack 从一串 1 和 0 转换过来,或者是 转换成这些 1 和 0 的字串。另外,pack 可以和“b*”模板一起使用从一个 1 和 0 的字串创建 一个位串。该顺序和 vec 需要的顺序是兼容的。

   $bitstring = pack "b*", join('', @bits);
   print "$bitstring\n";   # "TC",和前面例子一样

unpack 可以用于从该位串中抽取这个 0 和 1 的列表:

   @bits = split(//, unpack("b*", $bitstring));
   print "@bits\n";      # 0 0 1 0 1 0 1 0 1 1 0 0 0 0 1 0

如果你知道位串的位的确切长度,那么这个长度可以用于“*”的位置。

参阅 select 获取使用 vec 生成的位图的其他的例子。参阅 pack 和 unpack 获取操作二进制 数据的更高级别的方法。

29.2.196. wait

这个函数等待一个子进程退出并返回消失了的进程的 PID,或者如果没有子进程了就返回 -1(或者 在一些系统里,子进程自动被收割也如此。)它在 $? 里返回的状态和在 system 里描述的一样。 如果你有僵死子进程,那么你就应该调用这个函数,或者 waitpid。

如果你在等待一个子进程,但是用 wait 没有找到它,那么你可能就是调用了 system,关闭了 一个管道,或者在 fork 和 wait 之间使用了反勾号。这些构造也做 wait(2) 并且因此可能收割 你的子进程。使用 waitpid 避免这样的情况。

29.2.197. waitpid

这个函数等待特定的子进程结束并在该进程死亡之后返回其 PID,如果没有其他的子进程时返回 -1,或者 FLAGS 里的标志声明的是非阻塞状态而该进程尚未退出,则返回 0。死亡的进程返回的 状态存储在 $?,并且和 system 里描述的一样。要获取有效的标志值,那么你必须输入 “:sys_wait_h”从 POSIX 里输入标签组。下面是一个等待所有挂起的僵死进程的非阻塞的例子:

   use POSIX ":sys_wait_h";
   do {
      $kid = waitpid(-1, &WNOHANG);
   } until $kid == -1;

在那些既没有实现 waitpid(2) 也没有实现 wait4(2) 系统调用的平台上,你可以声明的 FLAGS 只有 0。换句话说,你在那里可以等待一个特定的 PID,但是你不能在非阻塞模式里做这些事情。

在一些系统里,返回值为 -1 意味着该子进程被自动收割了,因为你设置了 $SIG{CHLD} = 'IGNORE'。

29.2.198. wantarray

如果当前正在执行的子过程在寻找列表数值,那么此函数返回真,否则返回假。如果调用环境需要 的是一个标量,那么该函数返回一个定义了的假(""),而如果调用环境并不需要任何东西,(也 就是说,空环境)那么返回一个未定义的假值(undef);

下面是它的典型用法的例子:

   return unless defined wantarray;      # 不需要干什么事情
   my @a = complex_calculation();
   return wantaray ? @a : \@a;

又见 caller。这个函数真是应该命名为“wantlist”,但是我们是在列表环境还叫数组环境的 时候命名它的。(译注:数组英文词是“array”,列表英文词是“list”。)

29.2.199 warn

这个函数生成一条错误信息,象 die 那样把 LIST 打印到 STDERR,但是并不试图退出或者抛出 一个例外。比如:

   warn "Debug enbled" if $debug;

如果 LIST 为空并且 $@ 已经包含一个数值(通常是前面的 eval 留下来的),那么字串 "\t... caught" 在 STDERR 上附加在 $@ 后面。(这样做类似 die 传播错误的方法,只不过 warn 并不传播(抛出)该例外。)如果你提供的字串上是空的,那么使用 "Warning: something's wrong"。

和 die 一样,如果你提供的字串并不是以换行符结尾,那么自动附加文件和行号信息。warn 函数 和 Perl 的 -w 命令行选项没有关系,但是可以和它一起使用,比如在你想模拟内建函数的时候:

   warn "Something wicked\n" if $^W;

如果安装了 $SIG{__WARN__} 句柄,那么不会打印任何信息。这个句柄是负责对它看到的信息进行 适当处理用的。你想这么做的一个原因可能是把简单的警告转化成一个例外:

   
   local $SIG{__WARN__} = sub {
      my $msg = shift;
      die $msg if $msg =~ /isn't numeric/;
   };

因此大多数句柄都必须对那些它们原先没有准备处理的警告安排显示处理的工作,方法是在句柄里 调用 warn。这么做非常安全,它不会产生无限的循环,因为 WARN 挂钩不会在 WARN 里面被调用。这个行为和 $SIG{__DIE__} 的行为(它不会消除错误文本,但是可以再次调用 die 来改变它)略有区别。

使用 WARN 句柄给我们提供了一个强有力的抑制所有警告的方法,甚至连那些强制性的警告也 给抑制住了。有时候你需要把这个东西封装在 BEGINP{} 块里,这样它就可以在编译时起做用:

# 扫荡掉所有编译时间警告

   BEGIN { $SIG{__WARN__} = sub { warn $_[0] if $DOWARN } }
   my $foo = 10;
   my $foo = 20;      # 不要警告我说重复了 my $foo,
            # 不过,这可是你说的!

   # 在这以前没有编译时和运行时的警告
   $DOWARN = 1;      # 不是一个内建的变量

   #  在这以后打开运行时的警告
   warn "\$foo is alive an $foo!";   # 做显示

参阅 use warnings 用法获取警告的词法范围里的控制。参阅 Carp 模块里的 carp 和 cluck 函数获取其他制造警告信息的方法。

29.2.200. write

这个函数写一条格式化了的记录(可能是多行)到声明的文件句柄,使用和该文件句柄相关联的 格式——参阅第七章里的“格式变量”一节。缺省时与文件句柄相关联的格式是和文件句柄同名的 那个。不过,一个文件句柄的格式可以在你 select 了该句柄以后修改 $~ 变量来修改:

   $old_fh = select(HANDLE);
   $~ = "NEWNAME";
   select($old_fh);

或者说:

   use IO::Handle
   HANLDE->format_name("NEWNAME");

因为格式是放到一个包名字空间里的,所以如果该 format 是在另外一个包里声明的,那么你可能 不得不用该格式的全称:

   $~ = "OtherPack::NEWNAEM";

表单顶部(Top-of-form)的处理是自动操作的:如果当前页里面没有足够的空间容纳格式化的 记录,那么通过写一个进纸符来续该页,这时候在新页上使用一个特殊的表单顶部格式,然后写该 记录。在当前页里余下的行数放在变量 $- 里,你可以把它设置为 0 以强迫在下一次 write 的 时候使用新的一页。(你可能先得 select 该文件句柄。)缺省时,页顶格式的名字就是文件句柄 后面加上 "_TOP",不过,一个文件句柄的格式可以在你 select 了该句柄以后修改 $~ 变量来 修改,或者说:

   use IO::Handle;
   HANDLE->format_top_name("NEWNAME_TOP");

如果没有声明 FILEHANDLE,那么输出就会跑到当前缺省的输出文件句柄,这个文件句柄初始时是 STDOUT,但是可以用单参数形式的 select 操作符修改。如果 FILEHANDLE 是一个表达式,那么 在运行时计算该表达式以决定实际的 FILEHANDLE。

如果声明的 format 或者当前的页顶 format 并不存在,那么抛出一个例外。

糟糕的是,write 函数不是 read 的逆操作。用 print 做简单的字串输出。如果你找到这个函数 的原因是因为你想绕开标准 I/O,参阅 syswrite。

29.2.201 y//

转换操作符(因历史原因,也叫做翻译操作符),也叫做 tr///。参阅第五章。


to top