Home > database >  In bash, how do I start an application, read from its standard output, execute something else while
In bash, how do I start an application, read from its standard output, execute something else while

Time:06-08

I have a server application that prints to standard output something along Listening on http://localhost:12345, and then waits until its output is closed (e.g. via Ctrl-C). I want to start it, get its address, run commands using it, and then close or kill it. How can I do this?

I came up with this:

trap 'kill $(jobs -p)' EXIT
server > tempfile &
url=$(tail -f tempfile | perl -nle "m/(http\S )/; print \$1; exit 0;")
run-thing --using-server="$url"

Runnable code that approximates the above:

$ cat test.sh
trap 'kill $(jobs -p)' EXIT
python3 -uc 'import time; print("Listening on http://localhost:123"); time.sleep(99)' > tempfile &
url=$(tail -f tempfile | perl -nle "m/(http\S )/; print \$1; exit 0;")
echo "using server at $url, running jobs: $(jobs -p)"
$ bash test.sh; ps aux | grep [p]ython || echo server successfully killed
using server at http://localhost:123, running jobs: 2898
server successfully killed

I feel that this approach is pretty awful for a number of reasons. What else can I do?

CodePudding user response:

Using coprocesses

trap 'pkill -P $COPROC_PID' EXIT
coproc { server; }
url=$(<&${COPROC[0]} perl -nle 'm/(http\S )/; print $1; exit 0;')
run-thing --using-server="$url"

Runnable code that approximates the above:

$ cat test.sh
trap 'pkill -P $COPROC_PID' EXIT
coproc { python3 -uc 'import time; print("Listening on http://localhost:123"); time.sleep(99)'; }
url=$(<&${COPROC[0]} perl -nle 'm/(http\S )/; print $1; exit 0;')
echo "using server at $url, server $(pgrep python >/dev/null && echo is running)"
$ bash test.sh; ps aux | pgrep python || echo server successfully killed
using server at http://localhost:123, server is running
Terminated
server successfully killed

Using process substitution

trap "kill 0" EXIT
{ url=$(perl -nle 'm/(http\S )/; print $1; exit 0;'); } < <(server)
run-thing --using-server="$url"

This was found thanks to the helpful advice from libera's #bash. Runnable code that approximates the above:

$ cat test.sh
trap "kill 0" EXIT
{ url=$(perl -nle 'm/(http\S )/; print $1; exit 0;'); } \
< <(python3 -uc 'import time; print("Listening on http://localhost:123"); time.sleep(99)')
echo "using server at $url, server $(pgrep python >/dev/null && echo is running)"
$ bash test.sh; ps aux | pgrep python || echo server successfully killed
using server at http://localhost:123, server is running
server successfully killed

I'm not entirely sure why I need {}, which is a command grouping. It's like () but it doesn't spawn a subshell. kill 0 will kill all processes in the current process group. This may kill the process that started current script. You can also kill the server directly. As <() does spawn a subshell, you can find it by parent:

{ url=$(perl -nle 'm/(http\S )/; print $1; exit 0;'); 
  run-thing --using-server="$url";
} < <(server)
pkill -P $!

CodePudding user response:

You could use a fifo as a custom file descriptor (#3 in the code) for that:

tmpdir=$(mktemp -d)
mkfifo "$tmpdir/fifo"
exec 3<> "$tmpdir/fifo"

tail -f tempfile | perl -lne 'm/(http\S )/; print $1; exit 0;' >&3 &

IFS='' read -r url <&3   # will block until one line is read

printf 'We got this: %s\n' "$url"

kill %1   # kill the background job
exec 3>&-
rm -rf "$tmpdir"

remark: with tail -f file | somecommand you might not get any output (for an undefined amount of time) because of somecommand internal buffering. That said, you did work around that with the exit 0 of your perl one-liner.

  •  Tags:  
  • bash
  • Related