tags:

views:

468

answers:

7

I have a few bash scripts I run, but they can take several hours to finish, during which time they spew out download speeds, ETAs and similar information. I need to capture this information in perl, but I am running into a problem, I cannot read the output line by line(unless I'm missing something).

Any help working this out?

EDIT: to explain this a little better I'm running several bash scripts along side each other, I wish to use gtk with perl to produce handy progress bars. At present I'm running 2 threads for every bash script I wish to run, one master thread for updating the graphical information. It looks something like this(cut down as much as I possibly can):

  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  A: 

yes, you can.

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

The problem is that some applications does not spew out info line by line but update one line till they're finished. Is it your case?

gonzo
The lines aren't coming from the standard in, but I can open a line to the script using the open command, so that will work. My problem is that when no input is given I want to be able to do something other than wait for a line to be input(which is what that loop does).At the moment I'm running a very convoluted combo of threads to achieve this(I'll update the question to show this).
scragar
Unless you're running script on windows, you can use select to test if there is any data available in file descriptor. Roughly something like this:while (1) { my $rin = ''; vec($rin,fileno(STDIN),1) = 1; my ($nfound, $timeleft) = select($rin, undef, undef, 0); if ($nfound) { my $data; print "Got data!\n"; sysread STDIN, $data, 1024; print "DATA: $data\n"; } else { print "wait...\n"; sleep(1); }}
gonzo
+3  A: 

Backticks and the qx// operator both block until the sub-process finishes. You need to open the bash scripts on a pipe. If you need them to be non-blocking, open them as filehandles, using open2 or open3 if necessary, then put the handles into a select() and wait for them to become readable.

I just ran into a similar problem -- I had a very long-running process (a service that could run for weeks) that I opened with a qx//. The problem was that the output of this program eventually exceeded memory limits (around 2.5G on my architecture). I solved it by opening the sub-command on a pipe, then only saving the last 1000 lines of output. In doing so, I noticed that the qx// form only print the output once the command completed, but the pipe form was able to print output as it happened.

I don't have the code handy, but if you can wait until tomorrow, I'll post what I did.

Andrew Barnett
+1  A: 

Here it is with the GTK2 code for displaying the progress bars.

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

see the perl GTK2 docs for more info

plastic chris
Oh. My. That's the definition of overkill.
depesz
Live and learn... one of the better things about SO is that I'm not the smartest person here.
plastic chris
+1  A: 

I use this sub routine and method to log my external commands. It's called like this:

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

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

close($logFileHandle);

and here are the sub-routines:

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

Ron

Ron Savage
+2  A: 

See the perlipc (interprocess communication) for several things you can do. Piped opens and IPC::Open3 are handy.

brian d foy
+4  A: 

Instead of threads, and ``, use:

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

In this way open several filehandles (as many as many programs you need to run) and then use IO::Select to poll data from them.

Simplistic example.

Let's assume I have shell script that looks like this:

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

it's output might look like this:

=> ./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

Now, let's write a 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;
    }
}

As you can see there are no forks, no threads. And this is how it works:

=> 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
depesz
This looks to be the cleanest solution by far so far, thanks a lot.It shouldn't take too much work for my current(hacky and not quite working) code to work perfectly as you've provided.Thanks again.
scragar
A: 

The simplest way to run a child process with full control over its input and output is the IPC::Open2 module (or IPC::Open3 if you want to capture STDERR as well), but the issue if you want to deal with multiple at once, or especially if you want to do it in a GUI, is blocking. If you just do a <$fh> type read it's going to block until you have input, potentially wedging your whole UI. If the child process is interactive it's even worse because you can easily deadlock, with both the child and the parent waiting for input from the other. You can write your own select loop and do nonblocking I/O, but it's not really worth it. My suggestion would be to use POE, POE::Wheel::Run to interface with the child processes, and POE::Loop::Gtk to subsume POE into the GTK runloop.

hobbs