Perl的IO操做(2):更多文件句柄模式

open函數除了> >> <這三種最基本的文件句柄模式,還支持更豐富的操做模式,例如管道。其實bash shell支持的重定向模式,perl都支持,即便是2>&1這種高級重定向模式,perl也有對應的模式。html

打開管道文件句柄

perl程序內部也支持管道,以便和操做系統進行交互。例如,將perl的輸出在程序內部就輸出給操做系統的命令,或者將操做系統的命令執行結果輸出給perl程序內部。因此,perl有2種管道模式:句柄到管道、管道到句柄。shell

例如,將perl print語句的輸出,交給操做系統的cat -n命令來輸出行號。也就是說,下面的perl程序和cat -n命令的效果是同樣的。bash

#!/usr/bin/perl

open LOG,"| cat -n"
    or die "Can't open file: $!";

while(<LOG>){
    print $_;
}

再例如,將操做系統命令的執行結果經過管道交給perl文件句柄:app

#!/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";

可是涉及到兩個管道的時候,輸出到終端屏幕上時可能不太合意:scala

[root@xuexi perlapp]# perl 15.plx test.log 
[root@xuexi perlapp]#        1  A
       2  B
       3  C

如何讓輸出不附加在shell提示符後,我暫時也不知道如何作。指針

可是,輸出到文件中不會出現這樣的問題:code

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重定向

相關文章
相關標籤/搜索