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


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM