My $? command succeeded code fails, why? - bash

In my bash script, I am trying to have rsync retry 10 times if it looses connection to its destination before giving up.
I know this code traps all errors but I can live with that for now.
My code is:
lops="0"
while true; do
let "lops++"
rsync $OPT $SRCLINUX $TRG 2>&1 | tee --append ${LOGFILE}
if [ "$?" -eq "0" ]; then
echolog "rsync finished WITHOUT error after $lops times"
break
else
echolog "Re-starting rsync for the ${lops}th time due to ERRORS"
fi
if [[ "$lops" == "10" ]]; then
echolog "GAVE UP DUE TO TOO MANY rsync ERRORS - BACKUP NOT FINISHED"
break
fi
done
It does not work as expected, here is a what happens on the first error:
TDBG6/
rsync: connection unexpectedly closed (131505 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(226) [sender=3.1.1]
rsync finished WITHOUT error after 1 times
Is this because the $? contains the return value of tee, NOT rsync?
How can I fix this? (I am personally Linux syntax limited :)

I see at least 2 possibilities to fix your problem:
use PIPESTATUS:
An array variable containing a list of exit status values from the processes in the most-recently-executed foreground pipeline (which may contain only a single command).
Use it as:
rsync $OPT $SRCLINUX $TRG 2>&1 | tee --append ${LOGFILE}
if (( PIPESTATUS[0] == 0 )); then
use rsync's --log-file option.
Notes:
you have lots of quotes missing in your code!
don't use uppercase variable names.
Don't use let and use ((...)) for arithmetic.

In addition to the other suggestions for working around the pipe confounding your exit code, you can avoid the pipe by using process substitution like so:
rsync "$OPT" "$SRCLINUX" "$TRG" &> >( tee --append "${LOGFILE}" )
which will redirect both stdout and stderr (that's the &> part) into a "file" that is connected to stdin of the process within the >(...), in this case the tee command you want. So it's very much like the pipe, but without the pipe (the pipe connects stdout of the left to stdin of the right behind the scenes, we've just pushed it out in the open here).

I believe that the status is the status of the last command in the pipeline.
I don't know how to deal with this in general, but for this specific case you can just redirect the output of the whole loop:
while true; do
...
done | tee -a "$LOGFILE"
Depending on what it's for, this may also mean you don't need the "echolog" function.

Related

Not able to fetch the exit status of a multiple commands (separated by PIPE) which got assigned to a variable

Below is the sample script which i am trying to execute; but it fails to fetch the exit status of $cmd; is there any other way to fetch its exit status..!?
cmd="curl -mddddddd google.com"
status=$($cmd | wc -l)
echo ${PIPESTATUS[0]}
I know that, if i replace status=$($cmd | wc -l) with $cmd | wc -l , i could fetch the exit status of $cmd using PIPESTATUS. But in my case i have to assign it to a variable (example: status in above case).
Please help me here..!
Regards,
Rohith
What you're assigning to the status variable is not a status, but what $cmd | wc -l pipeline prints to standard output.
Why do you echo anyway? Try realstatus=${PIPESTATUS[0]}.
EDIT (After some digging and RTFMing...):
Just this -- realstatus=${PIPESTATUS[0]} -- doesn't seem to help, since $(command_substitution), which is in your code, is done "in a subshell environment", while PIPESTATUS is about "the most-recently-executed foreground pipeline"
If what you're trying to do in this particular case is to ensure the curl (aka $cmd) command was succesfull in the pipeline you should probably make use of pipefail option (see here).
If the output of the command is text and not excessively large, the simplest way to get the status of the command is to not use a pipe:
cmd_output=$($cmd)
echo "'$cmd' exited with $?"
linecount=$(wc -l <<<"$cmd_output")
echo "'wc' exited with $?"
What counts as "excessively large" depends on the system, but I successfully tested the code above with a command that generated 50 megabytes (over one million lines) of output on an old Linux system.
If the output of the command is too big to store in memory, another option is to put it in a temporary file:
$cmd >tmpfile
echo "'$cmd' exited with $?"
linecount=$(wc -l <tmpfile)
echo "'wc' exited with $?"
You need to be careful when using temporary files though. See Creating temporary files in Bash and How create a temporary file in shell script?.
Note that, as with the OP's example code, the unquoted $cmd in the code examples above is dangerous. It should not be used in real code.
If you just want to echo the pipe status, you can redirect that to stderr. But you have to do it in the subshell.
status=$($cmd | wc -l; echo ${PIPESTATUS[0]} >&2)
Or you can capture both variables from the subshell using read
read -rd $'\0' status pstatus <<<$($cmd | wc -l; echo ${PIPESTATUS[0]})

How to write to and parse STDIN/STDOUT/temporary buffer?

First I don't know if I talk about STDIN out STDOUT, but this is what I want to achieve :
There's a program that export database from distant server and send output as gzipped content.
I want to unzip the content, parse it.
If it's OK then import it, otherwise send an error message. I don't want to write to any temporary file on disk so I want to handle things directly from STD
someExportCommand > /dev/stdin #seems not work
#I want to write a message here
echo "Export database done"
cat /dev/stdin > gunzip > /dev/stdin
echo "Unzip done"
if [[ "$mycontentReadFromSTDIN" =* "password error" ]]; then
echo "error"
exit 1
fi
#I want to echo that we begin impor"
echo "Import begin"
cat /dev/stdin | mysql -u root db
#I want to echo that import finished
echo "Import finish"
The challenge here is not to write to a physical file. It's easier if it's the case but I want to do the hard way. Is it possible and how?
A literal implementation of what you're asking for (not a good idea, but doing exactly what you asked) might look like the following:
This is a bad idea for several reasons:
If a database is large enough to be bothering with, trying to fit it in memory, and especially in a shell variable is a bad idea.
In order to fit binary data into a shell variable, it needs to be encoded (as with base64, or uunencode, or other tools). This makes it even larger than it was before, and also adds performance overhead
...however, the bad-idea code, as requested:
#!/usr/bin/env bash
set -o pipefail # if any part of a command fails, count the whole thing a failure
if exportOutputB64=$(someExportCommand | base64); then
echo "Export database done" >&2
else
echo "Export database reports failure" >&2
exit 1
fi
if exportOutputDecompressedB64=$(base64 --decode <<<"$exportOutputB64" | gunzip -c | base64); then
echo "Decompress of export done" >&2
else
echo "Decompress of export failed" >&2
exit 1
fi
unset exportOutputB64
if grep -q 'password error' < <(base64 --decode <<<"$exportOutputDecompressedB64"); then
echo "Export contains password error; considering it a failure" >&2
exit 1
fi
echo "Import begin"
mysql -u root db < <(base64 --decode <<<"$exportOutputDecompressedB64")
If I were writing this myself, I'd just set up a pipeline that processes the whole thing in-place, and uses pipefail to ensure that errors in early stages are detected:
set -o pipefail
someExportCommand | gunzip -c | mysql -u root db
The important thing about a pipeline is that all parts of it run at the same time. Thus, someExportCommand is still running when mysql -u root db starts. Consequently, there's no need for a large buffer anywhere (in memory, or on disk) to store your database contents.
The requirement to not use a temporary file seems extremely misdirected; but you can avoid it by reading into a shell variable, or perhaps an array.
Any error message is likely to be on stderr, not stdin. But you should examine the program's exit status instead of looking for whether it prints an error message.
#!/bin/bash
result=$(someExportCommand) || exit
At this point, the script will have exited if there was a failure; and otherwise, result contains its output.
Now, similarly to error messages, status messages, too, should be printed to standard error, not standard output. A common convention is also to include the name of the script in the message.
echo "$0: Import begin" >&2
Now, pass the variable to mysql.
mysql -u root db <<<"$result"
Notice that the <<<"here string" syntax is a Bash feature; you can't use it with /bin/sh. If you need the script to be portable to sh, the standard solution is still to use a pipe;
printf '%s\n' "$result" | mysql -u root db
Finally, print the status message to stderr again.
echo "$0: Import finished" >&2
Using a shell variable for a long string is not particularly efficient or elegant; capturing the output into a temporary file is definitely the recommended approach.

Bash script: `exit 0` fails to exit

So I have this Bash script:
#!/bin/bash
PID=`ps -u ...`
if [ "$PID" = "" ]; then
echo $(date) Server off: not backing up
exit
else
echo "say Server backup in 10 seconds..." >> fifo
sleep 10
STARTTIME="$(date +%s)"
echo nosave >> fifo
echo savenow >> fifo
tail -n 3 -f server.log | while read line
do
if echo $line | grep -q 'save complete'; then
echo $(date) Backing up...
OF="./backups/backup $(date +%Y-%m-%d\ %H:%M:%S).tar.gz"
tar -czhf "$OF" data
echo autosave >> fifo
echo "$(date) Backup complete, resuming..."
echo "done"
exit 0
echo "done2"
fi
TIMEDIFF="$(($(date +%s)-STARTTIME))"
if ((TIMEDIFF > 70)); then
echo "Save took too long, canceling backup."
exit 1
fi
done
fi
Basically, the server takes input from a fifo and outputs to server.log. The fifo is used to send stop/start commands to the server for autosaves. At the end, once it receives the message from the server that the server has completed a save, it tar's the data directory and starts saves again.
It's at the exit 0 line that I'm having trouble. Everything executes fine, but I get this output:
srv:scripts $ ./backup.sh
Sun Nov 24 22:42:09 EST 2013 Backing up...
Sun Nov 24 22:42:10 EST 2013 Backup complete, resuming...
done
But it hangs there. Notice how "done" echoes but "done2" fails. Something is causing it to hang on exit 0.
ADDENDUM: Just to avoid confusion for people looking at this in the future, it hangs at the exit line and never returns to the command prompt. Not sure if I was clear enough in my original description.
Any thoughts? This is the entire script, there's nothing else going on and I'm calling it direct from bash.
Here's a smaller, self contained example that exhibits the same behavior:
echo foo > file
tail -f file | while read; do exit; done
The problem is that since each part of the pipeline runs in a subshell, exit only exits the while read loop, not the entire script.
It will then hang until tail finds a new line, tries to write it, and discovers that the pipe is broken.
To fix it, you can replace
tail -n 3 -f server.log | while read line
do
...
done
with
while read line
do
...
done < <(tail -n 3 -f server.log)
By redirecting from a process substitution instead, the flow doesn't have to wait for tail to finish like it would in a pipeline, and it won't run in a subshell so that exit will actually exits the entire script.
But it hangs there. Notice how "done" echoes but "done2" fails.
done2 won't be printed at all since exit 0 has already ended your script with return code 0.
I don't know the details of bash subshells inside loops, but normally the appropriate way to exit a loop is to use the "break" command. In some cases that's not enough (you really need to exit the program), but refactoring that program may be the easiest (safest, most portable) way to solve that. It may also improve readability, because people don't expect programs to exit in the middle of a loop.

How can I use read timeouts with stat?

I have the following code:
#!/bin/bash
read -t1 < <(stat -t "/my/mountpoint")
if [ $? -eq 1 ]; then
echo NFS mount stale. Removing...
umount -f -l /my/mountpoint
fi
How do I mute the output of stat while at the same time being still able to detect its error level in the subsequent test?
Adding >/dev/null 2>&1 inside the subshell, or in the end of the read line does not work. But there must be a way...
Thanks for any insights on this!
Use Command-Subsitution, Not Process Substitution
Instead of reading in from process subsitution, consider using command substitution instead. For example:
mountpoint=$(stat -t "/my/mountpoint" 2>&1)
This will silence the output by storing standard output in a variable, but leave the results retrievable by dereferencing $mountpoint. This approach also leaves the exit status accessible through $?.
A Clearer Alternative
Alternatively, you might just rewrite this more simply as:
mountpoint="/my/mountpoint"
if stat -t "$mountpoint" 2>&-
then
echo "NFS mount stale. Removing..."
umount -f -l "$mountpoint"
fi
To me, this seems more intention-revealing and less error-prone, but your mileage may certainly vary.
(Ab)using Read Timeouts
In the comments, the OP asked whether read timeouts could be abused to handle hung input from stat. The answer is yes, if you close standard error and check for an empty $REPLY string. For example:
mountpoint="/my/mountpoint"
read -t1 < <(stat -t "$mountpoint" 2>&-)
if [[ -n "$REPLY" ]]; then
echo "NFS mount stale. Removing..."
umount -f -l "$mountpoint"
fi
This works for several reasons:
When using the read builtin in Bash:
If no NAMEs are supplied, the line read is stored in the REPLY variable.
With standard error closed, $REPLY will be empty unless stat returns something on standard output, which it won't if it encounters an error. In other words, you're checking the contents of the $REPLY string instead of the exit status from read.
I think I got it! The redirection mentioned in your response seems to work within the subshell without wiping out the return code like 2>&1 did. So this works as expected:
read -t1 < <(rpcinfo -t 10.0.128.1 nfs 2>&-)
if [ $? -eq 0 ]; then
echo "NFS server/service available!"
else
echo "NFS server/service unavailable!"
fi
Where 10.0.128.1 is a 'bad' IP (no server/service responding). The script times out within a second and produces "NFS server/service unavailable!" response, but no output from rpcinfo. Likewise, when the IP is good, the desired response is output.
I upvoted your response!

Catching errors in Bash with glassfish commands [return code in pipes]

I am writing a bash script to manage deployments to a GF server for several environments. What I would like to know is how can I get the result of a GF command and then determine whether to continue or exit.
For example
Say I want to redeploy, I have this script
$GF_ASADMIN --port $GF_PORT redeploy --name $EAR_FILE_NAME --keepstate=true $EAR_FILE | tee -a $LOG
The variables are already defined. So GF will start to redeploy and either suceed or fail. I want to check if it does and act accordingly. I have this right after it.
RC=$?
if [[ $RC -eq 0 ]];
then echoInfo "Application Successfully redeployed!" | tee -a $LOG;
else
echoError "Failed to redeploy application!"
exit 1
fi;
However, it doesnt really seem to work .
The problem is the pipe
$GF_ASADMIN ... | tee -a $LOG
$? reflects the return code of tee.
Your are looking for PIPESTATUS. See man bash:
PIPESTATUS
An array variable (see Arrays below) containing a list of exit
status values from the processes in the most-recently-executed
foreground pipeline (which may contain only a single command).
See also this example to clarify the PIPESTATUS
false | true
echo ${PIPESTATUS[#]}
Output is: 1 0
The corrected code is:
RC=${PIPESTATUS[0]}
Or try using a code block redirect, for example:
{
if "$GF_ASADMIN" --port $GF_PORT redeploy --name "$EAR_FILE_NAME" --keepstate=true "$EAR_FILE"
then
echo Info "Application Successfully redeployed!"
else
echo Error "Failed to redeploy application!" >&2
exit 1
fi
} | tee -a "$LOG"

Resources