内容

名称

perlfaq8 - 系统交互

版本

版本 5.20210520

描述

本节 Perl FAQ 涵盖了与操作系统交互相关的问题。主题包括进程间通信 (IPC)、对用户界面(键盘、屏幕和指向设备)的控制,以及与数据操作无关的任何其他内容。

阅读与 Perl 在您的操作系统上的移植相关的 FAQ 和文档(例如,perlvmsperlplan9 等)。这些应该包含有关您的 Perl 的特殊情况的更详细的信息。

如何确定我正在运行哪个操作系统?

$^O 变量(如果您使用 English 则为 $OSNAME)包含您的 Perl 二进制文件构建的 operating system 的名称(而不是其版本号)。

为什么 exec() 不返回?

(由 brian d foy 贡献)

exec 函数的作用是将您的进程转换为另一个命令,并且永远不会返回。如果您不想这样做,请不要使用 exec。 :)

如果您想运行外部命令并保持 Perl 进程继续运行,请查看管道 openforksystem

如何使用键盘/屏幕/鼠标执行复杂操作?

您访问/控制键盘、屏幕和指向设备(“鼠标”)的方式取决于系统。尝试以下模块

键盘
Term::Cap               Standard perl distribution
Term::ReadKey           CPAN
Term::ReadLine::Gnu     CPAN
Term::ReadLine::Perl    CPAN
Term::Screen            CPAN
屏幕
Term::Cap               Standard perl distribution
Curses                  CPAN
Term::ANSIColor         CPAN
鼠标
Tk                      CPAN
Wx                      CPAN
Gtk2                    CPAN
Qt4                     kdebindings4 package

本节 perlfaq 中的其他答案中以示例形式展示了其中一些特定情况。

如何以彩色打印内容?

通常情况下,您不能,因为您不知道接收者是否拥有支持颜色的显示设备。如果您知道他们拥有支持颜色的 ANSI 终端,则可以使用 CPAN 上的 Term::ANSIColor 模块

use Term::ANSIColor;
print color("red"), "Stop!\n", color("reset");
print color("green"), "Go!\n", color("reset");

或者像这样

use Term::ANSIColor qw(:constants);
print RED, "Stop!\n", RESET;
print GREEN, "Go!\n", RESET;

如何只读取一个键而不等待回车键?

控制输入缓冲是一个非常依赖系统的操作。在许多系统上,您可以像 "perlfunc 中的 getc" 中所示的那样使用 stty 命令,但正如您所见,这已经让您陷入了可移植性问题。

open(TTY, "+</dev/tty") or die "no tty: $!";
system "stty  cbreak </dev/tty >/dev/tty 2>&1";
$key = getc(TTY);        # perhaps this works
# OR ELSE
sysread(TTY, $key, 1);    # probably this does
system "stty -cbreak </dev/tty >/dev/tty 2>&1";

CPAN 上的 Term::ReadKey 模块提供了一个易于使用的接口,它应该比对每个键都使用 stty 命令更有效率。它甚至包含对 Windows 的有限支持。

use Term::ReadKey;
ReadMode('cbreak');
$key = ReadKey(0);
ReadMode('normal');

但是,使用该代码需要您有一个可用的 C 编译器,并且可以使用它来构建和安装 CPAN 模块。以下是一个使用标准 POSIX 模块的解决方案,该模块已存在于您的系统中(假设您的系统支持 POSIX)。

use HotKey;
$key = readkey();

以下是如何使用 HotKey 模块,它隐藏了对 POSIX termios 结构进行操作的有些令人费解的调用。

# HotKey.pm
package HotKey;

use strict;
use warnings;

use parent 'Exporter';
our @EXPORT = qw(cbreak cooked readkey);

use POSIX qw(:termios_h);
my ($term, $oterm, $echo, $noecho, $fd_stdin);

$fd_stdin = fileno(STDIN);
$term     = POSIX::Termios->new();
$term->getattr($fd_stdin);
$oterm     = $term->getlflag();

$echo     = ECHO | ECHOK | ICANON;
$noecho   = $oterm & ~$echo;

sub cbreak {
    $term->setlflag($noecho);  # ok, so i don't want echo either
    $term->setcc(VTIME, 1);
    $term->setattr($fd_stdin, TCSANOW);
}

sub cooked {
    $term->setlflag($oterm);
    $term->setcc(VTIME, 0);
    $term->setattr($fd_stdin, TCSANOW);
}

sub readkey {
    my $key = '';
    cbreak();
    sysread(STDIN, $key, 1);
    cooked();
    return $key;
}

END { cooked() }

1;

如何检查键盘上是否有输入准备就绪?

最简单的方法是使用 CPAN 上的 Term::ReadKey 模块以非阻塞模式读取一个键,并传递 -1 作为参数以指示不阻塞。

use Term::ReadKey;

ReadMode('cbreak');

if (defined (my $char = ReadKey(-1)) ) {
    # input was waiting and it was $char
} else {
    # no input was waiting
}

ReadMode('normal');                  # restore normal tty settings

如何清除屏幕?

(由 brian d foy 贡献)

要清除屏幕,您只需打印告诉终端清除屏幕的特殊序列。一旦您有了该序列,您就可以在需要清除屏幕时输出它。

您可以使用 Term::ANSIScreen 模块来获取特殊序列。导入 cls 函数(或 :screen 标签)。

use Term::ANSIScreen qw(cls);
my $clear_screen = cls();

print $clear_screen;

如果您想处理终端控制的底层细节,Term::Cap 模块也可以获取特殊序列。Tputs 方法返回给定功能的字符串。

use Term::Cap;

my $terminal = Term::Cap->Tgetent( { OSPEED => 9600 } );
my $clear_screen = $terminal->Tputs('cl');

print $clear_screen;

在 Windows 上,您可以使用 Win32::Console 模块。在为要影响的输出文件句柄创建对象后,调用 Cls 方法。

Win32::Console;

my $OUT = Win32::Console->new(STD_OUTPUT_HANDLE);
my $clear_string = $OUT->Cls;

print $clear_screen;

如果您有一个执行此任务的命令行程序,您可以在反引号中调用它以捕获它的输出,以便您以后可以使用它。

my $clear_string = `clear`;

print $clear_string;

如何获取屏幕尺寸?

如果您从 CPAN 安装了 Term::ReadKey 模块,您可以使用它来获取以字符和像素为单位的宽度和高度。

use Term::ReadKey;
my ($wchar, $hchar, $wpixels, $hpixels) = GetTerminalSize();

这比原始的 ioctl 更便携,但没有那么说明性。

require './sys/ioctl.ph';
die "no TIOCGWINSZ " unless defined &TIOCGWINSZ;
open(my $tty_fh, "+</dev/tty")                     or die "No tty: $!";
unless (ioctl($tty_fh, &TIOCGWINSZ, $winsize='')) {
    die sprintf "$0: ioctl TIOCGWINSZ (%08x: $!)\n", &TIOCGWINSZ;
}
my ($row, $col, $xpixel, $ypixel) = unpack('S4', $winsize);
print "(row,col) = ($row,$col)";
print "  (xpixel,ypixel) = ($xpixel,$ypixel)" if $xpixel || $ypixel;
print "\n";

如何向用户询问密码?

(这个问题与网络无关。请参阅其他常见问题解答。)

"perlfunc 中的 crypt" 中有一个关于此的示例。首先,您将终端置于“无回显”模式,然后正常读取密码。您可以使用旧式的 ioctl() 函数、POSIX 终端控制(参见 POSIX 或其文档“骆驼书”)或对 stty 程序的调用来实现,这些方法的可移植性各不相同。

您还可以使用 CPAN 上的 Term::ReadKey 模块为大多数系统执行此操作,该模块更易于使用,理论上也更便携。

use Term::ReadKey;

ReadMode('noecho');
my $password = ReadLine(0);

如何读取和写入串行端口?

这取决于您的程序运行在哪个操作系统上。在 Unix 系统中,串行端口可以通过 /dev 目录下的文件访问;在其他系统中,设备名称可能会有所不同。以下是一些所有设备交互中常见的几个问题区域。

锁文件

您的系统可能使用锁文件来控制多个访问。请确保您遵循正确的协议。多个进程从一个设备读取数据会导致不可预测的行为。

打开模式

如果您希望对设备进行读写操作,则需要以更新模式打开它(有关详细信息,请参阅 "perlfunc 中的 open")。您可能希望使用 sysopen()O_RDWR|O_NDELAY|O_NOCTTYFcntl 模块(标准 perl 发行版的一部分)打开它,而不会冒阻塞的风险。有关此方法的更多信息,请参阅 "perlfunc 中的 sysopen"

行尾

某些设备期望在每行末尾使用 "\r" 而不是 "\n"。在某些 perl 端口中,"\r" 和 "\n" 与它们通常的(Unix)ASCII 值 "\015" 和 "\012" 不同。您可能需要直接给出所需的数值,使用八进制 ("\015")、十六进制 ("0x0D") 或作为控制字符规范 ("\cM")。

print DEV "atv1\012";    # wrong, for some devices
print DEV "atv1\015";    # right, for some devices

即使对于普通文本文件,"\n" 可以解决问题,但仍然没有统一的方案来终止跨 Unix、DOS/Win 和 Macintosh 可移植的行,除非将所有行尾都终止为 "\015\012",并从输出中删除不需要的部分。这尤其适用于套接字 I/O 和自动刷新,将在下面讨论。

刷新输出

如果您希望在 print() 时将字符发送到您的设备,您需要自动刷新该文件句柄。您可以使用 select()$| 变量来控制自动刷新(请参阅 "perlvar 中的 $|""perlfunc 中的 select",或 perlfaq5,“如何刷新/取消缓冲输出文件句柄?为什么我必须这样做?”)。

my $old_handle = select($dev_fh);
$| = 1;
select($old_handle);

您还会看到没有使用临时变量的代码,例如

select((select($deb_handle), $| = 1)[0]);

或者,如果您不介意因为害怕使用 $| 变量而引入几千行代码

use IO::Handle;
$dev_fh->autoflush(1);

如前所述,这在 Unix 和 Macintosh 之间使用套接字 I/O 时仍然不起作用。在这种情况下,您需要硬编码行终止符。

非阻塞输入

如果您正在执行阻塞式read()sysread()操作,则需要安排一个报警处理程序来提供超时(参见"perlfunc 中的 alarm")。如果您使用的是非阻塞式打开,则很可能进行非阻塞式读取,这意味着您可能需要使用 4 个参数的select()来确定该设备上的 I/O 是否已准备好(参见"perlfunc 中的 select")。

在尝试从他的来电显示器读取数据时,臭名昭著的 Jamie Zawinski <[email protected]>在经历了无数次咬牙切齿和与sysreadsysopen、POSIX 的tcgetattr函数以及其他各种在夜间发出砰砰声的函数作斗争之后,最终想出了这个方法

sub open_modem {
    use IPC::Open2;
    my $stty = `/bin/stty -g`;
    open2( \*MODEM_IN, \*MODEM_OUT, "cu -l$modem_device -s2400 2>&1");
    # starting cu hoses /dev/tty's stty settings, even when it has
    # been opened on a pipe...
    system("/bin/stty $stty");
    $_ = <MODEM_IN>;
    chomp;
    if ( !m/^Connected/ ) {
        print STDERR "$0: cu printed `$_' instead of `Connected'\n";
    }
}

如何解码加密的密码文件?

您在专用硬件上花费了大量的资金,但这肯定会引起人们的议论。

说真的,如果您使用的是 Unix 密码文件,您无法解码它们 - Unix 密码系统使用的是单向加密。它更像是哈希而不是加密。您所能做的最好的事情是检查其他内容是否哈希到相同的字符串。您无法将哈希值转换回原始字符串。像 Crack 这样的程序可以强制(并且智能地)尝试猜测密码,但不能(无法)保证快速成功。

如果您担心用户选择不安全的密码,您应该在他们尝试更改密码时主动进行检查(例如,通过修改passwd(1))。

如何将进程在后台启动?

(由 brian d foy 贡献)

没有一种方法可以在后台运行代码,这样您就不必等待它完成才能让程序继续执行其他任务。进程管理取决于您的特定操作系统,许多技术在perlipc中都有介绍。

一些 CPAN 模块可以提供帮助,包括IPC::Open2IPC::Open3IPC::RunParallel::JobsParallel::ForkManagerPOEProc::BackgroundWin32::Process。您可能还会使用许多其他模块,因此请检查这些命名空间以获取其他选项。

如果您使用的是类 Unix 系统,您可能可以使用系统调用,在命令末尾添加一个&

system("cmd &")

您还可以尝试使用fork,如perlfunc中所述(尽管这与许多模块为您执行的操作相同)。

STDIN、STDOUT 和 STDERR 是共享的

主进程和后台进程(“子”进程)共享相同的 STDIN、STDOUT 和 STDERR 文件句柄。如果两者同时尝试访问它们,可能会发生奇怪的事情。您可能希望为子进程关闭或重新打开这些句柄。您可以通过使用 open 打开管道(参见 "perlfunc 中的 open")来解决这个问题,但在某些系统上,这意味着子进程不能比父进程存活更长时间。

信号

您需要捕获 SIGCHLD 信号,可能还需要捕获 SIGPIPE 信号。SIGCHLD 在后台进程完成时发送。SIGPIPE 在您写入子进程已关闭的文件句柄时发送(未捕获的 SIGPIPE 会导致您的程序静默死亡)。对于 system("cmd&"),这不是问题。

僵尸进程

您必须准备好“收割”子进程,当它完成时。

$SIG{CHLD} = sub { wait };

$SIG{CHLD} = 'IGNORE';

您也可以使用双重 fork。您立即对第一个子进程使用 wait(),而 init 守护进程将在您的孙进程退出后对它使用 wait()

unless ($pid = fork) {
    unless (fork) {
        exec "what you really wanna do";
        die "exec failed!";
    }
    exit 0;
}
waitpid($pid, 0);

参见 "perlipc 中的信号",以获取执行此操作的其他代码示例。对于 system("prog &"),僵尸进程不是问题。

如何捕获控制字符/信号?

您实际上并没有“捕获”控制字符。相反,该字符会生成一个信号,该信号被发送到终端当前的前台进程组,然后您在您的进程中捕获该信号。信号在 "perlipc 中的信号" 和骆驼书中的“信号”部分有说明。

您可以将 %SIG 哈希的值设置为要处理信号的函数。在 Perl 捕获信号后,它会在 %SIG 中查找与信号同名的键,然后调用该键的子例程值。

# as an anonymous subroutine

$SIG{INT} = sub { syswrite(STDERR, "ouch\n", 5 ) };

# or a reference to a function

$SIG{INT} = \&ouch;

# or the name of the function as a string

$SIG{INT} = "ouch";

5.8 之前的 Perl 版本在其 C 源代码中包含信号处理程序,这些处理程序会捕获信号,并可能运行您在 %SIG 中设置的 Perl 函数。这违反了该级别信号处理的规则,导致 perl 崩溃。从 5.8.0 版本开始,perl 在捕获信号后查看 %SIG,而不是在捕获信号时查看。此答案的先前版本是错误的。

如何在 Unix 系统上修改 shadow 密码文件?

如果 Perl 安装正确且您的 shadow 库编写正确,则 perlfunc 中描述的 getpw*() 函数理论上应该提供对 shadow 密码文件条目的(只读)访问权限。要更改文件,请创建一个新的 shadow 密码文件(格式因系统而异 - 请参阅 passwd(1) 获取详细信息)并使用 pwd_mkdb(8) 安装它(有关更多详细信息,请参阅 pwd_mkdb(8))。

如何设置时间和日期?

假设您以足够的权限运行,您应该能够通过运行 date(1) 程序来设置系统范围的日期和时间。(无法在每个进程的基础上设置时间和日期。)此机制适用于 Unix、MS-DOS、Windows 和 NT;VMS 等效项是 set time

但是,如果您只想更改时区,您可能可以通过设置环境变量来解决。

$ENV{TZ} = "MST7MDT";           # Unixish
$ENV{'SYS$TIMEZONE_DIFFERENTIAL'}="-5" # vms
system('trn', 'comp.lang.perl.misc');

如何让 sleep() 或 alarm() 在不到一秒的时间内运行?

如果您想要比 sleep() 函数提供的 1 秒更精细的粒度,最简单的方法是使用 "select" in perlfunc 中记录的 select() 函数。尝试使用 Time::HiResBSD::Itimer 模块(可从 CPAN 获取,从 Perl 5.8 开始 Time::HiRes 是标准发行版的一部分)。

如何测量不到一秒的时间?

(由 brian d foy 贡献)

Time::HiRes 模块(从 Perl 5.8 开始是标准发行版的一部分)使用 gettimeofday() 系统调用来测量时间,该调用返回自纪元以来的微秒数。如果您无法为旧的 Perl 安装 Time::HiRes 并且您使用的是类 Unix 系统,您可能可以直接调用 gettimeofday(2)。请参阅 "syscall" in perlfunc

如何执行 atexit() 或 setjmp()/longjmp()?(异常处理)

您可以使用 END 块来模拟 atexit()。每个包的 END 块在程序或线程结束时被调用。有关 END 块的更多详细信息,请参阅 perlmod 手册页。

例如,您可以使用它来确保您的过滤器程序成功完成其输出,而不会填满磁盘。

END {
    close(STDOUT) || die "stdout close failed: $!";
}

但是,当未捕获的信号杀死程序时,END 块不会被调用,因此如果您使用 END 块,您也应该使用

use sigtrap qw(die normal-signals);

Perl 的异常处理机制是它的 eval() 运算符。您可以将 eval() 用作 setjmp,将 die() 用作 longjmp。有关此的详细信息,请参阅有关信号的部分,尤其是 "perlipc 中的信号" 中阻塞 flock() 的超时处理程序,或 Programming Perl 中的“信号”部分。

如果您只对异常处理感兴趣,请使用许多处理异常的 CPAN 模块之一,例如 Try::Tiny

如果您想要 atexit() 语法(以及 rmexit()),请尝试使用 CPAN 上提供的 AtExit 模块。

为什么我的套接字程序在 System V(Solaris)下无法正常工作?错误消息“协议不支持”是什么意思?

一些基于 Sys-V 的系统,特别是 Solaris 2.X,重新定义了一些标准套接字常量。由于这些常量在所有架构中都是一致的,因此它们经常被硬编码到 perl 代码中。处理此问题的正确方法是“使用 Socket”来获取正确的值。

请注意,即使 SunOS 和 Solaris 是二进制兼容的,这些值也是不同的。去弄清楚。

如何从 Perl 调用我系统的唯一 C 函数?

在大多数情况下,您编写一个外部模块来执行此操作 - 请参阅“我在哪里可以学习将 C 与 Perl 链接?[h2xs, xsubpp]”的答案。但是,如果该函数是系统调用,并且您的系统支持 syscall(),则可以使用 syscall 函数(在 perlfunc 中有记录)。

请记住检查随您的发行版一起提供的模块,以及 CPAN - 有人可能已经编写了一个模块来执行此操作。在 Windows 上,请尝试 Win32::API。在 Mac 上,请尝试 Mac::Carbon。如果没有模块具有对 C 函数的接口,您可以使用 Inline::C 在 Perl 源代码中内联一些 C 代码。

我在哪里可以获取执行 ioctl() 或 syscall() 的包含文件?

从历史上看,这些将由标准 perl 发行版的一部分的 h2ph 工具生成。此程序将 C 头文件中的 cpp(1) 指令转换为包含子程序定义的文件,例如 SYS_getitimer(),您可以将其用作函数的参数。它不能完美地工作,但通常可以完成大部分工作。像 errno.hsyscall.hsocket.h 这样的简单文件很好,但像 ioctl.h 这样的难文件几乎总是需要手动编辑。以下是如何安装 *.ph 文件

1. Become the super-user
2. cd /usr/include
3. h2ph *.h */*.h

如果您的系统支持动态加载,出于可移植性和理智的原因,您可能应该使用 h2xs(也是标准 perl 发行版的一部分)。此工具将 C 头文件转换为 Perl 扩展。有关如何开始使用 h2xs 的信息,请参阅 perlxstut

如果您的系统不支持动态加载,您可能仍然应该使用 h2xs。有关更多信息,请参阅 perlxstutExtUtils::MakeMaker(简而言之,只需使用 make perl 而不是普通的 make 来使用新的静态扩展重新构建 perl)。

为什么 setuid perl 脚本会抱怨内核问题?

某些操作系统内核存在漏洞,导致 setuid 脚本天生不安全。Perl 提供了一些选项(在 perlsec 中描述)来解决此类系统的问题。

如何同时向命令写入和读取管道?

IPC::Open2 模块(标准 perl 发行版的一部分)是一种易于使用的方案,它在内部使用 pipe()fork()exec() 来完成工作。但是,请务必阅读其文档中的死锁警告(参见 IPC::Open2)。参见 "perlipc 中的双向通信与另一个进程""perlipc 中的双向通信与自身"

您也可以使用 IPC::Open3 模块(标准 perl 发行版的一部分),但请注意,它的参数顺序与 IPC::Open2 不同(参见 IPC::Open3)。

为什么我无法使用 system() 获取命令的输出?

您混淆了 system() 和反引号(``)的目的。system() 运行命令并返回退出状态信息(以 16 位值的形式:低 7 位是进程死亡的信号(如果有),高 8 位是实际的退出值)。反引号(``)运行命令并返回它发送到 STDOUT 的内容。

my $exit_status   = system("mail-users");
my $output_string = `ls`;

如何捕获外部命令的 STDERR?

运行外部命令有三种基本方法

system $cmd;        # using system()
my $output = `$cmd`;        # using backticks (``)
open (my $pipe_fh, "$cmd |");    # using open()

使用system()时,STDOUT 和 STDERR 会与脚本的 STDOUT 和 STDERR 指向相同的位置,除非system()命令对其进行重定向。反引号和open()只读取命令的STDOUT

您也可以使用来自IPC::Open3open3()函数。Benjamin Goldberg 提供了一些示例代码。

要捕获程序的 STDOUT,但丢弃其 STDERR

use IPC::Open3;
use File::Spec;
my $in = '';
open(NULL, ">", File::Spec->devnull);
my $pid = open3($in, \*PH, ">&NULL", "cmd");
while( <PH> ) { }
waitpid($pid, 0);

要捕获程序的 STDERR,但丢弃其 STDOUT

use IPC::Open3;
use File::Spec;
my $in = '';
open(NULL, ">", File::Spec->devnull);
my $pid = open3($in, ">&NULL", \*PH, "cmd");
while( <PH> ) { }
waitpid($pid, 0);

要捕获程序的 STDERR,并让其 STDOUT 输出到我们自己的 STDERR

use IPC::Open3;
my $in = '';
my $pid = open3($in, ">&STDERR", \*PH, "cmd");
while( <PH> ) { }
waitpid($pid, 0);

要分别读取命令的 STDOUT 和 STDERR,您可以将它们重定向到临时文件,让命令运行,然后读取临时文件。

use IPC::Open3;
use IO::File;
my $in = '';
local *CATCHOUT = IO::File->new_tmpfile;
local *CATCHERR = IO::File->new_tmpfile;
my $pid = open3($in, ">&CATCHOUT", ">&CATCHERR", "cmd");
waitpid($pid, 0);
seek $_, 0, 0 for \*CATCHOUT, \*CATCHERR;
while( <CATCHOUT> ) {}
while( <CATCHERR> ) {}

但实际上没有必要让两者都是临时文件... 以下方法应该也能正常工作,不会出现死锁。

use IPC::Open3;
my $in = '';
use IO::File;
local *CATCHERR = IO::File->new_tmpfile;
my $pid = open3($in, \*CATCHOUT, ">&CATCHERR", "cmd");
while( <CATCHOUT> ) {}
waitpid($pid, 0);
seek CATCHERR, 0, 0;
while( <CATCHERR> ) {}

而且速度也会更快,因为我们可以立即开始处理程序的 stdout,而不是等待程序完成。

对于任何一种方法,您可以在调用之前更改文件描述符。

open(STDOUT, ">logfile");
system("ls");

或者您可以使用 Bourne shell 文件描述符重定向。

$output = `$cmd 2>some_file`;
open (PIPE, "cmd 2>some_file |");

您也可以使用文件描述符重定向使 STDERR 成为 STDOUT 的副本。

$output = `$cmd 2>&1`;
open (PIPE, "cmd 2>&1 |");

请注意,您不能简单地将 STDERR 打开为 Perl 程序中 STDOUT 的副本,并避免调用 shell 来进行重定向。这行不通。

open(STDERR, ">&STDOUT");
$alloutput = `cmd args`;  # stderr still escapes

这是因为open()使 STDERR 指向open()时 STDOUT 所指向的位置。反引号随后使 STDOUT 指向一个字符串,但不会更改 STDERR(它仍然指向旧的 STDOUT)。

请注意,您必须在反引号中使用 Bourne shell (sh(1)) 重定向语法,而不是csh(1)!有关 Perl 的system()、反引号和管道打开都使用 Bourne shell 的详细信息,请参阅 http://www.cpan.org/misc/olddoc/FMTEYEWTK.tgz 中“Far More Than You Ever Wanted To Know” 集合中的versus/csh.whynot 文章。要捕获命令的 STDERR 和 STDOUT

$output = `cmd 2>&1`;                       # either with backticks
$pid = open(PH, "cmd 2>&1 |");              # or with an open pipe
while (<PH>) { }                            #    plus a read

要捕获命令的 STDOUT,但丢弃其 STDERR

$output = `cmd 2>/dev/null`;                # either with backticks
$pid = open(PH, "cmd 2>/dev/null |");       # or with an open pipe
while (<PH>) { }                            #    plus a read

要捕获命令的 STDERR,但丢弃其 STDOUT

$output = `cmd 2>&1 1>/dev/null`;           # either with backticks
$pid = open(PH, "cmd 2>&1 1>/dev/null |");  # or with an open pipe
while (<PH>) { }                            #    plus a read

要交换命令的 STDOUT 和 STDERR,以便捕获 STDERR,但让其 STDOUT 输出到我们的旧 STDERR

$output = `cmd 3>&1 1>&2 2>&3 3>&-`;        # either with backticks
$pid = open(PH, "cmd 3>&1 1>&2 2>&3 3>&-|");# or with an open pipe
while (<PH>) { }                            #    plus a read

要分别读取命令的 STDOUT 和 STDERR,最简单的方法是将它们分别重定向到文件,然后在程序完成后从这些文件读取。

system("program args 1>program.stdout 2>program.stderr");

在所有这些示例中,顺序很重要。这是因为 shell 按严格的从左到右顺序处理文件描述符重定向。

system("prog args 1>tmpfile 2>&1");
system("prog args 2>&1 1>tmpfile");

第一个命令将标准输出和标准错误都发送到临时文件。第二个命令只将旧的标准输出发送到那里,而旧的标准错误显示在旧的标准输出上。

为什么open()在管道打开失败时不返回错误?

如果管道open()的第二个参数包含 shell 元字符,perl 会fork(),然后exec()一个 shell 来解码元字符,并最终运行所需的程序。如果程序无法运行,则是 shell 收到消息,而不是 Perl。您的 Perl 程序只能查明 shell 本身是否能够成功启动。您仍然可以捕获 shell 的 STDERR 并检查它是否有错误消息。请参阅本文档其他地方的"如何捕获外部命令的 STDERR?",或使用IPC::Open3 模块。

如果 `open()` 的参数中没有 shell 元字符,Perl 会直接运行命令,而不会使用 shell,并且可以正确报告命令是否启动。

在空上下文使用反引号有什么问题?

严格来说,没什么问题。从风格上来说,这不是编写可维护代码的好方法。Perl 有几种运行外部命令的操作符。反引号是其中之一;它们收集命令的输出以供程序使用。`system` 函数是另一种;它不这样做。

在程序中编写反引号向代码阅读者传达了一个明确的信息,即你想要收集命令的输出。为什么要发送一个不真实的明确信息呢?

考虑以下代码行

`cat /etc/termcap`;

你忘记检查 `$?` 以查看程序是否正确运行。即使你写了

print `cat /etc/termcap`;

这段代码可以而且应该写成

system("cat /etc/termcap") == 0
or die "cat program failed!";

这将根据 cat 命令的输出进行回显,而不是等到程序完成才打印出来。它还会检查返回值。

`system` 还提供对 shell 通配符处理是否可能发生的直接控制,而反引号则没有。

如何调用反引号而不进行 shell 处理?

这有点棘手。你不能简单地像这样写命令

@ok = `grep @opts '$search_string' @filenames`;

从 Perl 5.8.0 开始,你可以使用带有多个参数的 `open()`。就像 `system()` 和 `exec()` 的列表形式一样,不会发生任何 shell 转义。

open( GREP, "-|", 'grep', @opts, $search_string, @filenames );
chomp(@ok = <GREP>);
close GREP;

你也可以

my @ok = ();
if (open(GREP, "-|")) {
    while (<GREP>) {
        chomp;
        push(@ok, $_);
    }
    close GREP;
} else {
    exec 'grep', @opts, $search_string, @filenames;
}

就像 `system()` 一样,当你 `exec()` 一个列表时,不会发生任何 shell 转义。有关此的更多示例,请参阅 "perlipc 中的安全管道打开"

请注意,如果你使用的是 Windows,则无法解决此令人烦恼的问题。即使 Perl 模拟了 `fork()`,你仍然会遇到问题,因为 Windows 没有 argc/argv 样式的 API。

为什么我的脚本在输入 EOF 后(Unix 上为 ^D,MS-DOS 上为 ^Z)无法从 STDIN 读取?

这种情况只发生在你的 perl 被编译为使用 stdio 而不是 perlio 时,而 perlio 是默认的。一些(也许是所有?)stdio 会设置你可能需要清除的错误和 eof 标志。 POSIX 模块定义了 clearerr(),你可以使用它。这是技术上正确的方法。以下是一些不太可靠的解决方法

  1. 尝试保留 seekpointer 并转到那里,例如

    my $where = tell($log_fh);
    seek($log_fh, $where, 0);
  2. 如果这不起作用,请尝试跳转到文件的不同部分,然后返回。

  3. 如果这不起作用,请尝试跳转到文件的不同部分,读取一些内容,然后返回。

  4. 如果这不起作用,放弃你的 stdio 包并使用 sysread。

如何将我的 shell 脚本转换为 perl?

学习 Perl 并重写它。说真的,没有简单的转换器。在 shell 中做起来很麻烦的事情在 Perl 中很容易做,而这种笨拙正是 shell->perl 转换器几乎不可能编写的原因。通过重写它,你会思考你真正想做的事情,并希望能够摆脱 shell 的管道数据流范式,虽然它在某些情况下很方便,但会导致许多效率低下。

我可以使用 perl 来运行 telnet 或 ftp 会话吗?

尝试使用 Net::FTPTCP::ClientNet::Telnet 模块(可从 CPAN 获取)。 http://www.cpan.org/scripts/netstuff/telnet.emul.shar 也有助于模拟 telnet 协议,但 Net::Telnet 很可能更容易使用。

如果你只想假装是 telnet 但不需要初始 telnet 握手,那么标准的双进程方法就足够了

use IO::Socket;             # new in 5.004
my $handle = IO::Socket::INET->new('www.perl.com:80')
    or die "can't connect to port 80 on www.perl.com $!";
$handle->autoflush(1);
if (fork()) {               # XXX: undef means failure
    select($handle);
    print while <STDIN>;    # everything from stdin to socket
} else {
    print while <$handle>;  # everything from socket to stdout
}
close $handle;
exit;

如何在 Perl 中编写 expect?

曾经有一个名为 chat2.pl 的库(标准 perl 发行版的一部分),它从未真正完成。如果你在某个地方找到了它,不要使用它。如今,你最好的选择是查看可从 CPAN 获取的 Expect 模块,它还需要 CPAN 中的另外两个模块,IO::PtyIO::Stty

有没有办法从诸如“ps”之类的程序中隐藏 perl 的命令行?

首先要注意,如果你出于安全原因(例如,为了防止人们看到密码)这样做,那么你应该重写你的程序,以便关键信息永远不会作为参数传递。隐藏参数并不能使你的程序完全安全。

要实际更改可见的命令行,你可以像 perlvar 中所述的那样,将值赋给变量 $0。不过,这并不适用于所有操作系统。像 sendmail 这样的守护进程会将它们的状态放在那里,例如

$0 = "orcus [accepting connections]";

我在 Perl 脚本中 {更改了目录,修改了我的环境}。为什么当我退出脚本时,更改消失了?如何使我的更改可见?

Unix

严格来说,这是不可能的——脚本作为与启动它的 shell 不同的进程执行。对进程的更改不会反映在其父进程中——只会反映在更改后创建的任何子进程中。可能有一些 shell 魔术可以让你通过在你的 shell 中 eval() 脚本的输出来伪造它;查看 comp.unix.questions FAQ 获取详细信息。

如何在不等待进程完成的情况下关闭进程的文件句柄?

假设你的系统支持这些功能,只需向进程发送适当的信号(参见 "kill" in perlfunc)。通常的做法是先发送 TERM 信号,等待一段时间,然后发送 KILL 信号来结束它。

如何派生一个守护进程?

如果你指的是一个分离的(与它的 tty 分离)守护进程,那么以下过程据报道在大多数类 Unix 系统上都能正常工作。非 Unix 用户应该检查他们的 Your_OS::Process 模块以获取其他解决方案。

来自 CPAN 的 Proc::Daemon 模块提供了一个函数来为你执行这些操作。

如何确定我是否正在交互式运行?

(由 brian d foy 贡献)

这是一个很难回答的问题,最好的答案只是一个猜测。

你真正想知道的是什么?如果你只是想知道你的某个文件句柄是否连接到终端,你可以尝试使用 -t 文件测试。

if( -t STDOUT ) {
    print "I'm connected to a terminal!\n";
}

但是,如果你期望这意味着在另一端有一个真人,你可能会失望。使用 Expect 模块,另一个程序可以假装成一个人。这个程序甚至可能接近通过图灵测试。

IO::Interactive 模块尽其所能给你一个答案。它的 is_interactive 函数返回一个输出文件句柄;如果该模块认为会话是交互式的,则该文件句柄指向标准输出。否则,该文件句柄是一个空句柄,它只是丢弃输出。

use IO::Interactive;

print { is_interactive } "I might go to standard output!\n";

这仍然不能保证有一个真人正在回答你的提示或阅读你的输出。

如果你想知道如何处理你的发行版的自动化测试,你可以检查环境。例如,CPAN 测试人员设置了 AUTOMATED_TESTING 的值。

unless( $ENV{AUTOMATED_TESTING} ) {
    print "Hello interactive tester!\n";
}

如何超时一个缓慢的事件?

使用 alarm() 函数,可能与信号处理程序结合使用,如 "perlipc 中的信号" 和骆驼书中关于 "信号" 的部分所述。你也可以使用来自 CPAN 的更灵活的 Sys::AlarmCall 模块。

alarm() 函数并非在所有版本的 Windows 上都实现。请查看你特定版本的 Perl 的文档。

如何设置 CPU 限制?

(由 Xho 贡献)

使用来自 CPAN 的 BSD::Resource 模块。例如

use BSD::Resource;
setrlimit(RLIMIT_CPU,10,20) or die $!;

这将软限制和硬限制分别设置为 10 秒和 20 秒。在花费 10 秒在 CPU 上运行(不是 "墙上" 时间)后,进程将收到一个信号(在某些系统上为 XCPU),如果该信号未被捕获,则会导致进程终止。如果该信号被捕获,则在另外 10 秒(总共 20 秒)后,进程将被一个不可捕获的信号杀死。

有关详细信息,请参阅 BSD::Resource 和你的系统文档。

如何在 Unix 系统上避免僵尸进程?

使用来自 "perlipc 中的信号" 的收割器代码,在收到 SIGCHLD 时调用 wait(),或者使用 "perlfaq8 中的如何将进程置于后台?" 中描述的双重分叉技术。

如何使用 SQL 数据库?

DBI 模块为大多数数据库服务器和类型提供了一个抽象接口,包括 Oracle、DB2、Sybase、mysql、Postgresql、ODBC 和平面文件。DBI 模块通过数据库驱动程序或 DBD 访问每个数据库类型。你可以在 CPAN 上看到可用驱动程序的完整列表:http://www.cpan.org/modules/by-module/DBD/。你可以在 https://dbi.perl5.cn/ 上阅读有关 DBI 的更多信息。

其他模块提供更具体的访问:Win32::ODBCAlzaboiodbc,以及在 CPAN 搜索中找到的其他模块:https://metacpan.org/

如何让 system() 在按下 Ctrl+C 时退出?

你无法做到。你需要模仿 system() 调用(参见 perlipc 获取示例代码),然后为 INT 信号设置一个信号处理程序,将信号传递给子进程。或者你可以检查它。

$rc = system($cmd);
if ($rc & 127) { die "signal death" }

如何非阻塞地打开文件?

如果你幸运地使用的是支持非阻塞读取的系统(大多数类 Unix 系统都支持),你只需要使用 Fcntl 模块中的 O_NDELAYO_NONBLOCK 标志,并结合 sysopen() 函数。

use Fcntl;
sysopen(my $fh, "/foo/somefile", O_WRONLY|O_NDELAY|O_CREAT, 0644)
    or die "can't open /foo/somefile: $!":

如何区分来自 shell 和 Perl 的错误?

(答案由 brian d foy 贡献)

当你运行一个 Perl 脚本时,另一个程序正在为你运行这个脚本,而这个程序可能会输出错误消息。脚本本身也可能会发出警告和错误消息。大多数情况下,你无法区分谁说了什么。

你可能无法修复运行 Perl 的程序,但你可以通过定义自定义的警告和 die 函数来更改 Perl 输出警告的方式。

考虑这个脚本,它有一个你可能不会立即注意到的错误。

#!/usr/locl/bin/perl

print "Hello World\n";

当我从我的 shell(恰好是 bash)运行这个脚本时,我得到了一个错误。这看起来像是 Perl 忘记了它有一个 print() 函数,但我的 shebang 行不是 Perl 的路径,所以 shell 运行了脚本,我得到了错误。

$ ./test
./test: line 3: print: command not found

一个快速而肮脏的修复方法需要一些代码,但这可能是你解决问题所需的全部代码。

#!/usr/bin/perl -w

BEGIN {
    $SIG{__WARN__} = sub{ print STDERR "Perl: ", @_; };
    $SIG{__DIE__}  = sub{ print STDERR "Perl: ", @_; exit 1};
}

$a = 1 + undef;
$x / 0;
__END__

Perl 的消息前面带有 "Perl"。BEGIN 块在编译时工作,因此所有编译错误和警告都带有 "Perl:" 前缀。

Perl: Useless use of division (/) in void context at ./test line 9.
Perl: Name "main::a" used only once: possible typo at ./test line 8.
Perl: Name "main::x" used only once: possible typo at ./test line 9.
Perl: Use of uninitialized value in addition (+) at ./test line 8.
Perl: Use of uninitialized value in division (/) at ./test line 9.
Perl: Illegal division by zero at ./test line 9.
Perl: Illegal division by zero at -e line 3.

如果我没有看到 "Perl:",那么它就不是来自 Perl。

你也可以简单地了解所有 Perl 错误,虽然有些人可能知道所有错误,但你可能不知道。但是,它们都应该在 perldiag 手册页中。如果你在那里找不到错误,它可能不是 Perl 错误。

查找每个消息并不是最简单的方法,所以让 Perl 帮你做这件事。使用 diagnostics pragma,它将 Perl 的正常消息转换为更详细的主题讨论。

use diagnostics;

如果你没有得到一两段扩展的讨论,这可能不是 Perl 的消息。

如何从 CPAN 安装模块?

(由 brian d foy 贡献)

最简单的方法是使用 Perl 附带的 cpan 命令,让一个名为 CPAN 的模块为你完成。你可以给它一个要安装的模块列表

$ cpan IO::Interactive Getopt::Whatever

如果你更喜欢 CPANPLUS,它同样简单

$ cpanp i IO::Interactive Getopt::Whatever

如果你想从当前目录安装一个发行版,你可以告诉 CPAN.pm 安装 .(句点)

$ cpan .

查看这两个命令的文档,了解你可以做些什么。

如果你想尝试自己安装一个发行版,自己解决所有依赖关系,你可以遵循两种可能的构建路径之一。

对于使用 Makefile.PL 的发行版

$ perl Makefile.PL
$ make test install

对于使用 Build.PL 的发行版

$ perl Build.PL
$ ./Build test
$ ./Build install

某些发行版可能需要链接到库或其他第三方代码,它们的构建和安装顺序可能更复杂。检查你可能找到的任何 READMEINSTALL 文件。

requireuse 之间有什么区别?

(由 brian d foy 贡献)

Perl 在运行时运行 require 语句。一旦 Perl 加载、编译并运行文件,它就不会做任何其他事情。use 语句与在编译时运行的 require 相同,但 Perl 还会为加载的包调用 import 方法。这两个是相同的

use MODULE qw(import list);

BEGIN {
    require MODULE;
    MODULE->import(import list);
}

但是,你可以使用显式的空导入列表来抑制 import。这两个仍然发生在编译时

use MODULE ();

BEGIN {
    require MODULE;
}

由于 use 也会调用 import 方法,因此 MODULE 的实际值必须是裸字。也就是说,use 不能按名称加载文件,尽管 require 可以

require "$ENV{HOME}/lib/Foo.pm"; # no @INC searching!

有关更多详细信息,请参阅 perlfunc 中的 use 条目。

如何保留我自己的模块/库目录?

当你构建模块时,告诉 Perl 在哪里安装模块。

如果你想安装供自己使用的模块,最简单的方法可能是 local::lib,你可以从 CPAN 下载它。它为你设置了各种安装设置,并在你的程序中使用相同的设置。

如果你想要更多灵活性,你需要为你的特定情况配置你的 CPAN 客户端。

对于基于 Makefile.PL 的发行版,在生成 Makefile 时使用 INSTALL_BASE 选项

perl Makefile.PL INSTALL_BASE=/mydir/perl

你可以在 CPAN.pm 配置中设置它,以便当你使用 CPAN.pm shell 时,模块会自动安装到你的私有库目录中

% cpan
cpan> o conf makepl_arg INSTALL_BASE=/mydir/perl
cpan> o conf commit

对于基于Build.PL的发布,请使用--install_base选项

perl Build.PL --install_base /mydir/perl

您也可以配置CPAN.pm来自动使用此选项

% cpan
cpan> o conf mbuild_arg "--install_base /mydir/perl"
cpan> o conf commit

INSTALL_BASE告诉这些工具将您的模块放到/mydir/perl/lib/perl5中。有关如何运行新安装的模块的详细信息,请参阅"如何在运行时将目录添加到我的包含路径(@INC)?"

INSTALL_BASE有一个注意事项,因为它与旧版本的ExtUtils::MakeMaker所倡导的PREFIX和LIB设置的行为不同。INSTALL_BASE不支持在同一目录下为多个版本的Perl或不同的体系结构安装模块。您应该考虑是否真的需要这样做,如果需要,请使用旧的PREFIX和LIB设置。有关更多详细信息,请参阅ExtUtils::MakeMaker文档。

如何将我的程序所在的目录添加到模块/库搜索路径?

(由 brian d foy 贡献)

如果您已经知道目录,则可以像添加任何其他目录一样将它添加到@INC中。如果您在编译时知道目录,则可以使用use lib

use lib $directory;

此任务的诀窍是找到目录。在您的脚本执行任何其他操作(例如chdir)之前,您可以使用Cwd模块获取当前工作目录,该模块随Perl一起提供

BEGIN {
    use Cwd;
    our $directory = cwd;
}

use lib $directory;

您可以对$0的值执行类似的操作,该值保存脚本名称。它可能包含相对路径,但rel2abs可以将其转换为绝对路径。一旦您有了

BEGIN {
    use File::Spec::Functions qw(rel2abs);
    use File::Basename qw(dirname);

    my $path   = rel2abs( $0 );
    our $directory = dirname( $path );
}

use lib $directory;

随Perl一起提供的FindBin模块可能有效。它找到当前运行脚本的目录并将其放入$Bin中,然后您可以使用它来构建正确的库路径

use FindBin qw($Bin);

您还可以使用local::lib来完成大部分相同的事情。使用local::lib的设置安装模块,然后在您的程序中使用该模块

use local::lib; # sets up a local lib at ~/perl5

有关更多详细信息,请参阅local::lib文档。

如何在运行时将目录添加到我的包含路径(@INC)?

以下是一些修改包含路径的建议方法,包括环境变量、运行时开关和代码内语句

PERLLIB环境变量
$ export PERLLIB=/path/to/my/dir
$ perl program.pl
PERL5LIB环境变量
$ export PERL5LIB=/path/to/my/dir
$ perl program.pl
perl -Idir命令行标志
$ perl -I/path/to/my/dir program.pl
lib 编译指示
use lib "$ENV{HOME}/myown_perllib";
local::lib 模块
use local::lib;

use local::lib "~/myown_perllib";

模块安装在哪里?

模块是根据情况安装的(如上一节中描述的方法提供),并在操作系统中。所有这些路径都存储在 @INC 中,您可以使用以下单行代码显示它

perl -e 'print join("\n",@INC,"")'

相同的 信息显示在命令输出的末尾

perl -V

要找出模块源代码的位置,请使用

perldoc -l Encode

显示模块的路径。在某些情况下(例如,AutoLoader 模块),此命令将显示指向单独 pod 文件的路径;模块本身应该在同一个目录中,具有 'pm' 文件扩展名。

什么是 socket.ph 以及在哪里可以获取它?

它是一个 Perl 4 风格的文件,用于定义系统网络常量的值。有时它是在安装 Perl 时使用 h2ph 构建的,但有时它没有。现代程序应该使用 use Socket; 代替。

作者和版权

版权所有 (c) 1997-2010 Tom Christiansen、Nathan Torkington 和其他作者(如所述)。保留所有权利。

此文档是免费的;您可以在与 Perl 本身相同的条款下重新分发和/或修改它。

无论其分发方式如何,此文件中的所有代码示例均在此置于公共领域。您被允许并鼓励在您自己的程序中使用此代码,以供娱乐或盈利,您认为合适。在代码中添加一个简单的评论以表示感谢将是礼貌的,但不是必需的。