tags:

views:

2453

answers:

8

I'm sure there's some trivial one-liner with perl, ruby, bash whatever that would let me run a command in a loop until I observe some string in stdout, then stop. Ideally, I'd like to capture stdout as well, but if it's going to console, that might be enough.

The particular environment in question at the moment is RedHat Linux but need same thing on Mac sometimes too. So something, generic and *nixy would be best. Don't care about Windows - presumably a *nixy thing would work under cygwin.

UPDATE: Note that by "observe some string" I mean "stdout contains some string" not "stdout IS some string".

+4  A: 

There's a bunch of ways to do this, the first that came to mind was:

OUTPUT=""; 
while [ `echo $OUTPUT | grep -c somestring` = 0 ]; do 
  OUTPUT=`$cmd`; 
done

Where $cmd is your command to execute.

For the heck of it, here's a BASH function version, so you can call this more easily if it's something you're wanting to invoke from an interactive shell on a regular basis:

function run_until () {
  OUTPUT="";
  while [ `echo $OUTPUT | grep -c $2` = 0 ]; do
    OUTPUT=`$1`;
    echo $OUTPUT;
  done
}

Disclaimer: only lightly tested, may need to do some additional escaping etc. if your commands have lots of arguments or the string contains special chars.

EDIT: Based on feedback from Adam's comment - if you don't need the output for any reason (i.e. don't want to display the output), then you can use this shorter version, with less usage of backticks and therefore less overhead:

OUTPUT=0; 
while [ "$OUTPUT" = 0 ]; do 
  OUTPUT=`$cmd | grep -c somestring`;
done

BASH function version also:

function run_until () {
  OUTPUT=0; 
  while [ "$OUTPUT" = 0 ]; do 
    OUTPUT=`$1 | grep -c $2`; 
  done
}
Jay
You test a variable for being empty by piping it to grep in a sub-shell??? You can't be serious! Despite the fact that a shell can test for this easily (every shell can) and that code is way too complicated, it is also ultra inperformant.
Mecki
I don't think he's testing if it is empty, he's using grep to determine if the variable contains "somestring". But he could reduce the number of sub-shells by half by moving the piped grep to the following line where it's assigned to OUTPUT, then WHILE would be testing for an empty variable.
Adam Bellaire
seems like this will always fail the first while test as OUTPUT is empty to start with?
Alex Miller
I'm assuming the original poster's question about running a command in until a certain string on stdout was literal. So I'm not checking for empty stdout, I'm checking for a specific string (or not) in stdout. The while loop is checking for # of ocurrences of the search string in the output.
Jay
Oh, no I meant stdout *contains* some string. Sorry...
Alex Miller
Just to be clear: the code samples above check for the search string occuring anywhere in the output, and keep running the command until that string is found. I think that's what you wanted?
Jay
As noted in comments to other answers, the loop can be simplified much more to: until command | grep -s some_string ; do sleep 1; done ... the shell executes the pipeline until the grep exits with a 0 (success, found) status. The sleep 1 one possibility; a colon (do nothing) command is another.
Jonathan Leffler
@Jonathan - cool! I didn't know BASH supported the until keyword, well, until now ;) nice syntax addition
Jay
+1  A: 
while (/bin/true); do
  OUTPUT=`/some/command`
  if [[ "x$OUTPUT" != "x" ]]; then
    echo $OUTPUT
    break
  fi

  sleep 1
done
zigdon
So, x here is the string to search for? If I was searching for foo, I'd do: if [[ "foo$OUTPUT" != "foo" ]]; then?
Alex Miller
If "x$OUTPUT" == "x", then $OUTPUT is empty. This just checks if there is *any* output.
Michael Myers
+1  A: 

EDIT: My original answer was assuming that "some string" means "any string". If you need to look for a specific one, Perl is probably your best option, since almost nothing can beat Perl when it comes to REGEX matching.

However, if you can't use Perl for any reason (you can expect Perl to be present in most Linux distros, but nobody forces a user to install it, though Perl may not be available), you can do it with the help of grep. However, some of the grep solutions I have seen so far are suboptimal (they are slower than would be necessary). In that case I would rather do the following:

MATCH=''; while [[ "e$MATCH" == "e" ]]; do MATCH=`COMMAND | grep "SOME_STRING"`; done; echo $MATCH

Replace COMMAND with the actually command to run and *SOME_STRING* with the string to search. If SOME_STRING is found in the output of COMMAND, the loop will stop and print the output where SOME_STRING was found.

ORIGINAL ANSWER:

Probably not the best solution (I'm no good bash programmer), but it will work :-P

RUN=''; while [[ "e$RUN" == "e" ]]; do RUN=`XXXX`; done ; echo $RUN

Just replace XXXX with your command call, e.g. try using "echo" and it will never return (as echo never prints anything to stdout), however if you use "echo test" it will terminate at once and finally print out test.

Mecki
[[ "e$MATCH" == "e" ]] is much less clear than [[ -n "$MATCH" ]]
Charles Duffy
+10  A: 

In Perl:

#!/usr/local/bin/perl -w

if (@ARGV != 2)
{
    print "Usage: watchit.pl <cmd> <str>\n";
    exit(1);
}

$cmd = $ARGV[0];
$str = $ARGV[1];

while (1)
{
    my $output = `$cmd`;
    print $output; # or dump to file if desired
    if ($output =~ /$str/)
    {
        exit(0);
    }
}

Example:

[bash$] ./watchit.pl ls stop
watchit.pl
watchit.pl~
watchit.pl
watchit.pl~
... # from another terminal type "touch stop"
stop 
watchit.pl
watchit.pl~

You might want to add a sleep in there, though.

Derek Park
I accepted this one because a) it worked for me right away and b) it was easy to hack on to add a few things (like a counter, etc). But several of the other solutions here look good too.
Alex Miller
+1  A: 
CONT=1; while [ $CONT -gt 0 ]; do $CMD | tee -a $FILE | grep -q $REGEXP; CONT=$? ; done

The tee command can capture stdout in a pipe while still passing the data on, and -a makes it append to the file instead of overwriting it every time. grep -q will return 0 if there was a match, 1 otherwise and doesn't write anything to stdout. $? is the return value of the previous command, so $CONT will be the return value of grep in this case.

skoob
Much simpler: until $CMD | tee -a $FILE | grep -q $REGEXP; do sleep 1; done ... The shell will execute the pipeline repeatedly until the exit status of the grep is successful. No need for $CONT or explicit refereneces to $?.
Jonathan Leffler
Oh, and I'm not sure that '-q' is portable; POSIX mandates '-s' for exit with status 0 (found) or 1 (not found).
Jonathan Leffler
A: 

A simple way to do this would be

until `/some/command`
do
  sleep 1
done

The backticks around the command make the until test for some output to be returned rather than testing the exit value of the command.

Dave Webb
Only tests exit status, not stdout; thus does not answer question.
Charles Duffy
You missed the backticks around the command which change what the until evaluates. I'll add an edit to clarify this.
Dave Webb
The until loop runs /some/command and executes its output, testing the exit status. This is inherently broken.
Jonathan Leffler
+1  A: 

grep -c 99999 will print 99999 lines of context for the match (I assume this will be enough):

while true; do /some/command | grep expected -C 99999 && break; done

or

until /some/command | grep expected -C 9999; do echo -n .; done

...this will print some nice dots to indicate progress.

ordnungswidrig
+4  A: 

I'm suprised I haven't seen a brief perl one-liner mentioned here:

perl -e 'do { sleep(1); $_ = `command`; print $_; } until (m/search/);'

Perl is a really nice language for stuff like this. Replace "command" with the command you want to repeatedly run. Replace "search" with what you want to search for. If you want to search for something with a slash in it, then replace m/search/ with m#search string with /es#.

Also, perl runs on lots of different platforms, including Win32, and this will work wherever you have a perl installation. Just change your command appropriately.

Mark Santesson