chbrown
chbrown

Reputation: 12028

Capture output of error in bash exec I/O redirection

I want to open a TCP socket in bash, using the /dev/tcp/host/port psuedo-device syntax, but I need to recover gracefully if the host/port endpoint is not listening. This is easy if all I'm doing is sending data to the socket, or pulling data from it, since I can just use one-off redirection, i.e.:

echo 'ping' 2> >(logger) >/dev/tcp/127.0.0.1/8080

Or, reading from the socket:

cat 2> >(logger) </dev/tcp/127.0.0.1/8080

Where logger is the standard logger that writes lines from stdin to syslog.

The problem is that in some cases I need to write a request to the socket and get a response back. So I need to open a file descriptor for the socket with exec, but in this case, capturing the "-bash: connect: Connection refused" error is harder, since I don't want to redirect all output in the shell, which is what 2> >(logger) does when used in the context of exec. The following works great if the endpoint is down (since bash hits an error when opening the socket, and resets all redirections), but renders my shell pretty useless if it is able to make the connection.

exec 2> >(logger) 3<>/dev/tcp/127.0.0.1/8080

So far, the best solution I've come up with is the following, where I open a file descriptor (fd 9) pointing to the original STDERR, redirect STDERR (fd 2) to my logger, open the socket on fd 3, then set fd 2 back to the original STDERR, and close my temporary STDERR alias (fd 9).

exec 9>&2 2> >(logger) 3<>/dev/tcp/127.0.0.1/8080 2>&9 9>&-

Which seems like a lot of work for something so simple. Is there a way to tell bash that I want to redirect STDERR while bash runs the exec command, instead of considering the redirection as a literal argument to exec?


Please keep your answers / suggestions limited to bash (up to bash 4.3.30). I know about telnet, netcat, socat, etc. The idea for the project I'm working on (where this bash issue came up) arose from frustration at how slow a particular tool is, which is due to how it proxies all of its shell helpers off to external (python) interpreters. Also, I'd like to avoid mkfifo if possible.

Upvotes: 1

Views: 1793

Answers (1)

Hans Kl&#252;nder
Hans Kl&#252;nder

Reputation: 2292

eval 'exec 3<>/dev/tcp/127.0.0.1/8080' 2> >(logger)

or, much better because it does not destroy the old file descriptor 3:

eval 'exec {fd}<>/dev/tcp/127.0.0.1/8080' 2> >(logger)

which uses the smallest free file descriptor and assigns it to fd.

unset fd
eval 'exec {fd}<>/dev/tcp/127.0.0.1/8080' 2> >(logger)

if test $? != 0 # status of the last command - 0 if the connection succeeded
then
   echo cannot open connection >&2
fi

# $? shows the last status only. Later you can check success this way:

if test -z "$fd"
then
   echo could not open connection >&2 # past tense because it's later now
else
   echo some data to transmit >&$fd
   IFS= read -r just_on_line_of_response <&$fd
fi

Once you are done, delete the connection using

exec {fd}>&-

This will work as well:

eval "exec $fd>&-"

But this fails miserably plus closes your stdout:

exec $fd>&-

Upvotes: 1

Related Questions