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

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.

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]})

string comparison is shell script

I have a scenario to copy file from one server to another, for that i need to check any existing scp is in progress, have wrote a sample shell script but the condition is not being met even though syntax is correct, the main problem here is the output of ps command will gets stored in variable scpstat and the same compared for matching string in if statement, here I'm getting the output of the variable is different from executing outside of the script. can see it is formatted different in script execution when executing sh -x scpsamp.sh, why there is "sh" appended to the output, but while comparing without ps and assigning as scpstat='scp' i can able to get the condition correct, am i doing anything wrong while getting output in to the variable. please help
#!/bin/sh
scpstat=`ps -ef | grep scp | egrep -v 'grep|ssh' | awk '{print $8}')`
if [ "$scpstat" = "scp" ];
then
echo "SCP is in progress"
else
echo "No SCP in progress"
fi
sh -x output
It's notoriously difficult to extract information from the output of ps. If your system has pgrep, it's much easier:
if pgrep scp >/dev/null
then
echo "SCP is in progress"
else
echo "No SCP in progress"
fi

bash script to accept log on stdin and email log if inputting process fails

I'm a sysadmin and I frequently have a situation where I have a script or command that generates a lot of output which I would only like to have emailed to me if the command fails. It's pretty easy to write a script that runs the command, collects the output and emails it if the command fails, but I was thinking I should be able to write a command that
1) accepts log info on stdin
2) waits for the inputting process to exit and see what it's exit status was
3a) if the inputting process exited cleanly, append the logging input to a normal log file
3b) if the inputting process failed, append the logging input to the normal log and also send me an email.
It would look something like this on the command line:
something_important | mailonfail.sh me#example.com /var/log/normal_log
That would make it really easy to use in crontabs.
I'm having trouble figuring out how to make my script wait for the writing process and evaluate how that process exits.
Just to be exatra clear, here's how I can do it with a wrapper:
#! /bin/bash
something_important > output
ERR=$!
if [ "$ERR" -ne "0" ] ; then
cat something_important | mail -s "something_important failed" me#example.com
fi
cat something_important >> /var/log/normal_log
Again, that's not what I want, I want to write a script and pipe commands into it.
Does that make sense? How would I do that? Am I missing something?
Thanks Everyone!
-Dylan
Yes it does make sense, and you are close.
Here are some advises:
#!/bin/sh
TEMPFILE=$(mktemp)
trap "rm -f $TEMPFILE" EXIT
if [ ! something_important > $TEMPFILE ]; then
mail -s 'something goes oops' -a $TEMPFILE you#example.net
fi
cat $TEMPFILE >> /var/log/normal.log
I won't use bashisms so /bin/sh is fine
create a temporary file to avoid conflicts using mktemp(1)
use trap to remove file when the script exit, normally or not
if the command fail
then attach the file, which would or would not be preferred over embedding it
if it's a big file you could even gzip it, but the attachment method will change:
# using mailx
gzip -c9 $TEMPFILE | uuencode fail.log.gz | mailx -s subject ...
# using mutt
gzip $TEMPFILE
mutt -a $TEMPFILE.gz -s ...
gzip -d $TEMPFILE.gz
etc.

My $? command succeeded code fails, why?

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.

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.

Resources