If this were C, you'd set the file descriptors non-blocking and use the select() system call to tell when one file descriptor or the other has data ready to be read at any particular time.
The closest shell has to non-blocking I/O is terminal timeouts, which really isn't the same thing.
You could read both and cram them into a fifo I suppose... A bit of a kludge but fifos can combine two writers into one reader. As long as you write entire lines to the fifo, they ought to come out in the order you wrote them.
(That's why I didn't use cat to read from the terminal. It might not care about line boundaries. It might even block until it has x lines input or EOF...)
#!/bin/sh
# Create fifo
mkfifo /tmp/$$.fifo
# Read the logfile in the background, cram into fifo
( tail -f server.log > /tmp/$$.fifo ) &
P1="$!"
# Read from the fifo and use it here, in the background
( while [ -e /tmp/$$.fifo ] && IFS="" read LINE
do
echo "got line $LINE"
[ "$LINE" = "USER quit" ] && break
done < /tmp/$$.fifo
echo "Quitting" >&2
rm /tmp/$$.fifo
kill "$P1"
kill "$$" ) &
# Save its PID in case we need to kill it
P2="$!"
# Guarantee we kill all our background processes on exit
trap "rm -f /tmp/$$.fifo ; kill $P1 $P2 ; wait" EXIT
# Read from terminal, write to fifo.
# this MUST happen in the foreground!
while [ -e "/tmp/$$.fifo" ] && IFS="" read LINE
do
echo "USER $LINE"
done > /tmp/$$.fifo
This will spam error messages when you quit about killing nonexistent processes, and things dying uncleanly... but if it didn't at least try to kill them there's situation it could leave things hanging around.
---------- Post updated at 12:46 PM ---------- Previous update was at 12:23 PM ----------
Here's a less kludgy version which avoids the spammy messages, uses a pipe instead of a fifo, and puts the combined read at the bottom:
#!/bin/bash
( tail -f server.log &
trap "" SIGPIPE
while read LINE && echo "USER $LINE"
do
true
done
echo "SIGPIPE happened I guess, quitting" >&2
kill $!
wait
) | while read INPUT
do
[ "$INPUT" = "USER quit" ] && break
echo "got $INPUT"
done
'tail -f' runs in the background, writing to standard output -- i.e. the pipe -- by itself.
I trap SIGPIPE so that the subshell doesn't get killed instantly when the 'while read INPUT' loop quits. That might prevent it from killing the background 'tail -f', or at the very least, print warning messages about the subshell being killed abnormally.
Meanwhile the other while loop reads from the terminal and echoes lines straight back out to standard output -- i.e. the pipe again, the same pipe.
The subshell should detect when the main reading loop quits by detecting when 'echo' fails, allowing it to kill the 'tail -f' cleanly before terminating normally.