4

I have the following line in bash.

(sleep 1 ; echo "foo" ; sleep 1 ; echo "bar" ; sleep 30) | nc localhost 2222 \
| grep -m1 "baz"

This prints "baz" (if/when the other end of the TCP connection sends it) and exits after 32 seconds.

What I want it to do is to exit the sleep 30 early if it sees "baz". The -m flag exits grep, but does not kill the whole line.

How could I achieve this in bash (without using expect if possible)?


Update: the code above does quit, if and only if, the server tries to send something after baz. This does not solve this problem, as the server may not send anything for minutes.

fadedbee
  • 42,671
  • 44
  • 178
  • 308
  • How can it exit the sleep 30 early ? It won't be grepping anything whilst sleeping, all it will be doing is sleeping. What you probably want is it to sleep a short amount of time and have a counter to 30 that increments each time. –  Nov 06 '14 at 11:12
  • It will be grepping from the `stdout` of `nc` i.e. whatever the server sends. – fadedbee Nov 06 '14 at 11:20
  • My point still stands, nothing is happening whilst it is sleeping –  Nov 06 '14 at 11:24
  • Could you explain how the grep works (immediately) in `(echo "GET / HTTP/1.1 \r\n" ; sleep 120) | nc www.google.com 80 | grep html`, as I have probably misunderstood your point. I thought you were implying that sleep would cause the grep to have nothing to do. Just because there is nothing going into `nc` does not mean that there is nothing coming out. I understood that commands in a pipeline run concurrently. – fadedbee Nov 06 '14 at 11:31
  • Yes, commands in a pipeline run concurrently, hence it is possible to somehow "move from one to the other". Each pipe spawns a subshell, which is waiting for input and processes it as soon as possible. – fedorqui Nov 06 '14 at 11:49
  • My point was that the sleep function is completely separate from the grep, it is sleeping no matter what, if you were to put it in a loop though with a counter you could break out when grep succeeds. I don't think using a pipe here is the best option. –  Nov 06 '14 at 11:55

4 Answers4

3

You can catch the pid of the subshell you are opening. Then, something like this should make:

( echo "start"; sleep 1; echo $BASHPID > /tmp/subpid; echo "hello"; sleep 20; ) \
  | ( sleep 1; subpid=$(cat /tmp/subpid); grep -m1 hello && kill $subpid )

That is, you store the id of the subshell in a temp file and then continue with the descripting.

On the other side of the pipe, you read the content of the file (sleep 1 is to make sure it has been written in the file by the initial subshell) and, when you find the content with grep, you kill it.


From man bash:

BASHPID

Expands to the process ID of the current bash process. This differs from $$ under certain circumstances, such as subshells that do not require bash to be re-initialized.

Credits to:

Community
  • 1
  • 1
fedorqui
  • 275,237
  • 103
  • 548
  • 598
  • Thanks for taking the time to answer. I dislike using temporary files like that, but I'll accept your answer if a better one does not appear. – fadedbee Nov 06 '14 at 11:36
  • Yeah, me also. I tried but could not find an alternate way to communicate the ID from one side to the other. – fedorqui Nov 06 '14 at 11:39
3

If you like esoteric sides of Bash, you can use coproc for that.

coproc { { sleep 1; echo "foo"; sleep 1; echo "bar"; sleep 30; } | nc localhost 2222; }
grep -m1 baz <&${COPROC[0]}
[[ $COPROC_PID ]] && kill $COPROC_PID

Here, we're using coproc to run

{ { sleep 1; echo "foo"; sleep 1; echo "bar"; sleep 30; } | nc localhost 2222; }

in the background. coproc takes care to redirect the standard output and standard input of this compound command in the file descriptors set in ${COPROC[0]} and ${COPROC[1]}. Moreover, the PID of this job is in COPROC_PID. We then feed grep with the standard output of the background job. It's then easy to kill the job when we're done.

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
1

Suddenly found a solution based on Jidder`s comment.

(sleep 1 ; echo "foo" ; sleep 1 ; echo "bar" ; for i in `seq 1 30`; do echo -n '.'; sleep 1; done) | grep -m1 "bar"

Just sleeping in a loop does not work. But after adding echo -n '.' it works. It seems that an attempt to write to a closed pipe leads to abort. Though I have tested without nc.

user3132194
  • 2,381
  • 23
  • 17
  • Once you add `nc` to this, it requires that the server replies to the dot in order for the abort to happen. I haven't figured out why. – fadedbee Nov 07 '14 at 11:42
  • It is because there are two pipes: (echo/sleep)>nc and nc>grep. Grep closes its pipe, while the first pipe where dots are destinated is still opened. If you try to avoid expect while dealing with http, you could try other method, I'm doing it like this http://pastebin.com/Est348Hb . – user3132194 Nov 10 '14 at 04:19
1

I believe you really need to use expect ( http://expect.sourceforge.net/ , and there are packages for most OSes and distributions ).

Otherwise you'll have a hard time handling some cases and getting rid of buffering, etc. Expect does it for you (... well, once you wrote the right except script that handles all (or most) cases) (For a first draft, you can use autoexpect (http://linux.die.net/man/1/autoexpect) but you'll need to add variations (handling "wrong password" messages, etc))

Expect is an old tool (and is based, iirc, on Tcl), but there is not really a best tool for the job of "sending input and waiting for outputs (and reacting differently depending on outputs)"

Olivier Dulac
  • 3,695
  • 16
  • 31