perlfaq8 - 系統交互
版本 5.20210520
這部分的 Perl FAQ 涵蓋與作業系統互動相關的問題。主題包括進程間通信(IPC)、對用戶界面(鍵盤、屏幕和指示設備)的控制,以及幾乎與數據操作無關的任何其他事項。
閱讀與您的操作系統相關的 Perl 移植的常見問題解答和文檔(例如,perlvms、perlplan9,...)。這些應該包含有關您的 Perl 的細節信息。
變數$^O
(如果您使用English
,則為$OSNAME
)包含您的 Perl 二進制文件所建立的操作系統的名稱(而不是其發行版本號)。
exec()
不返回? (由 brian d foy 貢獻)
exec
函數的作用是將您的進程轉換為另一個命令並永遠不返回。如果這不是您想要的,請勿使用exec
。 :)
如果您想運行外部命令並仍保持 Perl 進程運行,請查看使用管道的open
、fork
或system
。
您如何訪問/控制鍵盤、屏幕和指示設備(“鼠標”)取決於系統。請嘗試以下模塊
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
這些特定情況的一些示例在本部分 Perl FAQ 的其他答案中顯示。
一般來說,您不應該這樣做,因為您不知道接收者是否有顏色感知的顯示設備。如果您知道他們有一個理解顏色的 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 發送 shell 命令要高效得多。它甚至包含對 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";
(此問題與網頁無關。有關此問題的其他 FAQ,請參見不同的 FAQ。)
在 perlfunc 中的 "crypt" 中有一個示例。首先,將終端置於“無回聲”模式,然後按常規方式讀取密碼。您可以使用舊式的 ioctl()
函數,POSIX 終端控制(參見 POSIX 或其文檔中的駱駝書),或呼叫 stty 程序來完成這一點,其可移植性因系統而異。
對於大多數系統,您也可以使用從 CPAN 安裝的 Term::ReadKey 模塊來完成此操作,它更易於使用,並且在理論上更具可移植性。
use Term::ReadKey;
ReadMode('noecho');
my $password = ReadLine(0);
這取決於您的程序運行在哪個操作系統上。在 Unix 的情況下,串行端口將通過 /dev
中的文件訪問;在其他系統上,設備名稱可能不同。所有設備交互的常見問題區域如下
您的系統可能使用鎖檔來控制多個存取。請確保您遵循正確的協定。多個進程從一個設備讀取可能導致不可預測的行為。
如果您預期在設備上進行讀取和寫入操作,您將需要以更新方式打開它(有關詳細信息,請參見perlfunc 中的 "open")。您可能希望使用Fcntl模塊(標準perl發行版的一部分)中的sysopen()
和O_RDWR|O_NDELAY|O_NOCTTY
來打開它,以避免阻塞的風險。有關此方法的更多信息,請參見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 <jwz@netscape.com>
,經過許多咬牙切齒和與sysread
、sysopen
、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::Open2或IPC::Open3,IPC::Run,Parallel::Jobs,Parallel::ForkManager,POE,Proc::Background和Win32::Process。還有許多其他模塊可供使用,因此請檢查這些命名空間以獲取其他選項。
如果您在類Unix系統上,您可以嘗試使用系統調用,在命令的末尾加上&
。
system("cmd &")
您也可以嘗試使用fork
,如perlfunc中所述(盡管這與許多模塊將為您執行的相同)。
主進程和後台進程(“子”進程)都共享相同的STDIN、STDOUT和STDERR文件處理程序。如果兩者同時嘗試訪問它們,可能會發生奇怪的事情。您可能需要為子進程關閉或重新打開這些文件處理程序。您可以使用open
打開管道來解決此問題(參見perlfunc中的“open”),但在某些系統上,這意味著子進程無法生存於父進程之外。
您將不得不捕獲SIGCHLD信號,可能還有SIGPIPE信號。當後台進程完成時,會發送SIGCHLD信號。當您向已關閉其子進程的文件處理程序寫入時,會發送SIGPIPE信號(未處理的SIGPIPE可能會導致您的程序悄悄退出)。這對於system("cmd&")
不是問題。
當子進程結束時,你必須準備好"收割"它。
$SIG{CHLD} = sub { wait };
$SIG{CHLD} = 'IGNORE';
你也可以使用雙重 fork。你立即 wait()
你的第一個子進程,而 init daemon 將在它退出後 wait()
你的孫子進程。
unless ($pid = fork) {
unless (fork) {
exec "what you really wanna do";
die "exec failed!";
}
exit 0;
}
waitpid($pid, 0);
參見 perlipc 中的 "Signals" 以獲得其他示例代碼。殭屍問題在於 system("prog &")
是不是問題。
你實際上不是“捕獲”控制字符。相反,該字符生成一個信號,發送到你終端當前的前景進程組,然後你在你的進程中捕獲它。信號在 perlipc 中的 "Signals" 和 Camel 中的 "Signals" 章節中有文檔記載。
你可以設置 %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";
Perl 5.8 之前的版本在其 C 源代碼中有信號處理程序,它會捕獲信號,並可能運行你在 %SIG
中設置的 Perl 函數。這違反了信號處理的規則,在這個層次上導致 perl 轉儲核心。自 5.8.0 版本以來,perl 在捕獲到信號後會查看 %SIG
,而不是在捕獲信號時查看。此前版本的答案是不正確的。
如果 perl 正確安裝並且你的影子庫正確編寫,則 perlfunc 中描述的 getpw*()
函數在理論上應該提供對影子密碼文件中項目的(只讀)訪問權限。要更改文件,請創建一個新的影子密碼文件(格式因系統而異--請參見 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() 函数提供的 1 秒更精确的时间粒度,最简单的方法是使用文档中所述的 select() 函数。尝试使用 Time::HiRes 和 BSD::Itimer 模块(从 Perl 5.8 开始,Time::HiRes 已经成为标准发布的一部分,可从 CPAN 获取)。
(由 brian d foy 貢獻)
Time::HiRes 模块(作为 Perl 5.8 的标准发布的一部分)使用 gettimeofday() 系统调用来测量时间,该函数返回自纪元以来的微秒数。如果你无法为旧版 Perl 安装 Time::HiRes,并且你使用的是类 Unix 系统,你可能可以直接调用 gettimeofday(2)。参见 perlfunc 中的 "syscall"。
你可以使用 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》中关于 "Signals" 的部分。
如果你只对异常处理感兴趣,请使用许多 CPAN 模块之一来处理异常,例如 Try::Tiny。
如果你想要 atexit() 语法(以及 rmexit()),请尝试从 CPAN 获取的 AtExit 模块。
某些基於 Sys-V 的系統,尤其是 Solaris 2.X,重新定義了一些標準的 socket 常數。由於這些常數在所有架構上都是恆定的,它們通常被硬編碼到 Perl 程式碼中。處理這個問題的正確方法是使用「use Socket」來獲取正確的值。
請注意,即使 SunOS 和 Solaris 是二進制相容的,這些值也是不同的。想一想吧。
在大多數情況下,您可以編寫一個外部模塊來完成這個工作--參見「我可以在哪裡學習如何將 C 與 Perl 連接?[h2xs, xsubpp]」的答案。但是,如果該函數是一個系統呼叫,而且您的系統支持 syscall()
,則可以使用 syscall
函數(在 perlfunc 中有文件)。
請記住檢查您發行版附帶的模塊以及 CPAN--可能有人已經編寫了一個模塊來完成這項工作。在 Windows 上,請嘗試 Win32::API。在 Mac 上,請嘗試 Mac::Carbon。如果沒有模塊與 C 函數有介面,您可以在 Perl 源代碼中內聯一些 C 代碼,使用 Inline::C。
從歷史上看,這些檔案是由標準 Perl 發行版的 h2ph 工具生成的。此程序將 C 標頭檔中的 cpp(1)
指令轉換為包含子例程定義的文件,例如 SYS_getitimer()
,您可以將其用作函數的參數。它並不完美,但通常可以完成大部分工作。像 errno.h、syscall.h 和 socket.h 這樣的簡單檔案是可以的,但是像 ioctl.h 這樣的難檔幾乎總是需要手動編輯。這是安裝 *.ph 檔案的方法。
1. Become the super-user
2. cd /usr/include
3. h2ph *.h */*.h
如果您的系統支持動態加載,出於可移植性和健全性的考慮,您可能應該使用標準 Perl 發行版中的 h2xs(也是一部分)。此工具將 C 標頭檔轉換為 Perl 擴展。參見 perlxstut 以了解如何開始使用 h2xs。
如果您的系統不支援動態加載,您仍然應該使用 h2xs。更多信息請參見 perlxstut 和 ExtUtils::MakeMaker(簡而言之,只需使用 make perl 而不是純粹的 make 來重新構建帶有新靜態擴展的 perl)。
一些操作系統在內核中存在使 setuid 腳本本質上不安全的 bug。Perl 為您提供了一些選項(在 perlsec 中描述)來解決這些系統的問題。
IPC::Open2 模組(標準 perl 發行版的一部分)是一種易於使用的方法,它內部使用 pipe()
、fork()
和 exec()
來完成工作。但請確保閱讀其文檔中的死鎖警告(參見 IPC::Open2)。參見 "與另一個進程的雙向通信" 中的 perlipc 和 "與自己的雙向通信" 中的 perlipc。
您也可以使用 IPC::Open3 模組(標準 perl 發行版的一部分),但請注意它與 IPC::Open2 有不同的參數順序(參見 IPC::Open3)。
您將 system()
和反引號(``)的目的混淆了。 system()
執行命令並返回退出狀態信息(作為 16 位值:低 7 位是進程死亡的信號(如果有的話),高 8 位是實際的退出值)。反引號(``)運行命令並返回其發送到 STDOUT 的內容。
my $exit_status = system("mail-users");
my $output_string = `ls`;
運行外部命令有三種基本方法
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::Open3中的open3()
函式。 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打開為STDOUT的副本,並避免調用shell來進行重定向。這不起作用
open(STDERR, ">&STDOUT");
$alloutput = `cmd args`; # stderr still escapes
這失敗了,因為在open()
時,STDERR會轉到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
要分開讀取命令的標準輸出和標準錯誤,最簡單的方法是將它們分別重定向到文件,然後在程序完成後從這些文件中讀取。
system("program args 1>program.stdout 2>program.stderr");
在所有這些示例中,順序都很重要。這是因為 shell 嚴格按照從左到右的順序處理文件描述符重定向。
system("prog args 1>tmpfile 2>&1");
system("prog args 2>&1 1>tmpfile");
第一個命令將標準輸出和標準錯誤都發送到臨時文件。第二個命令僅將舊的標準輸出發送到那裡,而舊的標準錯誤顯示在舊的標準輸出上。
如果 piped open() 的第二個參數包含 shell 元字符,perl 會 fork(),然後 exec() 一個 shell 來解碼元字符,最終運行所需的程序。如果無法運行該程序,則會將消息發送給 shell,而不是 Perl。您的 Perl 程序只能發現 shell 本身是否能夠成功啟動。您仍然可以捕獲 shell 的 STDERR 並檢查其是否有錯誤消息。另請參閱本文檔中的其他地方的 "How can I capture STDERR from an external command?",或使用 IPC::Open3 模塊。
如果 open() 的參數中沒有 shell 元字符,Perl 將直接運行該命令,而不使用 shell,並且可以正確報告命令是否已啟動。
嚴格來說,沒有問題。從風格上講,這不是編寫可維護代碼的好方法。Perl 有幾個運行外部命令的運算符。反引號是其中之一;它們收集命令的輸出供您的程序使用。系統函數是另一個;它不這樣做。
在您的程序中編寫反引號傳達給代碼讀者的明確信息,即您希望收集命令的輸出。為什麼要傳達一個不真實的明確信息呢?
考慮這條線
`cat /etc/termcap`;
您忘記檢查 $? 以查看程序是否正確運行。即使您編寫了
print `cat /etc/termcap`;
這段代碼可以且可能應該被編寫為
system("cat /etc/termcap") == 0
or die "cat program failed!";
它將回顯 cat 命令的輸出,因為它是在生成時,而不是等到程序完成後才將其打印出來。它還檢查返回值。
系統還可以直接控制是否允許 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-style API。
這只會發生在你的 perl 編譯為使用 stdio 而不是 perlio 的情況下,這是默認值。一些(也許全部?)stdio 設置了錯誤和 eof 標誌,你可能需要清除它們。 POSIX 模塊定義了你可以使用的 clearerr()
方法。這是技術上正確的方法。這裡還有一些不太可靠的解決方法
嘗試保留 seekpointer 並轉到那裡,像這樣
my $where = tell($log_fh);
seek($log_fh, $where, 0);
如果這樣做不起作用,嘗試轉到文件的不同部分,然後再返回。
如果這樣做不起作用,嘗試轉到文件的不同部分,讀取一些東西,然後再返回。
如果這樣做不起作用,放棄你的 stdio 包,使用 sysread。
學習 Perl 並重新編寫它。認真地說,沒有簡單的轉換器。在 shell 中難以做的事情在 Perl 中很容易做,這種笨拙正是使 shell->perl 轉換器幾乎不可能編寫的原因。通過重新編寫它,你會思考你真正想要做的事情,並且希望你能擺脫 shell 的管道數據流範式,儘管對某些問題很方便,但會導致許多效率低下的情況。
您可以嘗試使用Net::FTP、TCP::Client和Net::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;
很久以前,有一個名為chat2.pl的庫(屬於標準perl發行版的一部分),但它從未真正完成。如果您在某個地方找到了它,不要使用它。如今,您最好查看CPAN提供的Expect模組,該模組還需要CPAN的其他兩個模組:IO::Pty和IO::Stty。
首先,請注意,如果您出於安全原因進行此操作(例如避免人們看到密碼),則應重寫程序,使關鍵信息永遠不作為參數給出。隱藏參數不會使您的程序完全安全。
要實際更改可見的命令行,可以根據perlvar中的文件將值分配給變量$0。但這在所有操作系統上都不起作用。像sendmail這樣的守護程序將它們的狀態放在那裡,例如
$0 = "orcus [accepting connections]";
嚴格意義上來說,這是不可能的——腳本作為與啟動它的shell不同的進程運行。對進程的更改不會反映在其父進程中——只會反映在更改後創建的任何子進程中。您可以使用shell魔法來進行偽造,通過在您的shell中eval()
腳本的輸出;詳情請查看comp.unix.questions FAQ。
假設您的系統支持這些功能,只需向進程發送適當的信號(參見perlfunc
中的kill
)。通常會先發送TERM信號,等待一小段時間,然後發送KILL信號以結束它。
如果所謂的daemon進程是指與tty(終端)分離的進程,則以下過程據報告在大多數類Unix系統上都能夠正常工作。非Unix用戶應檢查其Your_OS::Process模塊以尋找其他解決方案。
打開/dev/tty並對其使用TIOCNOTTY ioctl。詳情請參見tty(1)。或者更好的做法是,您可以直接使用POSIX::setsid()
函數,這樣您就不必擔心進程組。
切換目錄到/
重新打開STDIN、STDOUT和STDERR,使它們不再連接到舊的tty。
像這樣將自己後台化
fork && exit;
Proc::Daemon模塊,可從CPAN獲取,提供了一個函數來執行這些操作。
(由 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()
函數,可能需要與信號處理器一起使用,如
在部分 Windows 版本中並未實作 alarm()
函式。請查閱您所使用的 Perl 版本的文件。
(由 Xho 貢獻)
使用 CPAN 的 BSD::Resource 模組。舉例來說
use BSD::Resource;
setrlimit(RLIMIT_CPU,10,20) or die $!;
這會將軟體與硬體限制分別設定為 10 和 20 秒。在 CPU 執行 10 秒後(非「牆壁」時間),該進程將會收到一個信號(在某些系統上為 XCPU),如果未被捕獲,該進程將會終止。如果該信號被捕獲,則在再過 10 秒(總共 20 秒)後,該進程將會被以非可捕獲信號殺死。
詳細資訊請參考 BSD::Resource 和您系統的文件。
使用來自 "Signals" in perlipc 的 reaper 代碼在收到 SIGCHLD 時調用 wait()
,否則使用在 perlfaq8 中描述的「如何在背景中啟動進程?」 的雙重 fork 技術。
DBI 模組提供對大多數資料庫服務器和類型的抽象介面,包括 Oracle、DB2、Sybase、mysql、Postgresql、ODBC 和平面文件。DBI 模組通過資料庫驅動程序或 DBD 訪問每種資料庫類型。您可以在 CPAN 上看到可用驅動程序的完整列表:http://www.cpan.org/modules/by-module/DBD/ 。您可以在 http://dbi.perl.org/ 上閱讀有關 DBI 的更多信息。
其他模組提供更具體的訪問方式:Win32::ODBC、Alzabo、iodbc
和其他在 CPAN 搜尋中找到的模組:https://metacpan.org/ 。
您無法這樣做。您需要模仿 system()
調用(請參閱 perlipc 以獲得示例代碼),然後擁有一個處理 INT 信號的信號處理程序,將該信號傳遞給子進程。或者您可以檢查它。
$rc = system($cmd);
if ($rc & 127) { die "signal death" }
如果您很幸運地在一個支持非阻塞讀取的系統上(大多數類Unix系統都支持),您只需要在使用Fcntl
模組的sysopen()
函數時,同時使用O_NDELAY
或O_NONBLOCK
標誌。
use Fcntl;
sysopen(my $fh, "/foo/somefile", O_WRONLY|O_NDELAY|O_CREAT, 0644)
or die "can't open /foo/somefile: $!":
(由brian d foy提供答案)
當您執行Perl腳本時,還有其他東西為您執行該腳本,而該其他東西可能會輸出錯誤消息。該腳本可能會發出自己的警告和錯誤消息。大多數情況下,您無法分辨誰說了什麼。
您可能無法修復運行perl的事物,但您可以通過定義自定義的警告和死函數來更改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為您完成。使用診斷pragma將perl的正常消息轉換為有關該主題的更長的討論。
use diagnostics;
如果您沒有得到一兩段擴展討論,那可能不是perl的消息。
(由 brian d foy 貢獻)
最簡單的方法是使用也名為CPAN的模塊讓它為您完成,方法是使用Perl提供的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
某些分發可能需要連結到庫或其他第三方代碼,它們的構建和安裝過程可能更加複雜。請檢查你可能找到的任何 README 或 INSTALL 文件。
(由 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
),您可以使用 Perl 自帶的 Cwd
模塊獲取當前工作目錄。
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 文件。
以下是修改包含路徑的建議方法,包括環境變量、運行時開關和代碼語句。
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
pragmause lib "$ENV{HOME}/myown_perllib";
use local::lib;
use local::lib "~/myown_perllib";
模組安裝位置為案例依賴(如前一節所述的方法),以及作業系統。 所有這些路徑都存儲在 @INC 中,您可以使用以下一行程式碼顯示:
perl -e 'print join("\n",@INC,"")'
相同的資訊會顯示在命令輸出的末尾
perl -V
要查找模組源代碼的位置,請使用
perldoc -l Encode
顯示模組的路徑。 在某些情況下(例如,AutoLoader 模組),此命令將顯示到單獨的 pod 文件的路徑; 模組本身應該位於相同的目錄中,並具有 'pm' 文件擴展名。
它是一個 Perl 4 風格的文件,定義了系統網絡常量的值。 有時會在安裝 Perl 時使用 h2ph 構建,但其他時候不會。 現代程序應該改用 use Socket;
。
版權(c)1997-2010年Tom Christiansen、Nathan Torkington和其他作者。 保留所有權利。
本文檔是免費的; 您可以重新分發它和/或修改它,條件是遵守與 Perl 本身相同的條款。
無論其分發方式如何,本文件中的所有代碼示例均被放入公共領域。 您可以根據自己的意願在自己的程序中使用此代碼進行娛樂或營利。 代碼中的一個簡單注釋給予學分是有禮貌的,但不是必需的。