【问题标题】:How can I read the output from external commands in real time in Perl?如何在 Perl 中实时读取外部命令的输出?
【发布时间】:2009-08-05 22:26:40
【问题描述】:

我运行了一些 bash 脚本,但它们可能需要几个小时才能完成,在此期间它们会喷出下载速度、ETA 和类似信息。我需要在 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.
  }

【问题讨论】:

  • 您确实应该使用适当的事件循环,例如 POE,而不是线程。使用 POE::Wheel::Run 将比您自己手动滚动的几乎事件循环获得更好的成功。 (我会推荐 AnyEvent::Subprocess,但它正在进行重大重构,不会立即解决您的问题。)

标签: perl bash stdin


【解决方案1】:

代替线程和``,使用:

 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 从 26513 开始:2009 年 8 月 7 日星期五 08:48:06 CEST 从 26513 开始:2009 年 8 月 7 日星期五 08:48:07 CEST 从 26513 开始:2009 年 8 月 7 日星期五 08:48:08 CEST 从 26513 开始:2009 年 8 月 7 日星期五 08:48:09 CEST 从 26513 开始:2009 年 8 月 7 日星期五 08:48:10 CEST

现在,让我们写一个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;
    }
}

如您所见,没有分叉,也没有线程。这就是它的工作原理:

=> 时间 ./multi-test.pl 从 28596 开始:2009 年 8 月 7 日星期五 09:05:54 CEST 从 28599 开始:2009 年 8 月 7 日星期五 09:05:54 CEST 从 28596 开始:2009 年 8 月 7 日星期五 09:05:55 CEST 从 28599 开始:2009 年 8 月 7 日星期五 09:05:55 CEST 从 28596 开始:2009 年 8 月 7 日星期五 09:05:56 CEST 从 28599 开始:2009 年 8 月 7 日星期五 09:05:56 CEST 从 28596 开始:2009 年 8 月 7 日星期五 09:05:57 CEST 从 28599 开始:2009 年 8 月 7 日星期五 09:05:57 CEST 从 28596 开始:2009 年 8 月 7 日星期五 09:05:58 CEST 从 28599 开始:2009 年 8 月 7 日星期五 09:05:58 CEST 真实0m5.128s 用户 0m0.060s 系统 0m0.076s

【讨论】:

  • 这看起来是迄今为止最干净的解决方案,非常感谢。我当前的(hacky 和不太正常的)代码应该不需要太多的工作就可以像你提供的那样完美地工作。再次感谢。
【解决方案2】:

反引号和 qx// 运算符都会阻塞,直到子进程完成。您需要在管道上打开 bash 脚本。如果您需要它们是非阻塞的,请将它们作为文件句柄打开,必要时使用 open2 或 open3,然后将句柄放入 select() 并等待它们变得可读。

我刚刚遇到了一个类似的问题——我有一个运行时间很长的进程(一项可以运行数周的服务),我用 qx// 打开了它。问题是这个程序的输出最终超出了内存限制(在我的架构上大约为 2.5G)。我通过在管道上打开子命令来解决它,然后只保存最后 1000 行输出。在此过程中,我注意到 qx// 表单仅在命令完成后打印输出,但管道表单能够在发生时打印输出。

我手头没有代码,但如果你能等到明天,我会发布我所做的。

【讨论】:

    【解决方案3】:

    请参阅perlipc(进程间通信)了解您可以做的几件事。管道打开和 IPC::Open3 很方便。

    【讨论】:

      【解决方案4】:

      是的,你可以。

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

      问题是某些应用程序不会逐行输出信息,而是更新一行直到完成。是你的情况吗?

      【讨论】:

      • 这些行不是来自标准输入,但我可以使用 open 命令打开脚本的一行,这样就可以了。我的问题是,当没有输入时,我希望能够做一些事情,而不是等待输入一行(这就是那个循环所做的)。目前我正在运行一个非常复杂的线程组合来实现这一点(我将更新问题以显示这一点)。
      • 除非您在 windows 上运行脚本,否则您可以使用 select 来测试文件描述符中是否有任何可用数据。大致是这样的:while (1) { my $rin = ''; vec($rin,fileno(STDIN),1) = 1;我的 ($nfound, $timeleft) = 选择($rin, undef, undef, 0); if ($nfound) { 我的 $data; print "得到数据!\n";系统读取标准输入,$data,1024;打印“数据:$数据\n”; } else { 打印 "等待...\n";睡眠(1); } }
      【解决方案5】:

      这里是用于显示进度条的 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了解更多信息

      【讨论】:

      • 哦。我的。这就是矫枉过正的定义。
      • 生活和学习...关于 SO 的好处之一是我不是这里最聪明的人。
      【解决方案6】:

      我使用这个子例程和方法来记录我的外部命令。它是这样称呼的:

      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; }
         }
      

      【讨论】:

        【解决方案7】:

        运行子进程并完全控制其输入和输出的最简单方法是 IPC::Open2 模块(如果您还想捕获 STDERR,则为 IPC::Open3),但如果您想处理多个一次,或者特别是如果您想在 GUI 中执行此操作,则正在阻塞。如果您只是执行&lt;$fh&gt; 类型的读取,它将阻塞,直到您输入,可能会楔入您的整个 UI。如果子进程是交互式的,那就更糟了,因为你很容易死锁,子进程和父进程都在等待对方的输入。您可以编写自己的 select 循环并进行非阻塞 I/O,但这并不值得。我的建议是使用POEPOE::Wheel::Run 与子进程交互,并使用POE::Loop::Gtk 将 POE 包含到 GTK 运行循环中。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-11-11
          • 2017-07-05
          • 2010-10-19
          • 1970-01-01
          • 1970-01-01
          • 2016-06-19
          • 2013-12-26
          相关资源
          最近更新 更多