2009-08-05 190 views
4

我有幾個bash腳本運行,但是他們可能需要幾個小時才能完成,在這段時間,他們會發出下載速度,ETA和類似的信息。我需要在perl中捕獲這些信息,但是我遇到了一個問題,我無法逐行讀取輸出行(除非我錯過了某些內容)。如何在Perl中實時讀取外部命令的輸出?

任何幫助解決這個問題?

編輯:解釋這個更好一點我運行幾個bash腳本沿彼此,我希望使用gtk與Perl產生方便的進度條。 目前,我爲每個希望運行的bash腳本運行2個線程,一個用於更新圖形信息的主線程。它看起來像這樣(儘可能多地減少):

my $command1 = threads->create(\&runCmd, './bash1', \@out1); 
    my $controll1 = threads->create(\&monitor, $command1, \@out1); 
    my $command1 = threads->create(\&runCmd, 'bash2', \@out2); 
    my $controll2 = threads->create(\&monitor, $command2, \@out2); 

    sub runCmd{ 
    my $cmd = shift; 
    my @bso = shift; 
    @bso = `$cmd` 
    } 
    sub monitor{ 
    my $thrd = shift; 
    my @bso = shift; 
    my $line; 
    while($thrd->is_running()){ 
     while($line = shift(@bso)){ 
     ## I check the line and do things with it here 
     } 
     ## update anything the script doesn't tell me here. 
     sleep 1;# don't cripple the system polling data. 
    } 
    ## thread quit, so we remove the status bar and check if another script is in the queue, I'm omitting this here. 
    } 
+1

你應該真的使用一個適當的事件循環,如POE,而不是線程。使用POE :: Wheel :: Run比使用手動滾動的幾乎事件循環獲得更好的成功。 (我會推薦AnyEvent :: Subprocess,但它正在進行一次重大的重構,並不會立即解決你的問題。) – jrockway 2009-08-06 16:12:33

回答

7

而是線程,和``,使用:

open my $fh, '-|', 'some_program --with-options'; 

這樣打開幾個文件句柄(多達許多程序需要運行),然後使用IO::Select向他們查詢數據。

簡單的例子。

假設我有shell腳本,看起來像這樣:

=> cat test.sh 
#!/bin/bash 
for i in $(seq 1 5) 
do 
    sleep 1 
    echo "from $$ : $(date)" 
done 

它的輸出可能是這樣的:

 
=> ./test.sh 
from 26513 : Fri Aug 7 08:48:06 CEST 2009 
from 26513 : Fri Aug 7 08:48:07 CEST 2009 
from 26513 : Fri Aug 7 08:48:08 CEST 2009 
from 26513 : Fri Aug 7 08:48:09 CEST 2009 
from 26513 : Fri Aug 7 08:48:10 CEST 2009 

現在,讓我們寫一個multi-test.pl

#!/usr/bin/perl -w 
use strict; 
use IO::Select; 

my $s = IO::Select->new(); 

for (1..2) { 
    open my $fh, '-|', './test.sh'; 
    $s->add($fh); 
} 

while (my @readers = $s->can_read()) { 
    for my $fh (@readers) { 
     if (eof $fh) { 
      $s->remove($fh); 
      next; 
     } 
     my $l = <$fh>; 
     print $l; 
    } 
} 

正如你所看到的,沒有叉子,沒有線程。這是它如何工作的:

 
=> time ./multi-test.pl 
from 28596 : Fri Aug 7 09:05:54 CEST 2009 
from 28599 : Fri Aug 7 09:05:54 CEST 2009 
from 28596 : Fri Aug 7 09:05:55 CEST 2009 
from 28599 : Fri Aug 7 09:05:55 CEST 2009 
from 28596 : Fri Aug 7 09:05:56 CEST 2009 
from 28599 : Fri Aug 7 09:05:56 CEST 2009 
from 28596 : Fri Aug 7 09:05:57 CEST 2009 
from 28599 : Fri Aug 7 09:05:57 CEST 2009 
from 28596 : Fri Aug 7 09:05:58 CEST 2009 
from 28599 : Fri Aug 7 09:05:58 CEST 2009 

real 0m5.128s 
user 0m0.060s 
sys  0m0.076s 
+0

迄今爲止,這看起來是最清潔的解決方案,非常感謝。 它不應該花費太多的工作,我目前(哈克和不太工作)的代碼完美工作,因爲你已經提供。 再次感謝。 – scragar 2009-08-07 11:40:54

1

是的,你可以。

while (<STDIN>) { print "Line: $_"; } 

問題是,有些應用程序不會逐行輸出信息,而是更新一行直到它們完成。是你的情況嗎?

+0

這些行不是來自標準的,但是我可以用open打開一行代碼命令,這樣就可以工作。我的問題是,當沒有給出任何輸入時,我希望能夠做一些事情,而不是等待輸入一行(這是循環的作用)。 目前我正在運行一個非常複雜的線程組合來實現這一點(我會更新問題來顯示這一點)。 – scragar 2009-08-05 23:27:08

+1

除非您在Windows上運行腳本,否則可以使用select來測試文件描述符中是否有可用的數據。大致是這樣的: while(1){ my $ rin =''; vec($ rin,fileno(STDIN),1)= 1; 我的($ nfound,$ timeleft)= select($ rin,undef,undef,0); if($ nfound){$ data; print「Got data!\ n」; sysread STDIN,$ data,1024; print「DATA:$ data \ n」; } else { print「wait ... \ n」; sleep(1); } } – 2009-08-06 01:24:04

3

反向引用和qx //運算符都會阻塞,直到子進程結束。您需要在管道上打開bash腳本。如果你需要它們是非阻塞的,打開它們作爲文件句柄,必要時使用open2或open3,然後把句柄放入select()並等待它們變得可讀。

我剛碰到類似的問題 - 我有一個運行時間很長的過程(一個可以運行數週的服務),我用qx打開//。問題在於這個程序的輸出最終超出了內存限制(在我的架構上大約爲2.5G)。我通過在管道上打開子命令來解決它,然後只保存最後1000行輸出。在這樣做的時候,我注意到qx //表單只在命令完成後纔打印輸出,但是管道表單能夠在發生輸出時打印輸出。

我沒有代碼方便,但如果你可以等到明天,我會發布我做的。

1

這裏是用於顯示進度條的GTK2代碼。

#!/usr/bin/perl 
use strict; 
use warnings; 

use Glib qw/TRUE FALSE/; 
use Gtk2 '-init'; 

my $window = Gtk2::Window->new('toplevel'); 
$window->set_resizable(TRUE); 
$window->set_title("command runner"); 

my $vbox = Gtk2::VBox->new(FALSE, 5); 
$vbox->set_border_width(10); 
$window->add($vbox); 
$vbox->show; 

# Create a centering alignment object; 
my $align = Gtk2::Alignment->new(0.5, 0.5, 0, 0); 
$vbox->pack_start($align, FALSE, FALSE, 5); 
$align->show; 

# Create the Gtk2::ProgressBar and attach it to the window reference. 
my $pbar = Gtk2::ProgressBar->new; 
$window->{pbar} = $pbar; 
$align->add($pbar); 
$pbar->show; 

# Add a button to exit the program. 
my $runbutton = Gtk2::Button->new("Run"); 
$runbutton->signal_connect_swapped(clicked => \&runCommands, $window); 
$vbox->pack_start($runbutton, FALSE, FALSE, 0); 

# This makes it so the button is the default. 
$runbutton->can_default(TRUE); 

# This grabs this button to be the default button. Simply hitting the "Enter" 
# key will cause this button to activate. 
$runbutton->grab_default; 
$runbutton->show; 

# Add a button to exit the program. 
my $closebutton = Gtk2::Button->new("Close"); 
$closebutton->signal_connect_swapped(clicked => sub { $_[0]->destroy;Gtk2->main_quit; }, $window); 
$vbox->pack_start($closebutton, FALSE, FALSE, 0); 

$closebutton->show; 

$window->show; 

Gtk2->main; 

sub pbar_increment { 
    my ($pbar, $amount) = @_; 

    # Calculate the value of the progress bar using the 
    # value range set in the adjustment object 
    my $new_val = $pbar->get_fraction() + $amount; 

    $new_val = 0.0 if $new_val > 1.0; 

    # Set the new value 
    $pbar->set_fraction($new_val); 
} 

sub runCommands { 
     use IO::Select; 

     my $s = IO::Select->new(); 

     for (1..2) { 
      open my $fh, '-|', './test.sh'; 
      $s->add($fh); 
     } 

     while (my @readers = $s->can_read()) { 
      for my $fh (@readers) { 
       if (eof $fh) { 
        $s->remove($fh); 
        next; 
       } 
       my $l = <$fh>; 
       print $l; 
       pbar_increment($pbar, .25) if $l =~ /output/; 
      } 
     } 
    } 

看到the perl GTK2 docs更多信息

+0

哦。我的。這是過度殺傷的定義。 – 2009-08-07 06:45:54

+0

生活和學習......關於SO的更好的事情之一是我不是這裏最聰明的人。 – 2009-08-08 21:29:04

1

我用這個子程序和方法登錄我的外部命令。這就是所謂的像這樣:

open($logFileHandle, "mylogfile.log"); 

logProcess($logFileHandle, "ls -lsaF", 1, 0); #any system command works 

close($logFileHandle); 

和這裏的子程序:

#****************************************************************************** 
# Sub-routine: logProcess() 
#  Author: Ron Savage 
#  Date: 10/31/2006 
# 
# Description: 
# This sub-routine runs the command sent to it and writes all the output from 
# the process to the log. 
#****************************************************************************** 
sub logProcess 
    { 
    my $results; 

    my ($logFileHandle, $cmd, $print_flag, $no_time_flag) = @_; 
    my $logMsg; 
    my $debug = 0; 

    if ($debug) { logMsg($logFileHandle,"Opening command: [$cmd]", $print_flag, $no_time_flag); } 
    if (open($results, "$cmd |")) 
     { 
     while (<$results>) 
     { 
     chomp; 
     if ($debug) { logMsg($logFileHandle,"Reading from command: [$_]", $print_flag, $no_time_flag); } 
     logMsg($logFileHandle, $_, $print_flag, $no_time_flag); 
     } 

     if ($debug) { logMsg($logFileHandle,"closing command.", $print_flag, $no_time_flag); } 
     close($results); 
     } 
    else 
     { 
     logMsg($logFileHandle, "Couldn't open command: [$cmd].") 
     } 
    } 

#****************************************************************************** 
# Sub-routine: logMsg() 
#  Author: Ron Savage 
#  Date: 10/31/2006 
# 
# Description: 
# This sub-routine prints the msg and logs it to the log file during the 
# install process. 
#****************************************************************************** 
sub logMsg 
    { 
    my ($logFileHandle, $msg, $print_flag, $time_flag) = @_; 
    if (!defined($print_flag)) { $print_flag = 1; } 
    if (!defined($time_flag)) { $time_flag = 1; } 

    my $logMsg; 

    if ($time_flag) 
     { $logMsg = "[" . timeStamp() . "] $msg\n"; } 
    else 
     { $logMsg = "$msg\n"; } 

    if (defined($logFileHandle)) { print $logFileHandle $logMsg; } 

    if ($print_flag) { print $logMsg; } 
    } 
2

perlipc(進程間通信)的幾件事情可以做。管道打開並且IPC :: Open3非常方便。

0

與在其輸入和輸出完全控制運行一個子進程的最簡單方法是IPC::Open2模塊(或IPC::Open3,如果你想捕捉STDERR爲好),但問題如果你想一次處理多個數據,或者特別是如果你想在GUI中處理數據,就會被阻塞。如果你只是做一個<$fh>類型的閱讀,它會阻止,直到你有輸入,可能會楔住你的整個用戶界面。如果子進程是交互式的,那麼它會更糟糕,因爲您可能很容易死鎖,同時子進程和父進程等待另一個進程的輸入。您可以編寫自己的select循環並執行非阻塞I/O,但這並不值得。我的建議是使用POE,POE::Wheel::Run與子進程對接,並使用POE::Loop::Gtk將POE包含到GTK runloop中。