open函數除了> >> <
這三種最基本的文件句柄模式,還支持更豐富的操作模式,例如管道。其實bash shell支持的重定向模式,perl都支持,即使是2>&1
這種高級重定向模式,perl也有對應的模式。
打開管道文件句柄
perl程序內部也支持管道,以便和操作系統進行交互。例如,將perl的輸出在程序內部就輸出給操作系統的命令,或者將操作系統的命令執行結果輸出給perl程序內部。所以,perl有2種管道模式:句柄到管道、管道到句柄。
例如,將perl print語句的輸出,交給操作系統的cat -n
命令來輸出行號。也就是說,下面的perl程序和cat -n
命令的效果是一樣的。
#!/usr/bin/perl
open LOG,"| cat -n"
or die "Can't open file: $!";
while(<LOG>){
print $_;
}
再例如,將操作系統命令的執行結果通過管道交給perl文件句柄:
#!/usr/bin/perl
open LOG,"cat -n test.log |"
or die "Can't open file: $!";
while(<LOG>){
print "from pipe: $_";
}
雖然只有兩種管道模式,但有3種寫法:
- 管道輸出到文件句柄模式:
-|
- 文件句柄輸出到管道模式:
|-
|
寫在左邊,表示句柄到管道,等價於|-
,|
寫在右邊,等價於管道到句柄,等價於-|
,可以認為"-"代表的就是外部命令
上面第三點|
的寫法見上面的例子便可理解。而|-
和-|
是作為open函數的模式參數的,以下幾種寫法是等價的:
open LOG, "|tr '[a-z]' '[A-Z]'";
open LOG, "|-", "tr '[a-z]' '[A-Z]'";
open LOG, "|-", "tr", '[a-z]', '[A-Z]';
open LOG, "cat -n '$file'|";
open LOG, "-|", "cat -n '$file'";
open LOG, "-|", "cat", "-n", $file;
而且,管道還可以繼續傳遞給管道:
open LOG, "|tr '[a-z]' '[A-Z]' | cat -n";
但是涉及到兩個管道的時候,輸出到終端屏幕上時可能不太合意:
[root@xuexi perlapp]# perl 15.plx test.log
[root@xuexi perlapp]# 1 A
2 B
3 C
如何讓輸出不附加在shell提示符后,我暫時也不知道如何做。
但是,輸出到文件中不會出現這樣的問題:
open LOG, "|tr '[a-z]' '[A-Z]' | cat -n >test2.log";
[root@xuexi perlapp]# perl 15.plx test.log
[root@xuexi perlapp]# cat test2.log
1 A
2 B
3 C
更多關於open和管道的解釋參見:Perl進程間通信。
以讀寫模式打開
默認情況下:
- 以
>
模式打開文件句柄時,會先截斷文件,也就是說無法從此文件句柄關聯的文件中讀取原有數據,且還會清空原有數據 - 以
>>
模式打開文件句柄時,首先會將指針指向文件的末尾以便追加數據,但無法讀取該文件句柄對應的文件數據
如何以"既可寫又可讀"的模式打開文件句柄?在Perl中可以在模式前使用+
符號來實現。
結合"+"的模式有3種,都用來實現讀寫更新操作。它們的意義如下:
+<
:read-update,如open FH, "+<$file"
,可以提供讀寫行為。如果文件不存在,則open失敗(以read為主,寫為輔),如果文件存在,則文件內容保留,但IO的指針放在文件開頭,也就是說無論讀寫操作,都從開頭開始,寫操作會從指針位置開始覆蓋同字節數的數據。+>
:write-update,如open FH, "+>$file"
,可以提供讀寫行為。如果文件不存在,則創建文件(以write為主,read為輔)。如果文件存在,則截斷整個文件,因此這種方式是先將文件清空然后寫數據,再從中讀數據。+>>
:append-update,如open FH, "+>>$file"
,提供讀寫行為。如果文件不存在,則創建(以append為主,read為輔),如果文件存在,則將IO指針放到文件尾部。一般情況下,每一次讀操作之前都需要通過seek
將指針移動到文件的某個位置,而寫操作則總是追加到文件尾部並自動移動指針到結尾。
一般來說,要同時提供讀寫操作,+<
是最可能需要的模式。
1.打開可供讀、寫、更新的文件句柄,但不截斷文件
open LOG,"+<","/tmp/test.log"
or die "Couldn't open file: $!";
如下面的例子,say語句會將數據寫入到test.log的尾部,因為遍歷完test.log后,指針在文件的尾部。
#!/usr/bin/perl
use 5.010;
open LOG,"+<","test.log"
or die "Couldn't open file: $!";
while(<LOG>){
print $_;
}
say LOG "from hello world";
但注意,如果將上面的say語句放進while循環,則會出現讀、寫錯亂的問題,因為+<
模式打開文件句柄時IO指針默認在文件的開頭:
#!/usr/bin/perl
use 5.010;
open LOG,"+<","test.log"
or die "Couldn't open file: $!";
while(<LOG>){
print $_;
say LOG "from hello world";
}
分析下這個錯亂:當讀取了第一行后,放置好指針位置,然后賦值給$_
並被print輸出,然后再寫入"from hello world",寫入的位置是指針的后面,它會直接更新后面對應數量的字符數。數一數"from hello world"的字符數量和替換掉的字符數量,會發現正好相等。
2.打開可供讀、寫、更新的文件句柄,但首先截斷文件
open LOG,"+>","/tmp/test.log"
or die "Couldn't open file: $!";
因為首先會截斷文件,無法直接去讀取內容。所以,這種操作模式,需要首先向文件中寫入數據,再去讀取數據。
#!/usr/bin/perl
use 5.010;
open LOG,"+>","test.log"
or die "Couldn't open file: $!";
say LOG "from hello world1";
say LOG "from hello world2";
say LOG "from hello world3";
while(<LOG>){
say $_;
}
3.打開可供讀、追加寫的文件句柄。它不會截斷文件。
open LOG,"+>>","/tmp/test.log"
or die "Couldn't open file: $!";
因為追加寫模式會將指針放置在文件尾部,如果不將指針移動到文件的某個位置(可通過seek
來移動),將無法讀出數據來。
例如:
#!/usr/bin/env perl
use strict;
use warnings;
use 5.010;
open LOG,"+>>","test.log"
or die "Couldn't open file: $!";
say LOG "from hello world1";
say LOG "from hello world2";
say LOG "from hello world3";
while(<LOG>){
print "First: ", $_; # 啥也不輸出
}
seek(LOG, 0, 0); # 將讀指針移動到文件開頭
while(<LOG>){
print "Second: ", $_; # 正常輸出
}
open打開STDOUT和STDIN
如果想要打開標准輸入、標准輸出,那么可以使用二參數格式的open,並將"-"指定為文件名。例如:
#
open LOG, "-"; # 打開標准輸入
open LOG, "<-"; # 打開標准輸入
open LOG, ">-"; # 打開標准輸出
沒有類似的直接打開標准錯誤輸出的方式。如果有一個文件名就是"-",這時想要打開這個文件而不是標准輸入或標准輸出,那么需要將"-"文件名作為open的第三個參數。
open LOG, "<", "-";
創建臨時文件
如果將open()函數打開文件句柄時的文件名指定為undef,則表示創建一個匿名文件句柄,即臨時文件。這個臨時文件將創建在/tmp目錄下,創建完成后將立即被刪除,但是卻一直持有並打開這個文件句柄直到文件句柄關閉。這樣,這個文件就成了看不到卻仍被進程占用的臨時文件。
什么時候才能用上打開就立即刪除的臨時文件?只讀或只寫的臨時文件都是沒有意義的,只有同時能讀寫的文件句柄才是有意義的,所以open的模式需要指定為+<
或+>
。顯然,+<
是更為通用的讀、寫模式。
例如:
#!/usr/bin/perl
use strict;
use warnings;
# 創建臨時文件
open my $tmp_file, '+<', undef or die "open filed: $!";
# 設置自動flush
select $tmp_file; $| = 1;;
# 這個臨時文件已經被刪除了
system("lsof -n -p $$ | grep 'deleted'");
# 寫入一點數據
say {$tmp_file} "Hello World1";
say {$tmp_file} "Hello World2";
say {$tmp_file} "Hello World3";
say {$tmp_file} "Hello World4";
# 指針移動到臨時文件的頭部來讀取數據
seek($tmp_file, 0, 0);
select STDOUT;
while(<$tmp_file>){
print "Reading from tmpfile: $_";
}
執行結果:
perl 22685 root 3u REG 0,2 0 108086391056997277 /tmp/PerlIO_JHnTx1 (deleted)
Reading from tmpfile: Hello World1
Reading from tmpfile: Hello World2
Reading from tmpfile: Hello World3
Reading from tmpfile: Hello World4
內存文件
如果將open()函數打開文件句柄時的文件名參數指定為一個標量變量(的引用,即下面示例中標量前加上了反斜線),也就是不再讀寫具體的文件,而是讀寫內存中的變量,這樣就實現了一個內存IO的模式。
#!/usr/bin/perl
$text = "Hello World1\nHello World2\n";
# 打開內存文件以便讀取操作
open MEMFILE, "<", \$text or die "open failed: $!";
print scalar <MEMFILE>;
# 提供內存文件以供寫入操作
$log = ""
open MEMWRITE, ">", \$log;
pritn MEMWRITE "abcdefg\n";
pritn MEMWRITE "ABCDEFG\n";
print $log;
如果內存文件操作的是STDOUT和STDERR這兩個特殊的文件句柄,如果需要重新打開它們,一定要先關閉它們再重新打開,因為內存文件不依賴於文件描述符,再次打開文件句柄不會覆蓋文件句柄。例如:
close STDOUT;
open(STDOUT, ">", \$variable)
or die "Can't open STDOUT: $!";
perl的高級重定向
在shell中可以通過>&
和<&
實現文件描述符的復制(duplicate)從而實現更高級的重定向。在perl中也同樣能實現,符號也一樣,只不過復制對象是文件句柄。
例如:
open LOG,">&STDOUT"
表示將寫入LOG文件句柄的數據重定向到STDOUT中。
shell中很常用的一個符號是>&FILENAME
或>FILENAME 2>&1
,它們都表示標准錯誤和標准輸出都輸出到FILENAME中。在perl中實現這種功能的方式為:(注意dup目標使用\*
的方式,且不加引號)
open LOG,">","/dev/null" or die "Can't open filehandle: $!";
open STDOUT,">&",\*LOG or die "Can't dup LOG:$!";
open STDERR,">&",\*STDOUT or die "Can't dup STDOUT: $!";
或者簡寫一下:
open STDOUT,">","/dev/null" or die "Can't dup LOG:$!";
open STDERR,">&",\*STDOUT or die "Can't dup STDOUT: $!";
測試下:
use 5.010;
open LOG,">>","/tmp/test.log" or die "Can't open filehandle: $!";
open STDOUT,">&",\*LOG or die "Can't dup LOG: $!";
open STDERR,">&",\*STDOUT or die "Can't dup STDOUT: $!";
say "hello world stdout default";
say STDOUT "hello world stdout";
say STDERR "hello world stderr";
會發現所有到STDOUT和STDERR的內容都追加到/tmp/test.log文件中。
如果在同一perl程序中,STDOUT和STDERR有多個輸出方向,那么dup這兩個文件句柄之前,需要先將它們保存起來。需要的時候再還原回來:
# 保存STDOUT和STDERR到$oldout和OLDERR
open(my $oldout, ">&STDOUT") or die "Can't dup STDOUT: $!";
open(OLDERR, ">&", \*STDERR) or die "Can't dup STDERR: $!";
# 實現標准錯誤、標准輸出都重定向到foo.out的功能,即"&>foo.out"
open(STDOUT, '>', "foo.out") or die "Can't redirect STDOUT: $!";
open(STDERR, ">&STDOUT") or die "Can't dup STDOUT: $!";
# 還原回STDOUT和STDERR
open(STDOUT, ">&", $oldout) or die "Can't dup \$oldout: $!";
open(STDERR, ">&OLDERR") or die "Can't dup OLDERR: $!";
因為這種高級重定向用的很少,所以不多做解釋。如需理解,可參考我的shell關於高級重定向的文章:徹底搞懂shell的高級I/O重定向,或者直接參考Perl的高級重定向文章:Perl IO:IO重定向。